Files
frontend-miniapp/tests/unit/FacilityDetail.spec.ts
cxk 735c5cd1ee
Some checks failed
CI / verify (push) Has been cancelled
统一导览点位摘要卡视觉体验
2026-07-20 22:01:25 +08:00

328 lines
9.9 KiB
TypeScript

// @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<string, unknown>) => Promise<void>),
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<string, unknown>) => Promise<void>) => {
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: '<div data-testid="guide-page-frame"><slot /></div>'
})
const GuideMapShellStub = defineComponent({
name: 'GuideMapShellStub',
props: {
targetFocusRequest: {
type: Object,
default: null
},
activeFloor: {
type: String,
default: ''
}
},
emits: ['floor-change', 'target-focus'],
template: '<div data-testid="guide-map-shell"><slot /></div>'
})
const mountDetail = () => mount(FacilityDetail, {
global: {
stubs: {
GuidePageFrame: GuidePageFrameStub,
GuideMapShell: GuideMapShellStub
}
}
})
const loadDetail = async (options: Record<string, unknown>) => {
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
})
})
})