327 lines
9.9 KiB
TypeScript
327 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.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
|
|
})
|
|
})
|
|
})
|