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 = {}) => ({ 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 和坐标的普通点位', 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('点位数据加载失败,请检查网络后重试') }) })