// @vitest-environment happy-dom import { defineComponent } from 'vue' import { mount } from '@vue/test-utils' import { describe, expect, it, vi } from 'vitest' import GuideMapShell from '@/components/navigation/GuideMapShell.vue' import { clearGuidePerformanceRecords, getGuidePerformanceRecords } from '@/services/performance/guidePerformance' const ThreeMapStub = defineComponent({ name: 'ThreeMap', props: { initialFloorId: { type: String, default: '' }, initialView: { type: String, default: '' }, visiblePoiIds: { type: Array, default: null }, targetFocus: { type: Object, default: null } }, emits: [ 'floor-change', 'target-focus', 'auto-switch', 'render-mode-fallback', 'semantic-exterior-exit' ], setup(_, { expose }) { expose({ switchFloor: vi.fn(), showMultiFloor: vi.fn(async () => true), getGuideViewportState: vi.fn(() => ({ scene: 'floor', floorId: 'L1', centerX: 29.0598, centerZ: 23.9424, visibleWorldSpan: 270.39, revision: 1 })) }) return {} }, template: '
' }) const TencentJsMapStub = defineComponent({ name: 'TencentJsMap', emits: ['semantic-enter-building'], template: '
' }) const modelSource = { loadPackage: async () => ({ overviewModelUrl: '', floors: [] }), loadFloorPois: async () => [] } const mountShell = (visiblePoiIds: string[] | null) => mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [ { id: 'L1', label: '1F' }, { id: 'L2', label: '2F' } ], activeFloor: 'L1', visiblePoiIds }, global: { stubs: { ThreeMap: ThreeMapStub } } }) describe('GuideMapShell 点位过滤契约', () => { it('馆内渲染模式使用单个开关,二维外观不显示楼层列表', async () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [{ id: 'L1', label: '1F' }, { id: 'L2', label: '2F' }], activeFloor: 'L1', indoorView: 'overview', indoorInitialView: 'overview', indoorRenderMode: 'two-d', showIndoorRenderModeToggle: true, showFloor: true, showFloorHeader: true, showLayerModeToggle: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) const toggle = wrapper.get('[data-testid="indoor-render-mode-toggle"]') expect(toggle.text()).toBe('2D') expect(wrapper.findAll('[data-testid="indoor-render-mode-toggle"]')).toHaveLength(1) expect(wrapper.find('.floor-switcher').exists()).toBe(false) expect(wrapper.find('.floor-header').exists()).toBe(false) expect(wrapper.find('.layer-mode-toggle').exists()).toBe(false) await toggle.trigger('tap') expect(wrapper.emitted('indoorRenderModeChange')).toEqual([['three-d']]) expect(wrapper.emitted('sceneViewportChange')).toBeUndefined() }) it('二维单层视图仍显示楼层栏,楼层栏只取决于业务场景', () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [{ id: 'L1', label: '1F' }, { id: 'L2', label: '2F' }], activeFloor: 'L1', indoorView: 'floor', indoorRenderMode: 'two-d', showFloor: true, showFloorHeader: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) expect(wrapper.find('.floor-switcher').exists()).toBe(true) expect(wrapper.find('.floor-header').exists()).toBe(false) }) it('外观路线和起点选择状态也不会显示室内楼层栏', () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [{ id: 'L1', label: '1F' }, { id: 'L2', label: '2F' }], activeFloor: 'L1', indoorView: 'overview', showFloor: true, showRoute: true, routeStartSelectionActive: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) expect(wrapper.find('.floor-switcher').exists()).toBe(false) }) it.each([ ['不过滤时透传 null', null], ['无结果时透传空列表', []], ['有结果时透传精确 ID', ['poi-a', 'poi-b']] ])('%s', (_label, visiblePoiIds) => { const wrapper = mountShell(visiblePoiIds as string[] | null) expect(wrapper.getComponent(ThreeMapStub).props('visiblePoiIds')).toEqual(visiblePoiIds) }) it('原样转发跨楼层与目标聚焦结果', async () => { const wrapper = mountShell(['poi-l2']) const renderer = wrapper.getComponent(ThreeMapStub) const focusResult = { requestId: 3, poiId: 'poi-l2', floorId: 'L2', status: 'focused' as const } renderer.vm.$emit('floor-change', 'L2') renderer.vm.$emit('target-focus', focusResult) await wrapper.vm.$nextTick() expect(wrapper.emitted('floorChange')).toEqual([['L2']]) expect(wrapper.emitted('targetFocus')).toEqual([[focusResult]]) }) it('仅在语义缩放启用时转发室内外层级切换事件', async () => { const indoorWrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [{ id: 'L1', label: '1F' }], activeFloor: 'L1', semanticZoomEnabled: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) indoorWrapper.getComponent(ThreeMapStub).vm.$emit('semantic-exterior-exit') await indoorWrapper.vm.$nextTick() expect(indoorWrapper.emitted('semanticExitOutdoor')).toEqual([[]]) const outdoorWrapper = mount(GuideMapShell, { props: { mapType: 'outdoor', indoorModelSource: modelSource, floors: [{ id: 'L1', label: '1F' }], activeFloor: 'L1', semanticZoomEnabled: true }, global: { stubs: { TencentJsMap: TencentJsMapStub, TencentMap: true } } }) const viewport = { latitude: 22.692763, longitude: 114.363572, zoom: 18.5 } outdoorWrapper.getComponent(TencentJsMapStub).vm.$emit('semantic-enter-building', viewport) await outdoorWrapper.vm.$nextTick() expect(outdoorWrapper.emitted('semanticEnterBuilding')).toEqual([[viewport]]) }) it('语义缩放关闭时不转发地图层级切换事件', async () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [{ id: 'L1', label: '1F' }], activeFloor: 'L1', semanticZoomEnabled: false }, global: { stubs: { ThreeMap: ThreeMapStub } } }) wrapper.getComponent(ThreeMapStub).vm.$emit('semantic-exterior-exit') await wrapper.vm.$nextTick() expect(wrapper.emitted('semanticExitOutdoor')).toBeUndefined() }) it('忽略旧渲染版本的 WebP 回退事件', async () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [{ id: 'L1', label: '1F' }, { id: 'L2', label: '2F' }], activeFloor: 'L1', indoorView: 'floor', sceneRevision: 2 }, global: { stubs: { ThreeMap: ThreeMapStub } } }) const renderer = wrapper.getComponent(ThreeMapStub) renderer.vm.$emit('render-mode-fallback', { mode: 'two-d', view: 'floor', floorId: 'L2', reason: 'model-load-timeout', sceneRevision: 1 }) await wrapper.vm.$nextTick() expect(wrapper.emitted('indoorRenderModeFallback')).toBeUndefined() }) it('动态楼层业务视图不会覆盖首次外观模型初始化视图', () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [ { id: 'L1', label: '1F' }, { id: 'L2', label: '2F' } ], activeFloor: 'L2', indoorView: 'floor', indoorInitialView: 'overview' }, global: { stubs: { ThreeMap: ThreeMapStub } } }) const renderer = wrapper.getComponent(ThreeMapStub) expect(renderer.props('initialFloorId')).toBe('L2') expect(renderer.props('initialView')).toBe('overview') }) it('外观状态点击当前业务楼层仍请求渲染器,且等待 floor-change 才提交业务视图', async () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [{ id: 'L2', label: '2F' }], activeFloor: 'L2', indoorView: 'overview', indoorInitialView: 'overview' }, global: { stubs: { ThreeMap: ThreeMapStub } } }) const renderer = wrapper.getComponent(ThreeMapStub) await (wrapper.vm as any).switchFloor('L2') expect((renderer.vm as any).switchFloor).toHaveBeenCalledWith('L2') expect(wrapper.emitted('indoorViewChange')).toBeUndefined() renderer.vm.$emit('floor-change', 'L2') await wrapper.vm.$nextTick() expect(wrapper.emitted('indoorViewChange')).toEqual([['floor']]) }) it('measures a manual floor switch until ThreeMap commits the target floor', async () => { clearGuidePerformanceRecords() const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [ { id: 'L1', label: '1F' }, { id: 'L2', label: '2F' } ], activeFloor: 'L1', indoorView: 'floor' }, global: { stubs: { ThreeMap: ThreeMapStub } } }) const renderer = wrapper.getComponent(ThreeMapStub) ;(wrapper.vm as any).switchFloor('L2') renderer.vm.$emit('floor-change', 'L2') await wrapper.vm.$nextTick() expect(getGuidePerformanceRecords()).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: 'interaction', name: 'floor-switch', outcome: 'success', detail: expect.objectContaining({ floorId: 'L2', source: 'manual' }) }) ])) }) it('起点选择阶段处于外观视图时仍隐藏室内楼层栏', () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [ { id: 'L1', label: '1F' }, { id: 'L2', label: '2F' } ], activeFloor: 'L1', indoorView: 'overview', showFloor: true, routeStartSelectionActive: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) expect(wrapper.find('.floor-switcher').exists()).toBe(false) }) it('ThreeMap 自动切换由父级提交后隐藏楼层栏', async () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [ { id: 'L1', label: '1F' }, { id: 'L2', label: '2F' } ], activeFloor: 'L1', indoorView: 'floor', showFloor: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) const renderer = wrapper.getComponent(ThreeMapStub) expect(wrapper.find('.floor-switcher').exists()).toBe(true) renderer.vm.$emit('auto-switch', { from: 'floor', to: 'overview', trigger: 'zoom-out', distance: 320 }) await wrapper.vm.$nextTick() expect(wrapper.find('.floor-switcher').exists()).toBe(true) expect(wrapper.emitted('autoSwitch')).toHaveLength(1) await wrapper.setProps({ indoorView: 'overview' }) expect(wrapper.find('.floor-switcher').exists()).toBe(false) }) it('多层视图只显示单层切换按钮,不显示单层楼层列表', () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [ { id: 'L1', label: '1F' }, { id: 'L2', label: '2F' } ], activeFloor: 'L2', indoorView: 'multi', layerMode: 'multi', showFloor: true, showFloorHeader: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) expect(wrapper.find('.floor-header-label').text()).toBe('单层') expect(wrapper.find('.floor-list').exists()).toBe(false) }) it('父组件楼层短暂为空时,仍用最后提交的楼层返回单层', async () => { const wrapper = mount(GuideMapShell, { props: { mapType: 'indoor', indoorModelSource: modelSource, floors: [ { id: 'L1', label: '1F' }, { id: 'L2', label: '2F' } ], activeFloor: 'L1', indoorView: 'floor', layerMode: 'single', showFloor: true, showFloorHeader: true }, global: { stubs: { ThreeMap: ThreeMapStub } } }) const renderer = wrapper.getComponent(ThreeMapStub) await (wrapper.vm as any).setLayerMode('multi') await wrapper.setProps({ indoorView: 'multi', layerMode: 'multi', activeFloor: '' }) await (wrapper.vm as any).setLayerMode('single') await new Promise((resolve) => setTimeout(resolve, 0)) expect((renderer.vm as any).switchFloor).toHaveBeenCalledWith('L1') }) })