// @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 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('@/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 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('首页通过内部页面栈进入时,顶部返回使用 navigateBack 回到导览首页', async () => { vi.stubGlobal('getCurrentPages', vi.fn(() => [ { route: 'pages/index/index', options: { tab: 'guide' } }, { 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() 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('独立页在微信嵌入态强制提供 H5 返回,并保留宿主识别参数', async () => { hostEnvironmentMocks.embeddedInWechatMiniProgram = true 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(true) 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&embedded=wechat-mini-program&weapp=1' })) 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: {} } ])) 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('navigateBack 失败后重启到导览首页', 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('根栈只创建一条历史标记,浏览器返回会回到导览首页', async () => { hostEnvironmentMocks.embeddedInWechatMiniProgram = true 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() window.dispatchEvent(new PopStateEvent('popstate', { state: {} })) await flushPromises() expect(uni.navigateBack).not.toHaveBeenCalled() expect(pushState).toHaveBeenCalledTimes(1) const url = vi.mocked(uni.reLaunch).mock.calls[0]?.[0]?.url || '' const query = new URLSearchParams(url.split('?')[1]) expect(url.split('?')[0]).toBe('/pages/index/index') expect(query.get('tab')).toBe('guide') expect(query.get('embedded')).toBe('wechat-mini-program') expect(query.get('weapp')).toBe('1') 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() }) })