// @vitest-environment happy-dom import { defineComponent } from 'vue' import { flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import ExplainListPage from '@/pages/explain/list.vue' const hostEnvironmentMocks = vi.hoisted(() => ({ embeddedInWechatMiniProgram: false })) const miniProgramBridgeMocks = vi.hoisted(() => ({ returnToWechatMiniProgram: vi.fn() })) const testState = vi.hoisted(() => ({ onLoadHandler: null as null | (() => void), onBackPressHandler: null as null | (() => boolean) })) vi.mock('@dcloudio/uni-app', () => ({ onLoad: (handler: () => void) => { testState.onLoadHandler = handler }, onBackPress: (handler: () => boolean) => { testState.onBackPressHandler = handler } })) vi.mock('@/utils/hostEnvironment', () => ({ isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram })) vi.mock('@/services/WechatMiniProgramBridgeService', () => ({ returnToWechatMiniProgram: miniProgramBridgeMocks.returnToWechatMiniProgram })) vi.mock('@/usecases/explainUseCase', () => ({ explainUseCase: { loadExplainHalls: async () => [], loadExplainHallSummaries: async () => ({}) } })) const GuidePageFrameStub = defineComponent({ name: 'GuidePageFrame', emits: ['back'], template: '
' }) const ExplainHallSelectStub = defineComponent({ name: 'ExplainHallSelect', props: { forceInternalHeader: Boolean }, emits: ['back'], template: '' }) const mountExplainList = () => mount(ExplainListPage, { global: { stubs: { GuidePageFrame: GuidePageFrameStub, ExplainHallSelect: ExplainHallSelectStub } } }) beforeEach(() => { hostEnvironmentMocks.embeddedInWechatMiniProgram = false miniProgramBridgeMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'not-embedded' }) testState.onLoadHandler = null testState.onBackPressHandler = null window.location.hash = '#/pages/explain/list' window.history.replaceState({}, '', window.location.href) vi.stubGlobal('uni', { navigateBack: vi.fn(), reLaunch: vi.fn(), showToast: vi.fn() }) vi.stubGlobal('getCurrentPages', vi.fn(() => [])) }) afterEach(() => { vi.restoreAllMocks() vi.unstubAllGlobals() }) describe('讲解展厅列表返回', () => { it('首页讲解 Tab 进入时,顶部返回使用 navigateBack 保留讲解 Tab', async () => { vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/index/index', options: { tab: 'explain' } }, { route: 'pages/explain/list', options: {} } ])) const wrapper = mountExplainList() testState.onLoadHandler?.() await flushPromises() await wrapper.get('[data-testid="frame-back"]').trigger('click') expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 })) expect(uni.reLaunch).not.toHaveBeenCalled() expect(miniProgramBridgeMocks.returnToWechatMiniProgram).not.toHaveBeenCalled() wrapper.unmount() }) it('页面栈只有展厅列表时使用 reLaunch 回到导览首页', async () => { const wrapper = mountExplainList() await wrapper.get('[data-testid="explain-list-back"]').trigger('click') expect(uni.navigateBack).not.toHaveBeenCalled() expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({ url: '/pages/index/index?tab=guide' })) wrapper.unmount() }) it('微信嵌入态直达页面由宿主提供标题与返回,并返回小程序', async () => { hostEnvironmentMocks.embeddedInWechatMiniProgram = true miniProgramBridgeMocks.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(uni.reLaunch).not.toHaveBeenCalled() expect(miniProgramBridgeMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1) wrapper.unmount() }) it('微信嵌入态从本项目首页进入时仍返回本项目首页', async () => { hostEnvironmentMocks.embeddedInWechatMiniProgram = true window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program' window.history.replaceState({}, '', window.location.href) vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/index/index', options: { tab: 'explain' } }, { route: 'pages/explain/list', options: {} } ])) const wrapper = mountExplainList() await wrapper.get('[data-testid="explain-list-back"]').trigger('click') expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 })) expect(uni.reLaunch).not.toHaveBeenCalled() expect(miniProgramBridgeMocks.returnToWechatMiniProgram).not.toHaveBeenCalled() wrapper.unmount() }) it('上一页不是首页时使用 reLaunch,避免回到不相关页面', async () => { vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/search/index', options: {} }, { route: 'pages/explain/list', options: {} } ])) const wrapper = mountExplainList() await wrapper.get('[data-testid="explain-list-back"]').trigger('click') expect(uni.navigateBack).not.toHaveBeenCalled() expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({ url: '/pages/index/index?tab=guide' })) wrapper.unmount() }) it('首页返回失败时重启到原讲解 Tab', async () => { vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/index/index', options: { tab: 'explain' } }, { route: 'pages/explain/list', options: {} } ])) vi.stubGlobal('uni', { navigateBack: vi.fn((options: { fail?: () => void }) => options.fail?.()), reLaunch: vi.fn(), showToast: vi.fn() }) const wrapper = mountExplainList() await wrapper.get('[data-testid="explain-list-back"]').trigger('click') expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({ url: '/pages/index/index?tab=explain' })) wrapper.unmount() }) it('导览首页返回失败后重启到原导览 Tab', async () => { vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/index/index', options: { tab: 'guide' } }, { route: 'pages/explain/list', options: {} } ])) vi.stubGlobal('uni', { navigateBack: vi.fn((options: { fail?: () => void }) => options.fail?.()), reLaunch: vi.fn(), showToast: vi.fn() }) const wrapper = mountExplainList() await wrapper.get('[data-testid="explain-list-back"]').trigger('click') expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({ url: '/pages/index/index?tab=guide' })) wrapper.unmount() }) it('重启导览首页失败时显示明确提示', async () => { vi.stubGlobal('uni', { navigateBack: vi.fn(), reLaunch: vi.fn((options: { fail?: () => void }) => options.fail?.()), showToast: vi.fn() }) const wrapper = mountExplainList() await wrapper.get('[data-testid="explain-list-back"]').trigger('click') expect(uni.showToast).toHaveBeenCalledWith({ title: '返回导览首页失败,请重试', icon: 'none' }) wrapper.unmount() }) it('微信嵌入态根栈不创建 H5 history 标记,平台返回交给宿主', async () => { hostEnvironmentMocks.embeddedInWechatMiniProgram = true miniProgramBridgeMocks.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() const pushState = vi.spyOn(window.history, 'pushState') testState.onLoadHandler?.() testState.onLoadHandler?.() await flushPromises() expect(pushState).not.toHaveBeenCalled() expect(uni.navigateBack).not.toHaveBeenCalled() expect(uni.reLaunch).not.toHaveBeenCalled() expect(testState.onBackPressHandler?.()).toBe(true) await flushPromises() expect(miniProgramBridgeMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1) wrapper.unmount() }) it('根栈页内返回先消费历史标记,再回到导览首页', async () => { let currentHistoryState: Record = {} vi.spyOn(window.history, 'state', 'get').mockImplementation(() => currentHistoryState) vi.spyOn(window.history, 'pushState').mockImplementation((state) => { currentHistoryState = (state || {}) as Record }) const wrapper = mountExplainList() const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => { currentHistoryState = {} window.dispatchEvent(new PopStateEvent('popstate', { state: currentHistoryState })) }) testState.onLoadHandler?.() expect(window.history.state).toMatchObject({ museumGuidePage: 'explain-list' }) await wrapper.get('[data-testid="explain-list-back"]').trigger('click') await flushPromises() expect(historyBack).toHaveBeenCalledTimes(1) expect(uni.reLaunch).toHaveBeenCalledTimes(1) expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({ url: '/pages/index/index?tab=guide' })) wrapper.unmount() }) it('普通 H5 浏览器返回也会回到导览首页,且不调用 history.back', async () => { const historyBack = vi.spyOn(window.history, 'back') const wrapper = mountExplainList() testState.onLoadHandler?.() window.dispatchEvent(new PopStateEvent('popstate', { state: {} })) await flushPromises() expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({ url: '/pages/index/index?tab=guide' })) expect(historyBack).not.toHaveBeenCalled() wrapper.unmount() }) it('正常内部页面栈不创建历史标记,并保留框架原有浏览器返回', async () => { vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/index/index', options: { tab: 'guide' } }, { route: 'pages/explain/list', options: {} } ])) const wrapper = mountExplainList() const pushState = vi.spyOn(window.history, 'pushState') testState.onLoadHandler?.() window.dispatchEvent(new PopStateEvent('popstate', { state: {} })) await flushPromises() expect(pushState).not.toHaveBeenCalled() expect(uni.reLaunch).not.toHaveBeenCalled() wrapper.unmount() }) it('平台返回事件复用同一返回策略并阻止默认退出', () => { const wrapper = mountExplainList() expect(testState.onBackPressHandler?.()).toBe(true) expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({ url: '/pages/index/index?tab=guide' })) wrapper.unmount() }) })