修复小程序讲解页面返回回退
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-28 10:40:25 +08:00
parent 0b3a31f556
commit 8b3e3f8d55
10 changed files with 281 additions and 82 deletions

View File

@@ -6,6 +6,7 @@ import { flushPromises, mount } from '@vue/test-utils'
const mocks = vi.hoisted(() => ({
onLoadHandler: null as null | ((options?: Record<string, unknown>) => Promise<void>),
onBackPressHandler: null as null | (() => boolean),
onHideHandler: null as null | (() => void),
onUnloadHandler: null as null | (() => void),
enterExplainDetail: vi.fn(),
@@ -17,7 +18,9 @@ const mocks = vi.hoisted(() => ({
close: vi.fn(),
seekToPercent: vi.fn(),
setPlaybackRate: vi.fn(),
toggleMute: vi.fn()
toggleMute: vi.fn(),
embeddedInWechatMiniProgram: false,
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
}))
const audioState = vi.hoisted(() => ({
@@ -36,6 +39,9 @@ vi.mock('@dcloudio/uni-app', () => ({
onLoad: (handler: (options?: Record<string, unknown>) => Promise<void>) => {
mocks.onLoadHandler = handler
},
onBackPress: (handler: () => boolean) => {
mocks.onBackPressHandler = handler
},
onHide: (handler: () => void) => {
mocks.onHideHandler = handler
},
@@ -74,7 +80,11 @@ vi.mock('@/composables/useGlobalAudioPlayer', () => ({
}))
vi.mock('@/utils/hostEnvironment', () => ({
isEmbeddedInWechatMiniProgram: () => false
isEmbeddedInWechatMiniProgram: () => mocks.embeddedInWechatMiniProgram
}))
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
returnToWechatMiniProgram: () => mocks.returnToWechatMiniProgram()
}))
import ExhibitDetail from '@/pages/exhibit/detail.vue'
@@ -106,6 +116,7 @@ const exhibit = {
describe('讲解详情音频优先布局', () => {
beforeEach(() => {
mocks.onLoadHandler = null
mocks.onBackPressHandler = null
mocks.onHideHandler = null
mocks.onUnloadHandler = null
audioState.currentAudio.value = null
@@ -131,6 +142,9 @@ describe('讲解详情音频优先布局', () => {
playInfo: { title: exhibit.name }
})
mocks.play.mockResolvedValue(undefined)
mocks.embeddedInWechatMiniProgram = false
mocks.returnToWechatMiniProgram.mockReset()
mocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'failed', reason: 'bridge-unavailable' })
vi.stubGlobal('uni', {
showToast: vi.fn(),
navigateBack: vi.fn(),
@@ -375,6 +389,28 @@ describe('讲解详情音频优先布局', () => {
expect(uni.redirectTo).not.toHaveBeenCalled()
})
it('小程序直达讲解详情时返回宿主,不进入 H5 讲解对象列表', async () => {
mocks.embeddedInWechatMiniProgram = true
mocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
const wrapper = mount(ExhibitDetail, {
global: { stubs: { GuidePageFrame: GuidePageFrameStub } }
})
await mocks.onLoadHandler?.({
id: exhibit.id,
targetType: 'STOP',
targetId: 'stop-1',
hallId: 'hall-1',
hallName: '恐龙厅'
})
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
await flushPromises()
expect(mocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(uni.redirectTo).not.toHaveBeenCalled()
})
it('离开详情页时立即关闭当前讲解播放器', async () => {
audioState.currentAudio.value = { id: 'audio-1', audioUrl: exhibit.audioUrl }
audioState.currentSource.value = { targetType: 'STOP', targetId: 'stop-1', lang: 'en-US' }
@@ -407,9 +443,6 @@ describe('讲解详情音频优先布局', () => {
})
await flushPromises()
vi.mocked(uni.navigateBack).mockImplementationOnce((options: any) => {
options.fail?.()
})
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
await flushPromises()
@@ -418,6 +451,6 @@ describe('讲解详情音频优先布局', () => {
expect(url.split('?')[0]).toBe('/pages/explain/guide-stop-list')
expect(params.get('hallId')).toBe('hall-1')
expect(params.get('hallName')).toBe('恐龙厅')
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
expect(uni.navigateBack).not.toHaveBeenCalled()
})
})

View File

@@ -7,15 +7,35 @@ import ExplainGuideStopListPage from '@/pages/explain/guide-stop-list.vue'
import { explainUseCase } from '@/usecases/explainUseCase'
const testState = vi.hoisted(() => ({
onLoadHandler: null as null | ((options?: Record<string, string>) => void)
onLoadHandler: null as null | ((options?: Record<string, string>) => void),
onBackPressHandler: null as null | (() => boolean)
}))
const hostEnvironmentMocks = vi.hoisted(() => ({
embeddedInWechatMiniProgram: false
}))
const miniProgramReturnMocks = vi.hoisted(() => ({
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
}))
vi.mock('@dcloudio/uni-app', () => ({
onLoad: (handler: (options?: Record<string, string>) => void) => {
testState.onLoadHandler = handler
},
onBackPress: (handler: () => boolean) => {
testState.onBackPressHandler = handler
}
}))
vi.mock('@/utils/hostEnvironment', () => ({
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
}))
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
returnToWechatMiniProgram: () => miniProgramReturnMocks.returnToWechatMiniProgram()
}))
vi.mock('@/usecases/explainUseCase', () => ({
explainUseCase: {
listGuideStopsPageByHall: vi.fn().mockResolvedValue({
@@ -53,6 +73,10 @@ const mountGuideStopList = () => mount(ExplainGuideStopListPage, {
beforeEach(() => {
testState.onLoadHandler = null
testState.onBackPressHandler = null
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
miniProgramReturnMocks.returnToWechatMiniProgram.mockReset()
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'failed', reason: 'bridge-unavailable' })
vi.stubGlobal('uni', {
navigateBack: vi.fn(),
reLaunch: vi.fn(),
@@ -155,6 +179,19 @@ describe('讲解对象列表返回', () => {
wrapper.unmount()
})
it('小程序直达展厅时返回宿主,不重启 H5 展厅列表', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
const wrapper = mountGuideStopList()
await wrapper.get('[data-testid="list-back"]').trigger('click')
await flushPromises()
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).not.toHaveBeenCalled()
wrapper.unmount()
})
it('页面栈返回失败时回退至独立展厅列表', async () => {
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/explain/list', options: {} },

View File

@@ -6,8 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import ExplainListPage from '@/pages/explain/list.vue'
const hostEnvironmentMocks = vi.hoisted(() => ({
embeddedInWechatMiniProgram: false,
navigateBackToWechatMiniProgram: vi.fn(() => false)
embeddedInWechatMiniProgram: false
}))
const miniProgramReturnMocks = vi.hoisted(() => ({
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
}))
const testState = vi.hoisted(() => ({
@@ -25,8 +28,11 @@ vi.mock('@dcloudio/uni-app', () => ({
}))
vi.mock('@/utils/hostEnvironment', () => ({
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram,
navigateBackToWechatMiniProgram: () => hostEnvironmentMocks.navigateBackToWechatMiniProgram()
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
}))
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
returnToWechatMiniProgram: () => miniProgramReturnMocks.returnToWechatMiniProgram()
}))
vi.mock('@/usecases/explainUseCase', () => ({
@@ -62,8 +68,8 @@ const mountExplainList = () => mount(ExplainListPage, {
beforeEach(() => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReset()
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(false)
miniProgramReturnMocks.returnToWechatMiniProgram.mockReset()
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'failed', reason: 'bridge-unavailable' })
testState.onLoadHandler = null
testState.onBackPressHandler = null
window.location.hash = '#/pages/explain/list'
@@ -100,7 +106,7 @@ describe('讲解展厅列表返回', () => {
it('小程序中的首页讲解 Tab 进入时仍优先回到项目首页', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/index/index', options: { tab: 'explain' } },
{ route: 'pages/explain/list', options: {} }
@@ -110,7 +116,7 @@ describe('讲解展厅列表返回', () => {
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).not.toHaveBeenCalled()
expect(miniProgramReturnMocks.returnToWechatMiniProgram).not.toHaveBeenCalled()
wrapper.unmount()
})
@@ -128,23 +134,24 @@ describe('讲解展厅列表返回', () => {
it('小程序直达独立入口时返回小程序,而不是重启项目首页', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program&weapp=1'
window.history.replaceState({}, '', window.location.href)
const wrapper = mountExplainList()
expect(wrapper.getComponent(ExplainHallSelectStub).props('forceInternalHeader')).toBe(false)
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
await flushPromises()
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).not.toHaveBeenCalled()
wrapper.unmount()
})
it('微信嵌入态从非讲解首页进入时返回小程序', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program'
window.history.replaceState({}, '', window.location.href)
vi.stubGlobal('getCurrentPages', vi.fn(() => [
@@ -154,9 +161,10 @@ describe('讲解展厅列表返回', () => {
const wrapper = mountExplainList()
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
await flushPromises()
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).not.toHaveBeenCalled()
wrapper.unmount()
})
@@ -216,7 +224,7 @@ describe('讲解展厅列表返回', () => {
it('小程序直达入口的浏览器返回会委托给小程序宿主', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program&weapp=1'
window.history.replaceState({}, '', window.location.href)
const wrapper = mountExplainList()
@@ -230,7 +238,7 @@ describe('讲解展厅列表返回', () => {
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(pushState).toHaveBeenCalledTimes(1)
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).not.toHaveBeenCalled()
wrapper.unmount()
})

View File

@@ -4,6 +4,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const target = { latitude: 22.604321, longitude: 114.058765, name: '停车场 A&B', address: '龙华区 #1' }
const setEmbeddedRuntime = () => Object.assign(window, { __wxjs_environment: 'miniprogram' })
const addWechatSdkMarker = () => {
const script = document.createElement('script')
script.dataset.wechatJsSdk = 'true'
document.head.appendChild(script)
}
const loadService = async () => {
vi.resetModules()
@@ -33,6 +38,7 @@ describe('微信小程序 web-view 桥接', () => {
it('等待延迟注入的 bridge 后导航到编码后的宿主页', async () => {
setEmbeddedRuntime()
addWechatSdkMarker()
const { openWechatMiniProgramLocation } = await loadService()
const result = openWechatMiniProgramLocation(target)
await vi.advanceTimersByTimeAsync(100)
@@ -84,4 +90,45 @@ describe('微信小程序 web-view 桥接', () => {
await expect(first).resolves.toEqual({ status: 'opened' })
expect(navigateTo).toHaveBeenCalledTimes(1)
})
it('等待延迟 bridge 后返回宿主小程序', async () => {
setEmbeddedRuntime()
addWechatSdkMarker()
const { returnToWechatMiniProgram } = await loadService()
const result = returnToWechatMiniProgram()
await vi.advanceTimersByTimeAsync(100)
const navigateBack = vi.fn(({ success }) => success())
Object.assign(window, {
wx: {
miniProgram: {
getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }),
navigateBack
}
}
})
await vi.advanceTimersByTimeAsync(100)
await expect(result).resolves.toEqual({ status: 'returned' })
expect(navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
})
it('原生返回失败时委托宿主页执行安全回退', async () => {
setEmbeddedRuntime()
const { MINI_PROGRAM_RETURN_MESSAGE, returnToWechatMiniProgram } = await loadService()
const postMessage = vi.fn()
Object.assign(window, {
wx: {
miniProgram: {
getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }),
navigateBack: ({ fail }: { fail?: (error: { errMsg?: string }) => void }) => fail?.({ errMsg: 'navigateBack:fail page stack is empty' }),
postMessage
}
}
})
await expect(returnToWechatMiniProgram()).resolves.toEqual({ status: 'returned' })
expect(postMessage).toHaveBeenCalledWith({
data: { type: MINI_PROGRAM_RETURN_MESSAGE }
})
})
})

View File

@@ -1,7 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
isEmbeddedInWechatMiniProgram,
navigateBackToWechatMiniProgram
isEmbeddedInWechatMiniProgram
} from '@/utils/hostEnvironment'
const createWindow = (overrides: Record<string, unknown> = {}) => ({
@@ -50,24 +49,3 @@ describe('微信小程序宿主环境识别', () => {
expect(isEmbeddedInWechatMiniProgram()).toBe(false)
})
})
describe('小程序宿主返回', () => {
it('嵌入态调用小程序 navigateBack', () => {
const navigateBack = vi.fn()
vi.stubGlobal('window', createWindow({
__wxjs_environment: 'miniprogram',
wx: { miniProgram: { navigateBack } }
}))
expect(navigateBackToWechatMiniProgram()).toBe(true)
expect(navigateBack).toHaveBeenCalledWith({ delta: 1 })
})
it('非嵌入态不会调用小程序返回桥接', () => {
const navigateBack = vi.fn()
vi.stubGlobal('window', createWindow({ wx: { miniProgram: { navigateBack } } }))
expect(navigateBackToWechatMiniProgram()).toBe(false)
expect(navigateBack).not.toHaveBeenCalled()
})
})