319
tests/unit/FacilityDetail.spec.ts
Normal file
319
tests/unit/FacilityDetail.spec.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
// @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()
|
||||
|
||||
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"]')
|
||||
const mapShell = wrapper.getComponent(GuideMapShellStub)
|
||||
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)
|
||||
expect(mapShell.props('activeFloor')).toBe('floor-l2')
|
||||
expect(mapShell.props('targetFocusRequest')).toMatchObject({
|
||||
poiId: 'poi-restroom-l2',
|
||||
floorId: 'floor-l2',
|
||||
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.reLaunch).toHaveBeenCalledWith({ url: '/pages/index/index' })
|
||||
expect(uni.navigateBack).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('缺少可用的位置数据')
|
||||
|
||||
mapShell.vm.$emit('target-focus', {
|
||||
requestId: 1,
|
||||
poiId: facilityPoi.id,
|
||||
floorId: facilityPoi.floorId,
|
||||
status: 'error'
|
||||
})
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
74
tests/unit/GuideMapShell.spec.ts
Normal file
74
tests/unit/GuideMapShell.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
|
||||
const ThreeMapStub = defineComponent({
|
||||
name: 'ThreeMap',
|
||||
props: {
|
||||
visiblePoiIds: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
targetFocus: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['floor-change', 'target-focus'],
|
||||
template: '<div data-testid="three-map"></div>'
|
||||
})
|
||||
|
||||
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.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]])
|
||||
})
|
||||
})
|
||||
166
tests/unit/GuideRepository.spec.ts
Normal file
166
tests/unit/GuideRepository.spec.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
SgsSdkApiProvider,
|
||||
SgsSdkManifestPayload
|
||||
} from '@/data/providers/sgsSdkApiProvider'
|
||||
import { SgsSdkGuideRepository } from '@/repositories/GuideRepository'
|
||||
import { toMuseumPoiFromSgs } from '@/data/adapters/sgsSdkGuideAdapter'
|
||||
|
||||
const manifest: SgsSdkManifestPayload = {
|
||||
mapId: 'museum',
|
||||
mapName: '深圳自然博物馆',
|
||||
floors: [
|
||||
{
|
||||
floorId: '101',
|
||||
floorCode: 'L1',
|
||||
floorName: '1F',
|
||||
sortOrder: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const floorPoi = {
|
||||
id: 'poi-toilet',
|
||||
name: '一层卫生间',
|
||||
type: 'toilet',
|
||||
typeName: '卫生间',
|
||||
floorId: '101',
|
||||
position: {
|
||||
x: 4,
|
||||
y: 12,
|
||||
z: 8
|
||||
}
|
||||
}
|
||||
|
||||
const theaterSpace = {
|
||||
id: 'space-theater',
|
||||
name: '球幕影院',
|
||||
type: 'theater',
|
||||
floorId: '101',
|
||||
boundaryWkt: 'POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))'
|
||||
}
|
||||
|
||||
const createProvider = (overrides: Partial<SgsSdkApiProvider> = {}) => ({
|
||||
getManifest: vi.fn().mockResolvedValue(manifest),
|
||||
getFloorPois: vi.fn().mockResolvedValue([floorPoi]),
|
||||
getFloorBusinessPois: vi.fn().mockResolvedValue([]),
|
||||
getFloorSpaces: vi.fn().mockResolvedValue([theaterSpace]),
|
||||
getNavigablePlaces: vi.fn().mockResolvedValue([]),
|
||||
...overrides
|
||||
}) as unknown as SgsSdkApiProvider
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('SgsSdkGuideRepository 点位搜索', () => {
|
||||
it('源数据仅有 floorCode 时回填 manifest 的稳定 floorId', () => {
|
||||
const poi = toMuseumPoiFromSgs({
|
||||
...floorPoi,
|
||||
id: 'poi-floor-code-only',
|
||||
floorId: undefined,
|
||||
floorCode: 'L1'
|
||||
}, manifest.floors)
|
||||
|
||||
expect(poi.floorId).toBe('101')
|
||||
expect(poi.floorLabel).toBe('1F')
|
||||
})
|
||||
|
||||
it('SDK null 坐标不会被误转换为模型原点', () => {
|
||||
const poi = toMuseumPoiFromSgs({
|
||||
...floorPoi,
|
||||
id: 'poi-with-null-position',
|
||||
position: {
|
||||
x: null,
|
||||
y: null,
|
||||
z: null
|
||||
}
|
||||
}, manifest.floors)
|
||||
|
||||
expect(poi.positionGltf).toBeUndefined()
|
||||
})
|
||||
|
||||
it('合并普通点位与空间点位,并保留影院真实分类和楼层高度', async () => {
|
||||
const repository = new SgsSdkGuideRepository(createProvider())
|
||||
|
||||
const results = await repository.searchPois()
|
||||
const theaterResults = results.filter((poi) => poi.name === '球幕影院')
|
||||
|
||||
expect(theaterResults).toHaveLength(1)
|
||||
expect(theaterResults[0]).toMatchObject({
|
||||
id: 'space-space-theater',
|
||||
floorId: '101',
|
||||
floorLabel: '1F',
|
||||
primaryCategory: {
|
||||
id: 'space_theater'
|
||||
},
|
||||
positionGltf: [5, 12, 5]
|
||||
})
|
||||
expect(results.some((poi) => poi.id === 'poi-toilet')).toBe(true)
|
||||
})
|
||||
|
||||
it('部分来源失败时继续返回可用点位,并在后续搜索重试失败来源', async () => {
|
||||
const getFloorSpaces = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('spaces offline'))
|
||||
.mockRejectedValueOnce(new Error('spaces offline'))
|
||||
.mockResolvedValue([theaterSpace])
|
||||
const provider = createProvider({ getFloorSpaces })
|
||||
const repository = new SgsSdkGuideRepository(provider)
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
const firstResults = await repository.searchPois()
|
||||
expect(firstResults.map((poi) => poi.id)).toEqual(['poi-toilet'])
|
||||
|
||||
const secondResults = await repository.searchPois()
|
||||
expect(secondResults.some((poi) => poi.id === 'space-space-theater')).toBe(true)
|
||||
expect(getFloorSpaces).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('补充来源失败不会阻止核心点位结果缓存', async () => {
|
||||
const getFloorPois = vi.fn().mockResolvedValue([floorPoi])
|
||||
const getFloorSpaces = vi.fn().mockResolvedValue([theaterSpace])
|
||||
const getFloorBusinessPois = vi.fn().mockRejectedValue(new Error('business offline'))
|
||||
const provider = createProvider({
|
||||
getFloorPois,
|
||||
getFloorSpaces,
|
||||
getFloorBusinessPois
|
||||
})
|
||||
const repository = new SgsSdkGuideRepository(provider)
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
await repository.listPois()
|
||||
await repository.listPois()
|
||||
|
||||
expect(getFloorPois).toHaveBeenCalledTimes(1)
|
||||
expect(getFloorSpaces).toHaveBeenCalledTimes(1)
|
||||
expect(getFloorBusinessPois).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('楼层高度来源短暂失败时不缓存缺失的空间点位', async () => {
|
||||
const getFloorPois = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('pois offline'))
|
||||
.mockRejectedValueOnce(new Error('pois offline'))
|
||||
.mockResolvedValue([floorPoi])
|
||||
const provider = createProvider({ getFloorPois })
|
||||
const repository = new SgsSdkGuideRepository(provider)
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
await expect(repository.searchPois()).rejects.toThrow('点位数据加载失败,请检查网络后重试')
|
||||
|
||||
const recoveredResults = await repository.searchPois()
|
||||
expect(recoveredResults.find((poi) => poi.id === 'space-space-theater')?.positionGltf)
|
||||
.toEqual([5, 12, 5])
|
||||
expect(getFloorPois).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('所有核心楼层与空间请求失败时返回明确的可重试错误', async () => {
|
||||
const provider = createProvider({
|
||||
getFloorPois: vi.fn().mockRejectedValue(new Error('pois offline')),
|
||||
getFloorSpaces: vi.fn().mockRejectedValue(new Error('spaces offline'))
|
||||
})
|
||||
const repository = new SgsSdkGuideRepository(provider)
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
await expect(repository.searchPois()).rejects.toThrow('点位数据加载失败,请检查网络后重试')
|
||||
})
|
||||
})
|
||||
283
tests/unit/IndexPoiSearchFlow.spec.ts
Normal file
283
tests/unit/IndexPoiSearchFlow.spec.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defineComponent, 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(() => ({
|
||||
searchMountCount: 0,
|
||||
searchUnmountCount: 0,
|
||||
searchRestoreCount: 0,
|
||||
onShowHandler: null as null | (() => Promise<void> | void)
|
||||
}))
|
||||
|
||||
vi.mock('@dcloudio/uni-app', () => ({
|
||||
onLoad: vi.fn(),
|
||||
onShow: (handler: () => Promise<void> | void) => {
|
||||
testState.onShowHandler = handler
|
||||
}
|
||||
}))
|
||||
|
||||
const floors = [
|
||||
{ id: 'L1', label: '1F', order: 1 },
|
||||
{ id: 'L2', label: '2F', order: 2 }
|
||||
]
|
||||
|
||||
const modelSource = {
|
||||
loadPackage: async () => ({ overviewModelUrl: '', floors: [] }),
|
||||
loadFloorPois: async () => []
|
||||
}
|
||||
|
||||
vi.mock('@/usecases/guideUseCase', () => ({
|
||||
guideUseCase: {
|
||||
getAssetBaseUrl: () => '/static/nav-assets',
|
||||
getModelSource: () => modelSource,
|
||||
normalizeFloorId: (value: string) => (
|
||||
value === '1F' ? 'L1' : value === '2F' ? 'L2' : value
|
||||
),
|
||||
getFloors: async () => floors
|
||||
}
|
||||
}))
|
||||
|
||||
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: '<main><slot /></main>'
|
||||
})
|
||||
|
||||
const GuideMapShellStub = defineComponent({
|
||||
name: 'GuideMapShell',
|
||||
props: {
|
||||
activeFloor: { type: String, default: '' },
|
||||
indoorView: { type: String, default: '' },
|
||||
visiblePoiIds: { type: Array, default: null },
|
||||
targetFocusRequest: { type: Object, default: null }
|
||||
},
|
||||
emits: ['floor-change', 'poi-click', 'selection-clear'],
|
||||
template: '<section data-testid="guide-map"><slot name="overlay" /><slot /></section>'
|
||||
})
|
||||
|
||||
const PoiSearchPanelStub = defineComponent({
|
||||
name: 'PoiSearchPanel',
|
||||
props: {
|
||||
currentFloorId: { type: String, default: '' },
|
||||
currentFloorLabel: { type: String, default: '' }
|
||||
},
|
||||
emits: [
|
||||
'expanded-change',
|
||||
'category-mode-change',
|
||||
'results-change',
|
||||
'result-tap',
|
||||
'cancel'
|
||||
],
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
testState.searchMountCount += 1
|
||||
})
|
||||
onUnmounted(() => {
|
||||
testState.searchUnmountCount += 1
|
||||
})
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
collapseHomePanel() {},
|
||||
resetSearchState() {},
|
||||
restoreResultListScroll() {
|
||||
testState.searchRestoreCount += 1
|
||||
}
|
||||
},
|
||||
template: '<div data-testid="poi-search"></div>'
|
||||
})
|
||||
|
||||
const stubs = {
|
||||
GuidePageFrame: GuidePageFrameStub,
|
||||
GuideMapShell: GuideMapShellStub,
|
||||
PoiSearchPanel: PoiSearchPanelStub,
|
||||
RoutePlannerPanel: true,
|
||||
ArrivalPanel: true,
|
||||
ExplainHallSelect: true
|
||||
}
|
||||
|
||||
const poi = {
|
||||
id: 'poi-l2-restroom',
|
||||
name: '二层卫生间',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
primaryCategory: { id: 'restroom', label: '卫生间' },
|
||||
categories: [{ id: 'restroom', label: '卫生间' }],
|
||||
positionGltf: [12, 3, 24] as [number, number, number],
|
||||
accessible: true
|
||||
}
|
||||
|
||||
const categoryContext = {
|
||||
origin: 'home' as const,
|
||||
keyword: '卫生间',
|
||||
categoryId: 'restroom' as const,
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
resultCount: 5,
|
||||
floorResultCount: 2,
|
||||
visiblePoiIds: ['poi-l2-restroom', 'poi-l2-restroom-2'],
|
||||
listScrollTop: 126
|
||||
}
|
||||
|
||||
const mountIndex = async () => {
|
||||
const wrapper = mount(IndexPage, {
|
||||
global: { stubs }
|
||||
})
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
testState.searchMountCount = 0
|
||||
testState.searchUnmountCount = 0
|
||||
testState.searchRestoreCount = 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.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('首页搜索与地图闭环', () => {
|
||||
it('向搜索面板传入当前楼层,并将分类结果 ID 精确传给地图', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
|
||||
expect(search.props('currentFloorId')).toBe('L1')
|
||||
expect(search.props('currentFloorLabel')).toBe('1F')
|
||||
|
||||
search.vm.$emit('category-mode-change', true)
|
||||
search.vm.$emit('results-change', {
|
||||
...categoryContext,
|
||||
active: true
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(map.props('activeFloor')).toBe('L2')
|
||||
expect(map.props('indoorView')).toBe('floor')
|
||||
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
||||
})
|
||||
|
||||
it('跨楼层结果使用既有 target-focus,并携带返回上下文打开详情', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
|
||||
search.vm.$emit('category-mode-change', true)
|
||||
search.vm.$emit('results-change', {
|
||||
...categoryContext,
|
||||
active: true
|
||||
})
|
||||
search.vm.$emit('result-tap', poi, categoryContext)
|
||||
await flushPromises()
|
||||
|
||||
expect(map.props('activeFloor')).toBe('L2')
|
||||
expect(map.props('targetFocusRequest')).toMatchObject({
|
||||
poiId: poi.id,
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
positionGltf: poi.positionGltf
|
||||
})
|
||||
|
||||
const navigateTo = vi.mocked(uni.navigateTo)
|
||||
const detailUrl = navigateTo.mock.calls[0]?.[0]?.url as string
|
||||
const query = new URLSearchParams(detailUrl.split('?')[1])
|
||||
expect(query.get('source')).toBe('search')
|
||||
expect(query.get('searchOrigin')).toBe('home')
|
||||
expect(query.get('searchKeyword')).toBe('卫生间')
|
||||
expect(query.get('searchCategoryId')).toBe('restroom')
|
||||
expect(query.get('searchFloorId')).toBe('L2')
|
||||
expect(query.get('resultCount')).toBe('2')
|
||||
expect(query.get('floorResultCount')).toBe('2')
|
||||
expect(query.get('visiblePoiIds')).toBe(categoryContext.visiblePoiIds.join(','))
|
||||
expect(query.get('listScrollTop')).toBe('126')
|
||||
expect(testState.searchMountCount).toBe(1)
|
||||
expect(testState.searchUnmountCount).toBe(0)
|
||||
})
|
||||
|
||||
it('页面重新显示时要求搜索面板恢复原列表位置', async () => {
|
||||
await mountIndex()
|
||||
await testState.onShowHandler?.()
|
||||
await flushPromises()
|
||||
|
||||
expect(testState.searchRestoreCount).toBe(1)
|
||||
})
|
||||
|
||||
it('地图点位卡隐藏首页 dock 时仍保留搜索组件,关闭后可继续使用原列表', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
|
||||
map.vm.$emit('poi-click', {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
primaryCategory: 'restroom',
|
||||
primaryCategoryZh: '卫生间',
|
||||
iconType: 'restroom',
|
||||
positionGltf: poi.positionGltf
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.get('[data-testid="poi-search"]').exists()).toBe(true)
|
||||
expect(testState.searchUnmountCount).toBe(0)
|
||||
|
||||
map.vm.$emit('selection-clear')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.getComponent(PoiSearchPanelStub).exists()).toBe(true)
|
||||
expect(testState.searchMountCount).toBe(1)
|
||||
})
|
||||
|
||||
it('单结果分类写入 resultCount=1,主动取消恢复默认标记并清除目标', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
const singleContext = {
|
||||
...categoryContext,
|
||||
floorResultCount: 1,
|
||||
visiblePoiIds: [poi.id]
|
||||
}
|
||||
|
||||
search.vm.$emit('category-mode-change', true)
|
||||
search.vm.$emit('results-change', { ...singleContext, active: true })
|
||||
search.vm.$emit('result-tap', poi, singleContext)
|
||||
await flushPromises()
|
||||
|
||||
const detailUrl = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url as string
|
||||
expect(new URLSearchParams(detailUrl.split('?')[1]).get('resultCount')).toBe('1')
|
||||
expect(map.props('targetFocusRequest')).not.toBeNull()
|
||||
|
||||
search.vm.$emit('cancel')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(map.props('visiblePoiIds')).toBeNull()
|
||||
expect(map.props('targetFocusRequest')).toBeNull()
|
||||
})
|
||||
})
|
||||
357
tests/unit/PoiSearchPanel.spec.ts
Normal file
357
tests/unit/PoiSearchPanel.spec.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
|
||||
import type { MuseumFloor, MuseumPoi } from '@/domain/museum'
|
||||
|
||||
const guideMocks = vi.hoisted(() => ({
|
||||
getFloors: vi.fn(),
|
||||
getInitialSearchSpacePoints: vi.fn(),
|
||||
searchPois: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/usecases/guideUseCase', () => ({
|
||||
guideUseCase: guideMocks
|
||||
}))
|
||||
|
||||
const floors: MuseumFloor[] = [
|
||||
{ id: 'floor-1', label: '1F', order: 1 },
|
||||
{ id: 'floor-2', label: '2F', order: 2 }
|
||||
]
|
||||
|
||||
const createPoi = (
|
||||
id: string,
|
||||
name: string,
|
||||
floorId: string,
|
||||
floorLabel: string,
|
||||
iconType: string,
|
||||
positionGltf?: [number, number, number]
|
||||
): MuseumPoi => ({
|
||||
id,
|
||||
name,
|
||||
floorId,
|
||||
floorLabel,
|
||||
primaryCategory: {
|
||||
id: iconType,
|
||||
label: name.includes('卫生间') ? '卫生间' : name.includes('母婴') ? '母婴室' : '展厅',
|
||||
iconType
|
||||
},
|
||||
categories: [],
|
||||
positionGltf,
|
||||
accessible: true
|
||||
})
|
||||
|
||||
const poiPool: MuseumPoi[] = [
|
||||
createPoi('hall-1', '地球厅', 'floor-1', '1F', 'exhibition_hall', [1, 0, 1]),
|
||||
createPoi('restroom-1', '卫生间 A', 'floor-1', '1F', 'toilet', [2, 0, 2]),
|
||||
createPoi('restroom-missing-position', '卫生间 B', 'floor-1', '1F', 'toilet'),
|
||||
createPoi('restroom-2', '卫生间 C', 'floor-2', '2F', 'toilet', [3, 0, 3]),
|
||||
createPoi('nursing-2', '母婴室', 'floor-2', '2F', 'mother_baby_room', [4, 0, 4])
|
||||
]
|
||||
|
||||
const flushMountedSearch = async () => {
|
||||
await flushPromises()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
const createDeferred = <T>() => {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
guideMocks.getFloors.mockResolvedValue(floors)
|
||||
guideMocks.getInitialSearchSpacePoints.mockResolvedValue(poiPool)
|
||||
guideMocks.searchPois.mockImplementation(async (keyword = '') => (
|
||||
keyword
|
||||
? poiPool.filter((poi) => poi.name.includes(keyword))
|
||||
: poiPool
|
||||
))
|
||||
vi.stubGlobal('uni', {
|
||||
hideKeyboard: vi.fn(),
|
||||
navigateTo: vi.fn(),
|
||||
showToast: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.documentElement.className = ''
|
||||
document.body.className = ''
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('POI 搜索面板实际渲染与状态闭环', () => {
|
||||
it('首页显示六类统一入口,搜索页显示完整十一类入口', async () => {
|
||||
const homeWrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
const homeCategories = homeWrapper.findAll('.home-category-chip')
|
||||
expect(homeCategories).toHaveLength(6)
|
||||
expect(homeCategories.map((item) => item.text())).toEqual([
|
||||
'展厅', '卫生间', '母婴室', '电梯', '楼梯', '扶梯'
|
||||
])
|
||||
homeCategories.forEach((item) => {
|
||||
expect(item.find('.line-icon').exists()).toBe(true)
|
||||
expect(item.find('.home-category-label').exists()).toBe(true)
|
||||
})
|
||||
homeWrapper.unmount()
|
||||
|
||||
const pageWrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
expect(pageWrapper.findAll('.category-item')).toHaveLength(11)
|
||||
expect(pageWrapper.text()).toContain('影院')
|
||||
expect(pageWrapper.text()).toContain('服务中心')
|
||||
expect(pageWrapper.text()).toContain('文创')
|
||||
expect(pageWrapper.text()).not.toContain('服务设施')
|
||||
pageWrapper.unmount()
|
||||
})
|
||||
|
||||
it('首页分类只展示当前楼层结果,并上报可定位点位的精确 marker ID', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.get('[data-testid="poi-home-category-results"]').text()).toContain('当前楼层 1F · 2 个点位')
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
const missingPositionRow = wrapper.get('[data-testid="poi-result-restroom-missing-position"]')
|
||||
expect(missingPositionRow.classes()).toContain('disabled')
|
||||
expect(missingPositionRow.text()).toContain('暂无地图坐标')
|
||||
|
||||
const resultStates = wrapper.emitted('results-change') || []
|
||||
expect(resultStates.at(-1)?.[0]).toMatchObject({
|
||||
active: true,
|
||||
categoryId: 'restroom',
|
||||
floorId: 'floor-1',
|
||||
floorLabel: '1F',
|
||||
resultCount: 2,
|
||||
floorResultCount: 2,
|
||||
visiblePoiIds: ['restroom-1']
|
||||
})
|
||||
|
||||
await missingPositionRow.trigger('tap')
|
||||
expect(wrapper.emitted('resultTap')).toBeUndefined()
|
||||
expect(uni.showToast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: '该点位缺少地图坐标,暂无法定位'
|
||||
}))
|
||||
|
||||
await wrapper.get('[data-testid="poi-result-restroom-1"]').trigger('tap')
|
||||
expect(wrapper.emitted('resultTap')?.at(-1)?.[1]).toMatchObject({
|
||||
categoryId: 'restroom',
|
||||
resultCount: 2,
|
||||
visiblePoiIds: ['restroom-1']
|
||||
})
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('首页分类跟随当前地图楼层更新结果与 marker ID,并对无结果给出反馈', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.setProps({ currentFloorId: 'floor-2', currentFloorLabel: '2F' })
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.get('[data-testid="poi-home-category-results"]').text()).toContain('当前楼层 2F · 1 个点位')
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-2"]').exists()).toBe(true)
|
||||
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||
active: true,
|
||||
floorId: 'floor-2',
|
||||
visiblePoiIds: ['restroom-2']
|
||||
})
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('[data-testid="poi-category-nursing-room"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-result-nursing-2"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.setProps({ currentFloorId: 'floor-1', currentFloorLabel: '1F' })
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.get('[data-testid="poi-empty"]').text()).toContain('当前楼层暂无匹配点位')
|
||||
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||
active: true,
|
||||
visiblePoiIds: []
|
||||
})
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('独立搜索页进入详情时传递关键词、分类、楼层、结果数和列表位置', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
const floor2Tab = wrapper.findAll('.floor-tab').find((item) => item.text() === '2F')
|
||||
expect(floor2Tab).toBeDefined()
|
||||
await floor2Tab!.trigger('tap')
|
||||
await flushMountedSearch()
|
||||
wrapper.get('[data-testid="poi-result-list"]').element.scrollTop = 128
|
||||
await wrapper.get('[data-testid="poi-result-restroom-2"]').trigger('tap')
|
||||
|
||||
expect(uni.navigateTo).toHaveBeenCalledTimes(1)
|
||||
const url = vi.mocked(uni.navigateTo).mock.calls[0][0].url
|
||||
const query = new URLSearchParams(url.split('?')[1])
|
||||
expect(query.get('source')).toBe('search')
|
||||
expect(query.get('searchOrigin')).toBe('page')
|
||||
expect(query.get('searchKeyword')).toBe('卫生间')
|
||||
expect(query.get('searchCategoryId')).toBe('restroom')
|
||||
expect(query.get('searchFloorId')).toBe('floor-2')
|
||||
expect(query.get('resultCount')).toBe('3')
|
||||
expect(query.get('floorResultCount')).toBe('1')
|
||||
expect(query.get('visiblePoiIds')).toBe('restroom-2')
|
||||
expect(query.get('listScrollTop')).toBe('128')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('页面重新显示时恢复原结果列表位置', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
const resultList = wrapper.get('[data-testid="poi-result-list"]')
|
||||
resultList.element.scrollTop = 128
|
||||
await wrapper.get('[data-testid="poi-result-restroom-1"]').trigger('tap')
|
||||
resultList.element.scrollTop = 0
|
||||
|
||||
await (wrapper.vm as unknown as {
|
||||
restoreResultListScroll: () => Promise<void>
|
||||
}).restoreResultListScroll()
|
||||
|
||||
expect(resultList.element.scrollTop).toBe(128)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('部分数据不会固化,分类点击会重新读取并恢复结果', async () => {
|
||||
guideMocks.getInitialSearchSpacePoints
|
||||
.mockResolvedValueOnce([poiPool[0]])
|
||||
.mockResolvedValueOnce(poiPool)
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(guideMocks.getInitialSearchSpacePoints).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('旧的部分结果晚到时不会覆盖最新完整缓存', async () => {
|
||||
const oldRequest = createDeferred<MuseumPoi[]>()
|
||||
const latestRequest = createDeferred<MuseumPoi[]>()
|
||||
guideMocks.getInitialSearchSpacePoints
|
||||
.mockImplementationOnce(() => oldRequest.promise)
|
||||
.mockImplementationOnce(() => latestRequest.promise)
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
expect(guideMocks.getInitialSearchSpacePoints).toHaveBeenCalledTimes(1)
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushPromises()
|
||||
expect(guideMocks.getInitialSearchSpacePoints).toHaveBeenCalledTimes(2)
|
||||
|
||||
latestRequest.resolve(poiPool)
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
|
||||
oldRequest.resolve([poiPool[0]])
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('[data-testid="poi-category-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('服务中心只展示真实服务台,不把普通服务空间误归类', async () => {
|
||||
const serviceSpace = createPoi(
|
||||
'space-service-lounge',
|
||||
'贵宾休息室',
|
||||
'floor-1',
|
||||
'1F',
|
||||
'service_space',
|
||||
[5, 0, 5]
|
||||
)
|
||||
const serviceDesk = createPoi(
|
||||
'poi-service-desk',
|
||||
'咨询服务台',
|
||||
'floor-1',
|
||||
'1F',
|
||||
'service_desk',
|
||||
[6, 0, 6]
|
||||
)
|
||||
guideMocks.getInitialSearchSpacePoints.mockResolvedValue([serviceSpace, serviceDesk])
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('[data-testid="poi-category-service-center"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.find('[data-testid="poi-result-poi-service-desk"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="poi-result-space-service-lounge"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('弱网失败显示可重试状态,恢复后重新渲染结果', async () => {
|
||||
guideMocks.getInitialSearchSpacePoints
|
||||
.mockRejectedValueOnce(new Error('network down'))
|
||||
.mockResolvedValueOnce(poiPool)
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.get('[data-testid="poi-empty"]').text()).toContain('点位加载失败')
|
||||
expect(wrapper.find('[data-testid="poi-retry"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="poi-retry"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-retry"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="poi-result-hall-1"]').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
67
tests/unit/SearchPage.spec.ts
Normal file
67
tests/unit/SearchPage.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
restoreCount: 0,
|
||||
onShowHandler: null as null | (() => Promise<void> | void)
|
||||
}))
|
||||
|
||||
vi.mock('@dcloudio/uni-app', () => ({
|
||||
onLoad: vi.fn(),
|
||||
onShow: (handler: () => Promise<void> | void) => {
|
||||
testState.onShowHandler = handler
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/hostEnvironment', () => ({
|
||||
isEmbeddedInWechatMiniProgram: () => false
|
||||
}))
|
||||
|
||||
import SearchPage from '@/pages/search/index.vue'
|
||||
|
||||
const PoiSearchPanelStub = defineComponent({
|
||||
name: 'PoiSearchPanel',
|
||||
methods: {
|
||||
restoreResultListScroll() {
|
||||
testState.restoreCount += 1
|
||||
}
|
||||
},
|
||||
template: '<div data-testid="poi-search-panel"></div>'
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
testState.restoreCount = 0
|
||||
testState.onShowHandler = null
|
||||
vi.stubGlobal('uni', {
|
||||
reLaunch: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('独立搜索页取消闭环', () => {
|
||||
it('主动返回地图时直接回到馆内首页', async () => {
|
||||
const wrapper = mount(SearchPage, {
|
||||
global: {
|
||||
stubs: {
|
||||
PoiSearchPanel: PoiSearchPanelStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await testState.onShowHandler?.()
|
||||
await flushPromises()
|
||||
expect(testState.restoreCount).toBe(1)
|
||||
|
||||
await wrapper.get('.map-link').trigger('tap')
|
||||
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith({
|
||||
url: '/pages/index/index?tab=guide'
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user