753 lines
25 KiB
TypeScript
753 lines
25 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||
import type {
|
||
SgsSdkApiProvider,
|
||
SgsSdkManifestPayload
|
||
} from '@/data/providers/sgsSdkApiProvider'
|
||
import { SgsSdkGuideRepository } from '@/repositories/GuideRepository'
|
||
import { SgsSdkGuideModelRepository } from '@/repositories/GuideModelRepository'
|
||
import {
|
||
isExhibitionHallSpace,
|
||
toMuseumPoiFromSgs,
|
||
toMuseumSpacePointFromSgs
|
||
} from '@/data/adapters/sgsSdkGuideAdapter'
|
||
import {
|
||
getPoiCategoryById,
|
||
matchesPoiCategory,
|
||
resolvePoiCategory
|
||
} from '@/domain/poiCategories'
|
||
import { toFacilityDetailViewModel } from '@/view-models/guideViewModels'
|
||
|
||
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('普通 POI 坐标与大屏端一致优先使用 meshCenter', () => {
|
||
const poi = toMuseumPoiFromSgs({
|
||
...floorPoi,
|
||
meshCenter: { x: 11, y: 12, z: 13 },
|
||
position: { x: 21, y: 22, z: 23 },
|
||
labelPosition: { x: 31, y: 32, z: 33 },
|
||
x: 41,
|
||
y: 42,
|
||
z: 43
|
||
}, manifest.floors)
|
||
|
||
expect(poi.positionGltf).toEqual([11, 12, 13])
|
||
})
|
||
|
||
it('普通 POI 坐标按 position、labelPosition、旧坐标依次回退', () => {
|
||
const basePoi = {
|
||
...floorPoi,
|
||
position: { x: null, y: null, z: null },
|
||
labelPosition: { x: 31, y: 32, z: 33 },
|
||
x: 41,
|
||
y: 42,
|
||
z: 43
|
||
}
|
||
|
||
expect(toMuseumPoiFromSgs(basePoi, manifest.floors).positionGltf).toEqual([31, 32, 33])
|
||
expect(toMuseumPoiFromSgs({
|
||
...basePoi,
|
||
labelPosition: { x: null, y: null, z: null }
|
||
}, manifest.floors).positionGltf).toEqual([41, 42, 43])
|
||
})
|
||
|
||
it('设备终端解析对象 extParams 的全部精确模型节点', () => {
|
||
const poi = toMuseumPoiFromSgs({
|
||
...floorPoi,
|
||
id: 'guide-screen-pair',
|
||
name: '一楼导览屏',
|
||
type: 'device_terminal',
|
||
extParams: {
|
||
sourceNodeNames: ['L1_导览屏_01', 'L1_导览屏_02']
|
||
}
|
||
}, manifest.floors)
|
||
|
||
expect(poi.primaryCategory.iconType).toBe('device_terminal')
|
||
expect(poi.sourceObjectName).toBe('L1_导览屏_01')
|
||
expect(poi.mergedSourceObjectNames).toEqual(['L1_导览屏_02'])
|
||
})
|
||
|
||
it('设备终端解析字符串 extParams,并保持显式节点字段优先级', () => {
|
||
const poi = toMuseumPoiFromSgs({
|
||
...floorPoi,
|
||
id: 'elevator-with-node-chain',
|
||
name: 'L1_H电梯',
|
||
type: 'device_terminal',
|
||
anchorNodeName: 'L1_H_电梯16',
|
||
sourceNodeName: 'L1_H_电梯15',
|
||
extParams: JSON.stringify({
|
||
sourceNodeNames: ['L1_H_电梯14', 'L1_H_电梯16']
|
||
})
|
||
}, manifest.floors)
|
||
|
||
expect(poi.sourceObjectName).toBe('L1_H_电梯16')
|
||
expect(poi.mergedSourceObjectNames).toEqual(['L1_H_电梯15', 'L1_H_电梯14'])
|
||
})
|
||
|
||
it('设备终端解析 extParams 中的单个 sourceNodeName', () => {
|
||
const poi = toMuseumPoiFromSgs({
|
||
...floorPoi,
|
||
id: 'vending-machine',
|
||
name: '自助售卖机',
|
||
type: 'device_terminal',
|
||
extParams: {
|
||
sourceNodeName: 'L1_自助售卖机'
|
||
}
|
||
}, manifest.floors)
|
||
|
||
expect(poi.sourceObjectName).toBe('L1_自助售卖机')
|
||
expect(poi.mergedSourceObjectNames).toEqual([])
|
||
})
|
||
|
||
it('缺少节点元数据的设备终端仅用精确后台名称作为模型节点', () => {
|
||
const poi = toMuseumPoiFromSgs({
|
||
...floorPoi,
|
||
id: 'security-device',
|
||
name: 'L1_安检机_01',
|
||
type: 'device_terminal'
|
||
}, manifest.floors)
|
||
|
||
expect(poi.sourceObjectName).toBe('L1_安检机_01')
|
||
expect(poi.mergedSourceObjectNames).toEqual([])
|
||
})
|
||
|
||
it('空间是否为展厅仅由后端分类决定', () => {
|
||
expect(isExhibitionHallSpace({
|
||
id: 'hall',
|
||
name: '人类厅',
|
||
type: 'exhibition_hall',
|
||
floorId: '101'
|
||
})).toBe(true)
|
||
expect(isExhibitionHallSpace({
|
||
...theaterSpace,
|
||
name: '动感多维影院'
|
||
})).toBe(false)
|
||
expect(isExhibitionHallSpace({
|
||
id: 'education-space',
|
||
name: '青少年探索中心',
|
||
type: 'education_activity',
|
||
floorId: '101'
|
||
})).toBe(false)
|
||
})
|
||
|
||
it('空间节点名为占位符时按楼层和空间名称匹配模型', () => {
|
||
const poi = toMuseumSpacePointFromSgs({
|
||
id: 'tea-room',
|
||
name: '茶水间',
|
||
type: 'service_space',
|
||
floorId: '101',
|
||
sourceNodeName: 'L1_???',
|
||
position: { x: 1, y: 12, z: 2 }
|
||
}, manifest.floors, '101')
|
||
|
||
expect(poi?.sourceObjectName).toBe('L1_茶水间')
|
||
})
|
||
|
||
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(resolvePoiCategory(theaterResults[0])?.id).toBe('cinema')
|
||
expect(matchesPoiCategory(
|
||
theaterResults[0],
|
||
getPoiCategoryById('cinema')!
|
||
)).toBe(true)
|
||
expect(matchesPoiCategory(
|
||
theaterResults[0],
|
||
getPoiCategoryById('exhibition-hall')!
|
||
)).toBe(false)
|
||
expect(toFacilityDetailViewModel(theaterResults[0]).category).toBe('影院')
|
||
expect(results.some((poi) => poi.id === 'poi-toilet')).toBe(true)
|
||
})
|
||
|
||
it('搜索可定位 ID 与同楼层地图 marker ID 保持一致', async () => {
|
||
const provider = createProvider()
|
||
const guideRepository = new SgsSdkGuideRepository(provider)
|
||
const modelRepository = new SgsSdkGuideModelRepository(provider, guideRepository)
|
||
|
||
const [searchResults, renderPois] = await Promise.all([
|
||
guideRepository.searchPois(),
|
||
modelRepository.loadFloorPois('101')
|
||
])
|
||
const renderPoiIds = new Set(renderPois.map((poi) => poi.id))
|
||
const locatableSearchIds = searchResults
|
||
.filter((poi) => poi.floorId === '101' && poi.positionGltf)
|
||
.map((poi) => poi.id)
|
||
|
||
expect(locatableSearchIds.length).toBeGreaterThan(0)
|
||
expect(locatableSearchIds.every((poiId) => renderPoiIds.has(poiId))).toBe(true)
|
||
expect(renderPois.find((poi) => poi.id === 'space-space-theater')).toMatchObject({
|
||
primaryCategory: 'space_theater',
|
||
iconType: 'theater'
|
||
})
|
||
})
|
||
|
||
it('地图标签只读取当前楼层 SDK 数据,不触发全馆搜索聚合', async () => {
|
||
const provider = createProvider()
|
||
const guideSearchRepository = {
|
||
listPois: vi.fn().mockResolvedValue([]),
|
||
listSpacePoints: vi.fn().mockResolvedValue([])
|
||
}
|
||
const modelRepository = new SgsSdkGuideModelRepository(
|
||
provider,
|
||
guideSearchRepository as never
|
||
)
|
||
|
||
const renderPois = await modelRepository.loadFloorPois('101')
|
||
|
||
expect(renderPois).not.toHaveLength(0)
|
||
expect(provider.getFloorPois).toHaveBeenCalledTimes(1)
|
||
expect(provider.getFloorBusinessPois).toHaveBeenCalledTimes(1)
|
||
expect(provider.getFloorSpaces).toHaveBeenCalledTimes(1)
|
||
expect(provider.getNavigablePlaces).toHaveBeenCalledTimes(1)
|
||
expect(guideSearchRepository.listPois).not.toHaveBeenCalled()
|
||
expect(guideSearchRepository.listSpacePoints).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('地图渲染链路透传设备终端的全部模型节点', async () => {
|
||
const provider = createProvider({
|
||
getFloorPois: vi.fn().mockResolvedValue([{
|
||
...floorPoi,
|
||
id: 'guide-screen-pair',
|
||
name: '一楼导览屏',
|
||
type: 'device_terminal',
|
||
extParams: JSON.stringify({
|
||
sourceNodeNames: ['L1_导览屏_01', 'L1_导览屏_02']
|
||
})
|
||
}])
|
||
})
|
||
const repository = new SgsSdkGuideModelRepository(provider)
|
||
|
||
const renderPoi = (await repository.loadFloorPois('101'))
|
||
.find((poi) => poi.id === 'guide-screen-pair')
|
||
|
||
expect(renderPoi).toMatchObject({
|
||
iconType: 'device_terminal',
|
||
sourceObjectName: 'L1_导览屏_01',
|
||
mergedSourceObjectNames: ['L1_导览屏_02']
|
||
})
|
||
})
|
||
|
||
it('首屏标签优先读取楼层 Bundle,并在首帧后预取搜索补充数据', async () => {
|
||
const getFloorBundle = vi.fn().mockResolvedValue({
|
||
pois: [floorPoi],
|
||
spaces: [theaterSpace]
|
||
})
|
||
const getFloorBusinessPois = vi.fn().mockResolvedValue([])
|
||
const getNavigablePlaces = vi.fn().mockResolvedValue([])
|
||
const provider = createProvider({
|
||
getFloorBundle,
|
||
getFloorBusinessPois,
|
||
getNavigablePlaces
|
||
})
|
||
const repository = new SgsSdkGuideModelRepository(provider)
|
||
|
||
const renderPois = await repository.loadFloorPoisFast('101')
|
||
|
||
expect(renderPois).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({ id: 'poi-toilet' }),
|
||
expect.objectContaining({ id: 'space-space-theater' })
|
||
]))
|
||
expect(getFloorBundle).toHaveBeenCalledWith('101')
|
||
expect(provider.getFloorPois).not.toHaveBeenCalled()
|
||
|
||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||
|
||
expect(getFloorBusinessPois).toHaveBeenCalledWith('101')
|
||
expect(getNavigablePlaces).toHaveBeenCalledWith('101')
|
||
})
|
||
|
||
it('外观首屏不会预取室内搜索补充数据', async () => {
|
||
const getFloorBusinessPois = vi.fn().mockResolvedValue([])
|
||
const getNavigablePlaces = vi.fn().mockResolvedValue([])
|
||
const provider = createProvider({
|
||
getManifest: vi.fn().mockResolvedValue({
|
||
...manifest,
|
||
floors: [{
|
||
floorId: 'outdoor',
|
||
floorCode: 'EXTERIOR',
|
||
floorName: '室外'
|
||
}]
|
||
}),
|
||
getFloorBundle: vi.fn().mockResolvedValue({ pois: [], spaces: [] }),
|
||
getFloorBusinessPois,
|
||
getNavigablePlaces
|
||
})
|
||
const repository = new SgsSdkGuideModelRepository(provider)
|
||
|
||
await expect(repository.loadFloorPoisFast('outdoor')).resolves.toEqual([])
|
||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||
|
||
expect(getFloorBusinessPois).not.toHaveBeenCalled()
|
||
expect(getNavigablePlaces).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('核心点位来源短暂失败时仍返回可直接定位的稳定空间 ID', async () => {
|
||
const getFloorPois = vi.fn()
|
||
.mockResolvedValueOnce([floorPoi])
|
||
.mockRejectedValueOnce(new Error('pois offline'))
|
||
.mockResolvedValue([floorPoi])
|
||
const getFloorSpaces = vi.fn()
|
||
.mockResolvedValueOnce([theaterSpace])
|
||
.mockRejectedValueOnce(new Error('spaces offline'))
|
||
.mockResolvedValue([theaterSpace])
|
||
const provider = createProvider({ getFloorPois, getFloorSpaces })
|
||
const guideRepository = new SgsSdkGuideRepository(provider)
|
||
const modelRepository = new SgsSdkGuideModelRepository(provider, guideRepository)
|
||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||
|
||
await guideRepository.listSpacePoints()
|
||
const fallbackResults = await guideRepository.searchPois()
|
||
const fallbackTheater = fallbackResults.find((poi) => poi.name === '球幕影院')
|
||
|
||
expect(fallbackTheater?.id).toBe('space-space-theater')
|
||
const recoveredTheater = (await guideRepository.searchPois())
|
||
.find((poi) => poi.name === '球幕影院')
|
||
expect(recoveredTheater?.id).toBe(fallbackTheater?.id)
|
||
await expect(guideRepository.getPoiById('space-space-theater'))
|
||
.resolves.toMatchObject({ id: 'space-space-theater' })
|
||
|
||
const renderPois = await modelRepository.loadFloorPois('101')
|
||
expect(renderPois.some((poi) => poi.id === fallbackTheater?.id)).toBe(true)
|
||
})
|
||
|
||
it('精确空间 ID 与普通点位 ID 同号时不会定位到错误点位', async () => {
|
||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||
const provider = createProvider({
|
||
getFloorPois: vi.fn().mockResolvedValue([{
|
||
...floorPoi,
|
||
id: '42',
|
||
name: '普通卫生间'
|
||
}]),
|
||
getFloorSpaces: vi.fn().mockResolvedValue([{
|
||
id: '42',
|
||
name: '公共空间',
|
||
type: 'public_area',
|
||
floorId: '101',
|
||
boundaryWkt: 'POLYGON((0 0, 8 0, 8 8, 0 8, 0 0))'
|
||
}])
|
||
})
|
||
const repository = new SgsSdkGuideRepository(provider)
|
||
|
||
await expect(repository.getPoiById('space-42')).resolves.toMatchObject({
|
||
id: 'space-42',
|
||
name: '公共空间'
|
||
})
|
||
await expect(repository.getPoiById('42')).resolves.toMatchObject({
|
||
id: '42',
|
||
name: '普通卫生间'
|
||
})
|
||
})
|
||
|
||
it('相同模型对象名不会删除不同稳定 ID 和坐标的普通点位', async () => {
|
||
const provider = createProvider({
|
||
getFloorSpaces: vi.fn().mockResolvedValue([]),
|
||
getFloorPois: vi.fn().mockResolvedValue([
|
||
{
|
||
...floorPoi,
|
||
id: '5625',
|
||
name: '男卫生间001',
|
||
sourceNodeName: 'L2_男卫生间001',
|
||
position: { x: 1, y: 12, z: 1 }
|
||
},
|
||
{
|
||
...floorPoi,
|
||
id: '5629',
|
||
name: '卫生间01',
|
||
sourceNodeName: 'L2_男卫生间001',
|
||
position: { x: 9, y: 12, z: 9 }
|
||
}
|
||
])
|
||
})
|
||
const guideRepository = new SgsSdkGuideRepository(provider)
|
||
const modelRepository = new SgsSdkGuideModelRepository(provider, guideRepository)
|
||
|
||
const renderPois = await modelRepository.loadFloorPois('101')
|
||
|
||
expect(renderPois.filter((poi) => poi.id === '5625' || poi.id === '5629'))
|
||
.toHaveLength(2)
|
||
})
|
||
|
||
it('外观地图使用室外楼层预计算的空间标签位置', async () => {
|
||
const exteriorFloor = {
|
||
floorId: 'outdoor',
|
||
floorCode: 'EXTERIOR',
|
||
floorName: '外观/室外层',
|
||
sortOrder: 10
|
||
}
|
||
const exteriorRoadSpace = {
|
||
id: 'space-honghua-road',
|
||
name: '红花路',
|
||
type: 'road_area',
|
||
floorId: exteriorFloor.floorId,
|
||
sourceNodeName: '室外_红花路',
|
||
labelPosition: {
|
||
x: -1.206116,
|
||
y: 2.505,
|
||
z: -84.258389
|
||
},
|
||
meshCenter: {
|
||
x: -1.206116,
|
||
y: 0.005,
|
||
z: -84.258389
|
||
}
|
||
}
|
||
const getFloorSpaces = vi.fn().mockImplementation(async (floorId: string) => (
|
||
floorId === exteriorFloor.floorId ? [exteriorRoadSpace] : [theaterSpace]
|
||
))
|
||
const getFloorBundle = vi.fn().mockResolvedValue({
|
||
model: {
|
||
modelUrl: '/models/overview.glb'
|
||
}
|
||
})
|
||
const provider = createProvider({
|
||
getManifest: vi.fn().mockResolvedValue({
|
||
...manifest,
|
||
floors: [exteriorFloor, ...manifest.floors]
|
||
}),
|
||
getFloorPois: vi.fn().mockResolvedValue([]),
|
||
getFloorSpaces,
|
||
getFloorBundle
|
||
})
|
||
const repository = new SgsSdkGuideModelRepository(provider, new SgsSdkGuideRepository(provider))
|
||
|
||
const packageData = await repository.loadPackage()
|
||
const renderPois = await repository.loadFloorPois(exteriorFloor.floorId)
|
||
|
||
expect(packageData.overviewFloorId).toBe(exteriorFloor.floorId)
|
||
expect(getFloorSpaces).toHaveBeenCalledWith(exteriorFloor.floorId)
|
||
expect(renderPois).toContainEqual(expect.objectContaining({
|
||
id: 'space-space-honghua-road',
|
||
name: '红花路',
|
||
floorId: exteriorFloor.floorId,
|
||
sourceObjectName: '室外_红花路',
|
||
positionGltf: [-1.206116, 2.505, -84.258389]
|
||
}))
|
||
})
|
||
|
||
it('Manifest 提供模型地址时不再为每个楼层读取 Bundle', async () => {
|
||
const exteriorFloor = {
|
||
floorId: 'outdoor',
|
||
floorCode: 'EXTERIOR',
|
||
floorName: '外观/室外层',
|
||
sortOrder: 10,
|
||
modelUrl: '/models/overview-lightweight.glb',
|
||
fallbackModelUrl: '/models/overview.glb'
|
||
}
|
||
const indoorFloor = {
|
||
...manifest.floors[0],
|
||
modelUrl: '/models/l1-lightweight.glb',
|
||
fallbackModelUrl: '/models/l1.glb'
|
||
}
|
||
const getFloorBundle = vi.fn()
|
||
const provider = createProvider({
|
||
getManifest: vi.fn().mockResolvedValue({
|
||
...manifest,
|
||
floors: [exteriorFloor, indoorFloor]
|
||
}),
|
||
getFloorBundle
|
||
})
|
||
const repository = new SgsSdkGuideModelRepository(provider, new SgsSdkGuideRepository(provider))
|
||
|
||
await expect(repository.loadPackage()).resolves.toMatchObject({
|
||
overviewModelUrl: '/models/overview-lightweight.glb',
|
||
floors: [expect.objectContaining({
|
||
floorId: '101',
|
||
modelUrl: '/models/l1-lightweight.glb',
|
||
modelUrls: ['/models/l1-lightweight.glb', '/models/l1.glb']
|
||
})]
|
||
})
|
||
expect(getFloorBundle).not.toHaveBeenCalled()
|
||
})
|
||
|
||
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('点位数据加载失败,请检查网络后重试')
|
||
})
|
||
|
||
it('将同楼层编号售票机渲染为一个逻辑对象并保留全部物理锚点', async () => {
|
||
const numberedTicketMachines = [
|
||
{ id: '5682', name: 'L1_售票机_01', x: 10 },
|
||
{ id: '5683', name: 'L1_售票机_02', x: 20 },
|
||
{ id: '5684', name: 'L1_售票机_03', x: 30 },
|
||
{ id: '5685', name: 'L1_售票机_04', x: 40 }
|
||
].map(({ id, name, x }) => ({
|
||
id,
|
||
name,
|
||
type: 'ticket_office',
|
||
floorId: '101',
|
||
position: { x, y: 12, z: x }
|
||
}))
|
||
const provider = createProvider({
|
||
getFloorPois: vi.fn().mockResolvedValue([
|
||
...numberedTicketMachines,
|
||
{
|
||
id: '5591',
|
||
name: '售票机',
|
||
type: 'ticket_office',
|
||
floorId: '101',
|
||
position: { x: 50, y: 12, z: 50 }
|
||
},
|
||
{
|
||
id: '352502409978861752',
|
||
name: '展厅售票点',
|
||
type: 'ticket_office',
|
||
floorId: '101',
|
||
position: { x: 60, y: 12, z: 60 }
|
||
}
|
||
]),
|
||
getFloorSpaces: vi.fn().mockResolvedValue([])
|
||
})
|
||
const repository = new SgsSdkGuideModelRepository(provider)
|
||
|
||
const renderPois = await repository.loadFloorPois('101')
|
||
const numberedSeries = renderPois.find((poi) => poi.id === '5682')
|
||
|
||
expect(numberedSeries).toMatchObject({
|
||
name: '售票机',
|
||
positionGltf: [10, 12, 10],
|
||
displayPolicy: {
|
||
labelVisible: true,
|
||
minLabelTier: 'tight'
|
||
},
|
||
mergedDisplayPositions: [
|
||
[20, 12, 20],
|
||
[30, 12, 30],
|
||
[40, 12, 40]
|
||
],
|
||
mergedMarkerPositions: [
|
||
[20, 12, 20],
|
||
[30, 12, 30],
|
||
[40, 12, 40]
|
||
]
|
||
})
|
||
expect(renderPois.map((poi) => poi.id)).toEqual(expect.arrayContaining([
|
||
'5682',
|
||
'5591',
|
||
'352502409978861752'
|
||
]))
|
||
expect(renderPois.find((poi) => poi.id === '5591')?.displayPolicy).toMatchObject({
|
||
labelVisible: true,
|
||
minLabelTier: 'tight'
|
||
})
|
||
expect(renderPois.find((poi) => poi.id === '352502409978861752')?.displayPolicy).toMatchObject({
|
||
labelVisible: true,
|
||
minLabelTier: 'tight'
|
||
})
|
||
expect(renderPois).toHaveLength(3)
|
||
})
|
||
|
||
it('仅把 FLOOR_EXIT 导航点转换为楼层出入口标签', async () => {
|
||
const provider = createProvider({
|
||
getFloorPois: vi.fn().mockResolvedValue([]),
|
||
getFloorSpaces: vi.fn().mockResolvedValue([]),
|
||
getNavigablePlaces: vi.fn().mockResolvedValue([
|
||
{
|
||
id: '2077983437229297849',
|
||
name: '展览入口',
|
||
category: 'door',
|
||
anchorType: 'FLOOR_EXIT',
|
||
floorId: '101',
|
||
position: { x: -28.536788, y: 1, z: -4.662431 }
|
||
},
|
||
{
|
||
id: '2077983437229297850',
|
||
name: '观众次入口',
|
||
category: 'door',
|
||
anchorType: 'FLOOR_EXIT',
|
||
floorId: '101',
|
||
position: { x: -106.745851, y: 1, z: 76.900369 }
|
||
},
|
||
{
|
||
id: '2077983437229297851',
|
||
name: '影剧院入口',
|
||
category: 'door',
|
||
anchorType: 'FLOOR_EXIT',
|
||
floorId: '101',
|
||
position: { x: -8.49571, y: 1, z: 11.77092 }
|
||
},
|
||
{
|
||
id: 'space-entrance',
|
||
name: '普通空间入口',
|
||
category: 'door',
|
||
anchorType: 'SPACE_ENTRANCE',
|
||
floorId: '101',
|
||
position: { x: 1, y: 1, z: 1 }
|
||
},
|
||
{
|
||
id: 'poi-entrance',
|
||
name: '普通点位入口',
|
||
category: 'door',
|
||
anchorType: 'POI_ENTRANCE',
|
||
floorId: '101',
|
||
position: { x: 2, y: 1, z: 2 }
|
||
}
|
||
])
|
||
})
|
||
const repository = new SgsSdkGuideModelRepository(provider)
|
||
|
||
const renderPois = await repository.loadFloorPois('101')
|
||
const floorExits = renderPois.filter((poi) => poi.id.startsWith('floor-exit-'))
|
||
|
||
expect(floorExits.map((poi) => poi.name)).toEqual([
|
||
'展览入口',
|
||
'观众次入口',
|
||
'影剧院入口'
|
||
])
|
||
expect(floorExits[0]).toMatchObject({
|
||
positionGltf: [-28.536788, 1, -4.662431],
|
||
primaryCategory: 'navigation_anchor',
|
||
iconType: 'entrance_exit',
|
||
displayPolicy: {
|
||
labelVisible: true,
|
||
minLabelTier: 'tight'
|
||
}
|
||
})
|
||
expect(renderPois.some((poi) => poi.id.includes('space-entrance'))).toBe(false)
|
||
expect(renderPois.some((poi) => poi.id.includes('poi-entrance'))).toBe(false)
|
||
})
|
||
})
|