// @vitest-environment happy-dom import { defineComponent } from 'vue' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import type { MuseumPoi } from '@/domain/museum' import { parseFacilityDetailSearchContext } from '@/view-models/guideViewModels' const mocks = vi.hoisted(() => ({ onLoadHandler: null as null | ((options?: Record) => Promise), getFloors: vi.fn(), getPoiById: vi.fn(), searchPois: vi.fn(), getHallById: vi.fn(), loadPackage: vi.fn(), loadFloorPois: vi.fn() })) vi.mock('@dcloudio/uni-app', () => ({ onLoad: (handler: (options?: Record) => Promise) => { mocks.onLoadHandler = handler } })) vi.mock('@/usecases/guideUseCase', () => ({ guideUseCase: { getModelSource: () => ({ loadPackage: mocks.loadPackage, loadFloorPois: mocks.loadFloorPois }), getFloors: mocks.getFloors, getPoiById: mocks.getPoiById, searchPois: mocks.searchPois, normalizeFloorId: (value: string) => value } })) vi.mock('@/usecases/explainUseCase', () => ({ explainUseCase: { getHallById: mocks.getHallById } })) vi.mock('@/utils/guideTopTabs', () => ({ navigateToGuideTopTab: vi.fn() })) import FacilityDetail from '@/pages/facility/detail.vue' const facilityPoi: MuseumPoi = { id: 'poi-restroom-l2', name: '二层卫生间', floorId: 'floor-l2', floorLabel: '2F', primaryCategory: { id: 'restroom', label: '卫生间' }, categories: [{ id: 'restroom', label: '卫生间' }], positionGltf: [12, 3, -8], accessible: true } const cinemaPoi: MuseumPoi = { ...facilityPoi, id: 'space-cinema-l1', name: '球幕影院', floorId: 'floor-l1', floorLabel: '1F', primaryCategory: { id: 'space_theater', label: '剧场空间', iconType: 'theater' }, categories: [{ id: 'space_theater', label: '剧场空间', iconType: 'theater' }], accessible: false } const GuidePageFrameStub = defineComponent({ name: 'GuidePageFrameStub', emits: ['back', 'tab-change'], template: '
' }) const GuideMapShellStub = defineComponent({ name: 'GuideMapShellStub', props: { targetFocusRequest: { type: Object, default: null }, activeFloor: { type: String, default: '' } }, emits: ['floor-change', 'target-focus'], template: '
' }) const mountDetail = () => mount(FacilityDetail, { global: { stubs: { GuidePageFrame: GuidePageFrameStub, GuideMapShell: GuideMapShellStub } } }) const loadDetail = async (options: Record) => { expect(mocks.onLoadHandler).toBeTypeOf('function') await mocks.onLoadHandler?.(options) await flushPromises() } beforeEach(() => { mocks.onLoadHandler = null mocks.getFloors.mockResolvedValue([ { id: 'floor-l1', label: '1F', order: 1 }, { id: 'floor-l2', label: '2F', order: 2 } ]) mocks.getPoiById.mockResolvedValue(facilityPoi) mocks.searchPois.mockResolvedValue([]) mocks.getHallById.mockResolvedValue(null) mocks.loadPackage.mockResolvedValue({ overviewModelUrl: '/overview.glb', floors: [ { floorId: 'floor-l1', label: '1F', order: 1, modelUrl: '/l1.glb' }, { floorId: 'floor-l2', label: '2F', order: 2, modelUrl: '/l2.glb' } ] }) mocks.loadFloorPois.mockResolvedValue([]) vi.stubGlobal('uni', { navigateBack: vi.fn(), reLaunch: vi.fn() }) }) afterEach(() => { vi.unstubAllGlobals() }) describe('点位详情实际渲染与返回闭环', () => { it('目标楼层与点位就绪后挂载位置预览,避免默认楼层中间帧', async () => { const wrapper = mountDetail() expect(wrapper.get('[data-testid="facility-map-loading"]').text()).toContain('正在准备位置预览') await loadDetail({ id: encodeURIComponent(facilityPoi.id), target: encodeURIComponent(facilityPoi.name), floorId: facilityPoi.floorId, floorLabel: facilityPoi.floorLabel }) expect(wrapper.getComponent(GuideMapShellStub).props('activeFloor')).toBe('floor-l2') expect(wrapper.getComponent(GuideMapShellStub).props('targetFocusRequest')).toMatchObject({ poiId: facilityPoi.id, floorId: facilityPoi.floorId }) }) it('只展示真实详情字段,并发送正确的位置预览请求', async () => { const wrapper = mountDetail() await loadDetail({ id: encodeURIComponent(facilityPoi.id), target: encodeURIComponent(facilityPoi.name), floorId: facilityPoi.floorId, floorLabel: facilityPoi.floorLabel }) const sheet = wrapper.get('[data-testid="facility-detail-sheet"]') expect(sheet.classes()).toContain('guide-poi-card') expect(sheet.text()).toContain('二层卫生间') expect(sheet.text()).toContain('类别洗手间') expect(sheet.text()).toContain('楼层2F') expect(sheet.text()).not.toContain('可无障碍到达') expect(sheet.text()).not.toContain('展示点位') expect(sheet.text()).not.toContain('已在对应楼层模型中标出点位') expect(wrapper.find('.detail-restore').exists()).toBe(false) const mapShell = wrapper.getComponent(GuideMapShellStub) expect(mapShell.props('activeFloor')).toBe('floor-l2') expect(mapShell.props('targetFocusRequest')).toMatchObject({ poiId: facilityPoi.id, floorId: facilityPoi.floorId, positionGltf: [12, 3, -8] }) }) it('详情复用统一分类名称,不回退到 SDK 原始空间类别', async () => { mocks.getPoiById.mockResolvedValue(cinemaPoi) const wrapper = mountDetail() await loadDetail({ source: 'search', resultCount: '2', id: cinemaPoi.id, target: cinemaPoi.name, floorId: cinemaPoi.floorId, floorLabel: cinemaPoi.floorLabel }) const sheetText = wrapper.get('[data-testid="facility-detail-sheet"]').text() expect(sheetText).toContain('类别影院') expect(sheetText).not.toContain('剧场空间') }) it('搜索多结果关闭详情后返回原结果列表', async () => { const wrapper = mountDetail() await loadDetail({ source: 'search', resultCount: '4', id: facilityPoi.id, target: facilityPoi.name }) await wrapper.get('[data-testid="facility-detail-close"]').trigger('tap') expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 })) expect(uni.reLaunch).not.toHaveBeenCalled() }) it('搜索单结果关闭详情同样仅使用页面栈返回', async () => { const wrapper = mountDetail() await loadDetail({ source: 'search', resultCount: '1', id: facilityPoi.id, target: facilityPoi.name }) await wrapper.get('[data-testid="facility-detail-close"]').trigger('tap') expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 })) expect(uni.reLaunch).not.toHaveBeenCalled() }) it('直接进入详情时使用常规返回', async () => { const wrapper = mountDetail() await loadDetail({ id: facilityPoi.id, target: facilityPoi.name }) await wrapper.getComponent(GuidePageFrameStub).vm.$emit('back') expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 })) expect(uni.reLaunch).not.toHaveBeenCalled() }) it('位置预览失败时显示面向用户的错误反馈', async () => { const wrapper = mountDetail() await loadDetail({ id: facilityPoi.id, target: facilityPoi.name }) const mapShell = wrapper.getComponent(GuideMapShellStub) mapShell.vm.$emit('target-focus', { requestId: 1, poiId: facilityPoi.id, floorId: facilityPoi.floorId, status: 'missing' }) await flushPromises() expect(wrapper.get('[data-testid="facility-location-error"]').text()).toContain('缺少可用的位置数据') }) it('位置数据源加载失败时提供明确重试反馈', async () => { mocks.getPoiById.mockRejectedValueOnce(new Error('network unavailable')) mocks.loadPackage.mockRejectedValueOnce(new Error('model package unavailable')) const wrapper = mountDetail() await loadDetail({ source: 'search', resultCount: '2', id: 'poi-network-error', target: '网络异常点位', floorId: 'floor-l2', floorLabel: '2F' }) expect(wrapper.get('[data-testid="facility-location-error"]').text()) .toContain('位置数据加载失败,请稍后重试') expect(wrapper.get('[data-testid="facility-detail-meta"]').text()).toContain('楼层2F') }) it('点位缺少楼层时显示可见兜底且不发送无效定位请求', async () => { mocks.getPoiById.mockResolvedValueOnce({ ...facilityPoi, id: 'poi-without-floor', floorId: '', floorLabel: '' }) const wrapper = mountDetail() await loadDetail({ id: 'poi-without-floor', target: '楼层缺失点位' }) expect(wrapper.get('[data-testid="facility-detail-meta"]').text()) .toContain('楼层楼层信息缺失') expect(wrapper.get('[data-testid="facility-location-error"]').text()) .toContain('缺少楼层信息') expect(wrapper.getComponent(GuideMapShellStub).props('targetFocusRequest')).toBeNull() }) it('完整解析并保留搜索关键词、分类、楼层和列表位置上下文', () => { expect(parseFacilityDetailSearchContext({ source: 'search', searchOrigin: 'home', searchKeyword: encodeURIComponent('卫生间'), searchCategoryId: 'restroom', searchFloorId: 'floor-l2', searchFloorLabel: '2F', resultCount: '4', floorResultCount: '2', visiblePoiIds: 'poi-a,poi-b,poi-a', listScrollTop: '168' })).toEqual({ source: 'search', searchOrigin: 'home', searchKeyword: '卫生间', searchCategoryId: 'restroom', searchFloorId: 'floor-l2', searchFloorLabel: '2F', resultCount: 4, floorResultCount: 2, visiblePoiIds: ['poi-a', 'poi-b'], listScrollTop: 168 }) }) })