// @vitest-environment happy-dom import { defineComponent, nextTick, onMounted, onUnmounted } from 'vue' import { flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import IndexPage from '@/pages/index/index.vue' const testState = vi.hoisted(() => ({ threeMapMountCount: 0, threeMapUnmountCount: 0, onShowHandler: null as null | (() => Promise | void) })) vi.mock('@dcloudio/uni-app', () => ({ onLoad: vi.fn(), onShow: (handler: () => Promise | void) => { testState.onShowHandler = handler } })) vi.mock('@/utils/hostEnvironment', () => ({ isEmbeddedInWechatMiniProgram: () => false })) const modelSource = { loadPackage: async () => ({ overviewModelUrl: '', floors: [] }), loadFloorPois: async () => [] } vi.mock('@/usecases/guideUseCase', () => ({ guideUseCase: { getAssetBaseUrl: () => '/static/nav-assets', getModelSource: () => modelSource, normalizeFloorId: (value: string) => value, getFloors: async () => [] } })) vi.mock('@/usecases/guideRouteUseCase', () => ({ guideRouteUseCase: { listTargets: async () => [], searchTargets: async () => [], getRouteReadiness: async () => ({ ready: false, message: '' }), planRoute: async () => null } })) vi.mock('@/usecases/explainUseCase', () => ({ explainUseCase: { loadExplainHalls: async () => [], loadExplainHallSummaries: async () => ({}), listHalls: async () => [], searchExplain: async () => [] } })) const GuidePageFrameStub = defineComponent({ name: 'GuidePageFrame', template: '
' }) const GuideMapShellStub = defineComponent({ name: 'GuideMapShell', props: { mapType: { type: String, default: 'indoor' } }, emits: ['initial-model-ready', 'initial-model-failed', 'mode-change'], setup() { onMounted(() => { testState.threeMapMountCount += 1 }) onUnmounted(() => { testState.threeMapUnmountCount += 1 }) return {} }, template: `
` }) const stubs = { GuidePageFrame: GuidePageFrameStub, GuideMapShell: GuideMapShellStub, PoiSearchPanel: true, RoutePlannerPanel: true, ArrivalPanel: true, ExplainHallSelect: true } const mountIndex = async () => { const wrapper = mount(IndexPage, { global: { stubs } }) await flushPromises() return wrapper } const initialModelReady = { view: 'overview' as const, floorId: 'L1' } const initialModelFailed = { view: 'overview' as const, floorId: 'L1', message: 'network failed' } beforeEach(() => { vi.useFakeTimers() testState.threeMapMountCount = 0 testState.threeMapUnmountCount = 0 testState.onShowHandler = null window.history.replaceState({}, '', '/#/pages/index/index?tab=guide') vi.stubGlobal('uni', { navigateTo: vi.fn(), navigateBack: vi.fn(), reLaunch: vi.fn(), showToast: vi.fn(), hideKeyboard: vi.fn() }) }) afterEach(() => { vi.useRealTimers() vi.restoreAllMocks() vi.unstubAllGlobals() }) describe('首页启动遮罩与馆内模型生命周期', () => { it('12 秒超时后仍保持 indoor,且不会挂载 TencentMap', async () => { const wrapper = await mountIndex() await vi.advanceTimersByTimeAsync(12000) await vi.advanceTimersByTimeAsync(360) expect(wrapper.getComponent(GuideMapShellStub).props('mapType')).toBe('indoor') expect(wrapper.get('[data-testid="three-map"]').exists()).toBe(true) expect(wrapper.find('[data-testid="tencent-map"]').exists()).toBe(false) expect(testState.threeMapMountCount).toBe(1) expect(testState.threeMapUnmountCount).toBe(0) wrapper.unmount() }) it('超时后的迟到 initialModelReady 仍可完成当前 ThreeMap 加载', async () => { const wrapper = await mountIndex() const shell = wrapper.getComponent(GuideMapShellStub) await vi.advanceTimersByTimeAsync(12000) shell.vm.$emit('initial-model-ready', initialModelReady) await nextTick() await vi.advanceTimersByTimeAsync(360) expect(shell.props('mapType')).toBe('indoor') expect(wrapper.find('.app-launch-overlay').exists()).toBe(false) expect(testState.threeMapMountCount).toBe(1) expect(testState.threeMapUnmountCount).toBe(0) wrapper.unmount() }) it('initialModelFailed 后保留 ThreeMap,重试就绪不产生额外挂载', async () => { const wrapper = await mountIndex() const shell = wrapper.getComponent(GuideMapShellStub) shell.vm.$emit('initial-model-failed', initialModelFailed) await nextTick() await vi.advanceTimersByTimeAsync(360) expect(shell.props('mapType')).toBe('indoor') expect(wrapper.get('[data-testid="three-map"]').exists()).toBe(true) expect(wrapper.find('[data-testid="tencent-map"]').exists()).toBe(false) shell.vm.$emit('initial-model-ready', initialModelReady) await nextTick() expect(testState.threeMapMountCount).toBe(1) expect(testState.threeMapUnmountCount).toBe(0) wrapper.unmount() }) it('仅在用户主动切换到馆外 2D 时才挂载 TencentMap', async () => { const wrapper = await mountIndex() const shell = wrapper.getComponent(GuideMapShellStub) shell.vm.$emit('mode-change', '2d') await nextTick() expect(shell.props('mapType')).toBe('outdoor') expect(wrapper.get('[data-testid="tencent-map"]').exists()).toBe(true) shell.vm.$emit('mode-change', '3d') await nextTick() expect(shell.props('mapType')).toBe('indoor') wrapper.unmount() }) })