Files
frontend-miniapp/tests/unit/GuideRepository.spec.ts
lyf 8e187d6d4f
Some checks failed
CI / verify (push) Has been cancelled
修复点位精确匹配并调整加载提示
2026-07-12 22:31:07 +08:00

292 lines
10 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 { toMuseumPoiFromSgs } 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('合并普通点位与空间点位,并保留影院真实分类和楼层高度', 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: 'hall-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 === 'hall-space-theater')).toMatchObject({
primaryCategory: 'space_theater',
iconType: 'theater'
})
})
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('hall-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: 'hall-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 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 === 'hall-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 === 'hall-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('点位数据加载失败,请检查网络后重试')
})
})