整改点位搜索数据加载与分类逻辑
This commit is contained in:
408
tests/unit/GuideRepositorySearch.spec.ts
Normal file
408
tests/unit/GuideRepositorySearch.spec.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
resolvePoiCategory,
|
||||
type PoiCategoryId
|
||||
} from '@/domain/poiCategories'
|
||||
import type { SgsSdkApiProvider, SgsSdkManifestPayload } from '@/data/providers/sgsSdkApiProvider'
|
||||
import type { StaticNavAssetsProvider } from '@/data/providers/staticNavAssetsProvider'
|
||||
import { SgsSdkGuideRepository, StaticGuideRepository } from '@/repositories/GuideRepository'
|
||||
import {
|
||||
SgsSdkGuideModelRepository,
|
||||
StaticGuideModelRepository
|
||||
} from '@/repositories/GuideModelRepository'
|
||||
|
||||
const manifest: SgsSdkManifestPayload = {
|
||||
mapId: 'museum',
|
||||
floors: [{
|
||||
floorId: 'L1',
|
||||
floorCode: 'L1',
|
||||
floorName: '1F',
|
||||
sortOrder: 1
|
||||
}]
|
||||
}
|
||||
|
||||
const position = (x: number): [number, number, number] => [x, 12, x]
|
||||
|
||||
const allQuickCategoryIds: readonly PoiCategoryId[] = [
|
||||
'exhibition-hall',
|
||||
'cinema',
|
||||
'ticket-office',
|
||||
'dining',
|
||||
'shopping',
|
||||
'service-center',
|
||||
'restroom',
|
||||
'nursing-room',
|
||||
'elevator',
|
||||
'escalator'
|
||||
]
|
||||
|
||||
const floorSpaces = [
|
||||
{ id: 'space-hall', name: '地球展厅', type: 'exhibition_hall', floorId: 'L1', center: { x: 1, y: 12, z: 1 } },
|
||||
{ id: 'space-cinema', name: '穹幕影院', type: 'theater', floorId: 'L1', center: { x: 2, y: 12, z: 2 } },
|
||||
{ id: 'space-dining', name: '餐饮区', type: 'restaurant', floorId: 'L1', center: { x: 3, y: 12, z: 3 } },
|
||||
{ id: 'space-shopping', name: '文创商店', type: 'commercial', floorId: 'L1', center: { x: 4, y: 12, z: 4 } }
|
||||
]
|
||||
|
||||
const floorPois = [
|
||||
{ id: 'ticket', name: '售票处', type: 'ticket_office', floorId: 'L1', position: { x: 10, y: 12, z: 10 } },
|
||||
{ id: 'service', name: '咨询服务台', type: 'service_desk', floorId: 'L1', position: { x: 11, y: 12, z: 11 } },
|
||||
{
|
||||
id: 'restroom',
|
||||
name: '女卫 01',
|
||||
type: 'toilet',
|
||||
floorId: 'L1',
|
||||
spatialAreaId: 'shared-service-zone',
|
||||
position: { x: 12, y: 12, z: 12 }
|
||||
},
|
||||
{ id: 'nursing', name: '母婴室', type: 'mother_baby_room', floorId: 'L1', position: { x: 13, y: 12, z: 13 } },
|
||||
{
|
||||
id: 'elevator',
|
||||
name: '无障碍电梯',
|
||||
type: 'elevator',
|
||||
floorId: 'L1',
|
||||
spatialAreaId: 'shared-service-zone',
|
||||
position: { x: 14, y: 12, z: 14 }
|
||||
},
|
||||
{ id: 'escalator', name: '自动扶梯', type: 'escalator', floorId: 'L1', position: { x: 15, y: 12, z: 15 } },
|
||||
{
|
||||
id: 'poi-dining',
|
||||
name: '餐饮区',
|
||||
type: 'restaurant',
|
||||
floorId: 'L1',
|
||||
spatialAreaId: 'space-dining',
|
||||
spatialAreaName: '餐饮区',
|
||||
position: { x: 3, y: 12, z: 3 }
|
||||
},
|
||||
{ id: 'door', name: '北入口', type: 'entrance_exit', floorId: 'L1', position: { x: 16, y: 12, z: 16 } },
|
||||
{ id: 'anchor', name: '入口锚点', type: 'entrance_anchor', floorId: 'L1', position: { x: 17, y: 12, z: 17 } },
|
||||
{ id: 'route-node', name: '路线节点', type: 'route_node', floorId: 'L1', position: { x: 18, y: 12, z: 18 } },
|
||||
{ id: 'guide-stop', name: '讲解点', type: 'operation_experience', floorId: 'L1', position: { x: 19, y: 12, z: 19 } }
|
||||
]
|
||||
|
||||
const businessPois = [
|
||||
{
|
||||
id: 'business-dining',
|
||||
name: '餐饮区',
|
||||
type: 'restaurant',
|
||||
poiGroup: 'BUSINESS',
|
||||
businessType: 'restaurant',
|
||||
spatialAreaId: 'space-dining',
|
||||
spatialAreaName: '餐饮区',
|
||||
floorId: 'L1',
|
||||
position: { x: 3, y: 12, z: 3 }
|
||||
},
|
||||
{
|
||||
id: 'business-shopping',
|
||||
name: '文创商店',
|
||||
type: 'shop',
|
||||
poiGroup: 'BUSINESS',
|
||||
businessType: 'shop',
|
||||
spatialAreaId: 'space-shopping',
|
||||
spatialAreaName: '文创商店',
|
||||
floorId: 'L1',
|
||||
position: { x: 4, y: 12, z: 4 }
|
||||
}
|
||||
]
|
||||
|
||||
const createSgsProvider = (overrides: Partial<SgsSdkApiProvider> = {}) => ({
|
||||
getManifest: vi.fn().mockResolvedValue(manifest),
|
||||
getFloorPois: vi.fn().mockResolvedValue(floorPois),
|
||||
getFloorBusinessPois: vi.fn().mockResolvedValue(businessPois),
|
||||
getFloorSpaces: vi.fn().mockResolvedValue(floorSpaces),
|
||||
getNavigablePlaces: vi.fn().mockResolvedValue([
|
||||
{ id: 'nav-door', name: '入口', type: 'navigable_place', floorId: 'L1', position: { x: 20, y: 12, z: 20 } }
|
||||
]),
|
||||
...overrides
|
||||
}) as unknown as SgsSdkApiProvider
|
||||
|
||||
describe('GuideRepository visitor search contracts', () => {
|
||||
it('uses spaces for default browse, exposes all ten quick categories, and never returns anchors', async () => {
|
||||
const provider = createSgsProvider()
|
||||
const repository = new SgsSdkGuideRepository(provider)
|
||||
|
||||
const [defaultResults, availability] = await Promise.all([
|
||||
repository.listDestinationPois('L1'),
|
||||
repository.getQuickFindCategoryAvailability('L1')
|
||||
])
|
||||
|
||||
expect(defaultResults.map((poi) => poi.id)).toEqual(expect.arrayContaining([
|
||||
'hall-space-hall',
|
||||
'hall-space-cinema',
|
||||
'space-space-dining',
|
||||
'space-space-shopping'
|
||||
]))
|
||||
expect(defaultResults.map((poi) => poi.id)).not.toEqual(expect.arrayContaining([
|
||||
'restroom',
|
||||
'elevator',
|
||||
'ticket',
|
||||
'door',
|
||||
'anchor',
|
||||
'route-node',
|
||||
'guide-stop'
|
||||
]))
|
||||
expect(availability).toEqual(allQuickCategoryIds.map((id) => ({
|
||||
id,
|
||||
count: 1,
|
||||
disabled: false
|
||||
})))
|
||||
|
||||
await Promise.all(allQuickCategoryIds.map(async (categoryId) => {
|
||||
const results = await repository.listQuickFindPois(categoryId, 'L1')
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].floorId).toBe('L1')
|
||||
expect(results[0].positionGltf).toBeDefined()
|
||||
expect(resolvePoiCategory(results[0])?.id).toBe(categoryId)
|
||||
}))
|
||||
|
||||
const renderPois = await new SgsSdkGuideModelRepository(provider, repository).loadFloorPois('L1')
|
||||
const renderPoiIds = new Set(renderPois.map((poi) => poi.id))
|
||||
// Facilities in one spatial area are distinct visitor places and must both
|
||||
// survive model marker deduplication.
|
||||
expect(renderPoiIds.has('restroom')).toBe(true)
|
||||
expect(renderPoiIds.has('elevator')).toBe(true)
|
||||
for (const hiddenPoiId of ['door', 'anchor', 'route-node', 'guide-stop']) {
|
||||
expect(renderPoiIds.has(hiddenPoiId)).toBe(false)
|
||||
}
|
||||
const quickResultIds = (await Promise.all(allQuickCategoryIds.map((categoryId) => (
|
||||
repository.listQuickFindPois(categoryId, 'L1')
|
||||
)))).flat().map((poi) => poi.id)
|
||||
expect([...defaultResults, ...quickResultIds.map((id) => ({ id }))]
|
||||
.every((poi) => renderPoiIds.has(poi.id))).toBe(true)
|
||||
})
|
||||
|
||||
it('normalizes restaurant/shop sources, deduplicates a real place, and excludes non-visitor search records', async () => {
|
||||
const repository = new SgsSdkGuideRepository(createSgsProvider())
|
||||
|
||||
const [dining, shopping, keywordDining, keywordRestroom, hiddenKeyword] = await Promise.all([
|
||||
repository.listQuickFindPois('dining', 'L1'),
|
||||
repository.listQuickFindPois('shopping', 'L1'),
|
||||
repository.searchPois('餐饮', 'L1'),
|
||||
repository.searchPois('洗手间', 'L1'),
|
||||
repository.searchPois('入口', 'L1')
|
||||
])
|
||||
|
||||
expect(dining.map((poi) => poi.id)).toEqual(['space-space-dining'])
|
||||
expect(shopping.map((poi) => poi.id)).toEqual(['space-space-shopping'])
|
||||
expect(keywordDining.map((poi) => poi.id)).toEqual(['space-space-dining'])
|
||||
expect(keywordRestroom.map((poi) => poi.id)).toEqual(['restroom'])
|
||||
expect(hiddenKeyword).toEqual([])
|
||||
})
|
||||
|
||||
it('deduplicates co-located sources when they do not provide a shared spatial area ID', async () => {
|
||||
const repository = new SgsSdkGuideRepository(createSgsProvider({
|
||||
getFloorPois: vi.fn().mockResolvedValue([{
|
||||
id: 'poi-cafe',
|
||||
name: '中庭咖啡厅',
|
||||
type: 'restaurant',
|
||||
floorId: 'L1',
|
||||
position: { x: 40, y: 12, z: 40 }
|
||||
}]),
|
||||
getFloorBusinessPois: vi.fn().mockResolvedValue([{
|
||||
id: 'business-cafe',
|
||||
name: '中庭咖啡厅',
|
||||
type: 'restaurant',
|
||||
poiGroup: 'BUSINESS',
|
||||
businessType: 'restaurant',
|
||||
floorId: 'L1',
|
||||
position: { x: 40, y: 12, z: 40 }
|
||||
}]),
|
||||
getFloorSpaces: vi.fn().mockResolvedValue([{
|
||||
id: 'space-cafe',
|
||||
name: '中庭咖啡厅',
|
||||
type: 'restaurant',
|
||||
floorId: 'L1',
|
||||
center: { x: 40, y: 12, z: 40 }
|
||||
}])
|
||||
}))
|
||||
|
||||
const [quickFind, keyword] = await Promise.all([
|
||||
repository.listQuickFindPois('dining', 'L1'),
|
||||
repository.searchPois('咖啡', 'L1')
|
||||
])
|
||||
|
||||
expect(quickFind).toHaveLength(1)
|
||||
expect(keyword).toHaveLength(1)
|
||||
expect(quickFind[0]).toMatchObject({ kind: 'space', name: '中庭咖啡厅' })
|
||||
expect(keyword.map((poi) => poi.id)).toEqual(quickFind.map((poi) => poi.id))
|
||||
})
|
||||
|
||||
it('deduplicates source representations linked to one space before positional fallback', async () => {
|
||||
const repository = new SgsSdkGuideRepository(createSgsProvider({
|
||||
getFloorPois: vi.fn().mockResolvedValue([{
|
||||
id: 'poi-cafe-counter',
|
||||
name: '咖啡取餐台',
|
||||
type: 'restaurant',
|
||||
floorId: 'L1',
|
||||
spatialAreaId: 'space-cafe',
|
||||
position: { x: 45, y: 12, z: 45 }
|
||||
}]),
|
||||
getFloorBusinessPois: vi.fn().mockResolvedValue([{
|
||||
id: 'business-cafe',
|
||||
name: '中庭咖啡厅',
|
||||
type: 'restaurant',
|
||||
poiGroup: 'BUSINESS',
|
||||
businessType: 'restaurant',
|
||||
floorId: 'L1',
|
||||
spatialAreaId: 'space-cafe',
|
||||
position: { x: 46, y: 12, z: 46 }
|
||||
}]),
|
||||
getFloorSpaces: vi.fn().mockResolvedValue([{
|
||||
id: 'space-cafe',
|
||||
name: '中庭咖啡厅',
|
||||
type: 'restaurant',
|
||||
floorId: 'L1',
|
||||
center: { x: 47, y: 12, z: 47 }
|
||||
}])
|
||||
}))
|
||||
|
||||
const [quickFind, keyword] = await Promise.all([
|
||||
repository.listQuickFindPois('dining', 'L1'),
|
||||
repository.searchPois('咖啡', 'L1')
|
||||
])
|
||||
|
||||
expect(quickFind).toEqual([
|
||||
expect.objectContaining({ id: 'space-space-cafe', kind: 'space' })
|
||||
])
|
||||
expect(keyword.map((poi) => poi.id)).toEqual(quickFind.map((poi) => poi.id))
|
||||
})
|
||||
|
||||
it('keeps an unlinked commercial POI out of default destinations but exposes it through shopping', async () => {
|
||||
const repository = new SgsSdkGuideRepository(createSgsProvider({
|
||||
getFloorPois: vi.fn().mockResolvedValue([]),
|
||||
getFloorBusinessPois: vi.fn().mockResolvedValue([{
|
||||
id: 'business-commercial',
|
||||
name: '纪念品商店',
|
||||
type: 'commercial',
|
||||
poiGroup: 'BUSINESS',
|
||||
businessType: 'commercial',
|
||||
floorId: 'L1',
|
||||
position: { x: 30, y: 12, z: 30 }
|
||||
}]),
|
||||
getFloorSpaces: vi.fn().mockResolvedValue([])
|
||||
}))
|
||||
|
||||
await expect(repository.listDestinationPois('L1')).resolves.toEqual([])
|
||||
await expect(repository.listQuickFindPois('shopping', 'L1')).resolves.toMatchObject([
|
||||
{ id: 'business-commercial', kind: 'facility' }
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps an unlinked restaurant POI out of default destinations but exposes it through dining and keyword search', async () => {
|
||||
const repository = new SgsSdkGuideRepository(createSgsProvider({
|
||||
getFloorPois: vi.fn().mockResolvedValue([{
|
||||
id: 'poi-restaurant',
|
||||
name: '咖啡餐饮点',
|
||||
type: 'restaurant',
|
||||
floorId: 'L1',
|
||||
position: { x: 31, y: 12, z: 31 }
|
||||
}]),
|
||||
getFloorBusinessPois: vi.fn().mockResolvedValue([]),
|
||||
getFloorSpaces: vi.fn().mockResolvedValue([])
|
||||
}))
|
||||
|
||||
await expect(repository.listDestinationPois('L1')).resolves.toEqual([])
|
||||
await expect(repository.listQuickFindPois('dining', 'L1')).resolves.toMatchObject([
|
||||
{ id: 'poi-restaurant', kind: 'facility' }
|
||||
])
|
||||
await expect(repository.searchPois('咖啡', 'L1')).resolves.toMatchObject([
|
||||
{ id: 'poi-restaurant' }
|
||||
])
|
||||
})
|
||||
|
||||
it('falls back to static hall destinations when no independent space source exists', async () => {
|
||||
const provider = {
|
||||
baseUrl: '',
|
||||
loadPoiIndex: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'static-hall',
|
||||
name: '地球展厅',
|
||||
floorId: 'L1',
|
||||
primaryCategory: 'touring_poi',
|
||||
primaryCategoryZh: '展厅',
|
||||
iconType: 'exhibition_hall',
|
||||
positionGltf: position(1)
|
||||
},
|
||||
{
|
||||
id: 'static-restroom',
|
||||
name: '男卫生间',
|
||||
floorId: 'L1',
|
||||
primaryCategory: 'basic_service_facility',
|
||||
primaryCategoryZh: '服务设施',
|
||||
iconType: 'toilet',
|
||||
positionGltf: position(2)
|
||||
},
|
||||
{
|
||||
id: 'static-service-desk',
|
||||
name: '服务台',
|
||||
floorId: 'L1',
|
||||
primaryCategory: 'basic_service_facility',
|
||||
primaryCategoryZh: '基础服务设施',
|
||||
iconType: 'restroom',
|
||||
positionGltf: position(3)
|
||||
},
|
||||
{
|
||||
id: 'static-tea-room',
|
||||
name: '茶水间',
|
||||
floorId: 'L1',
|
||||
primaryCategory: 'basic_service_facility',
|
||||
primaryCategoryZh: '基础服务设施',
|
||||
iconType: 'restroom',
|
||||
positionGltf: position(4)
|
||||
}
|
||||
])
|
||||
} as unknown as StaticNavAssetsProvider
|
||||
const repository = new StaticGuideRepository(provider)
|
||||
|
||||
await expect(repository.listDestinationPois('L1')).resolves.toMatchObject([
|
||||
{ id: 'static-hall', kind: 'hall' }
|
||||
])
|
||||
await expect(repository.listQuickFindPois('restroom', 'L1')).resolves.toMatchObject([
|
||||
{ id: 'static-restroom' }
|
||||
])
|
||||
await expect(repository.searchPois('洗手间', 'L1')).resolves.toMatchObject([
|
||||
{ id: 'static-restroom' }
|
||||
])
|
||||
await expect(repository.searchPois('服务', 'L1')).resolves.toEqual([
|
||||
expect.objectContaining({ id: 'static-service-desk' })
|
||||
])
|
||||
await expect(repository.listQuickFindPois('service-center', 'L1')).resolves.toMatchObject([
|
||||
{ id: 'static-service-desk' }
|
||||
])
|
||||
await expect(repository.getQuickFindCategoryAvailability('L1')).resolves.toEqual(expect.arrayContaining([
|
||||
{ id: 'service-center', count: 1, disabled: false },
|
||||
{ id: 'restroom', count: 1, disabled: false }
|
||||
]))
|
||||
})
|
||||
|
||||
it('keeps static guide records out of the model render pool', async () => {
|
||||
const provider = {
|
||||
loadFloorIndex: vi.fn().mockResolvedValue({
|
||||
floors: [{ floorId: 'L1', poiDataAsset: 'poi_by_floor/L1.json' }]
|
||||
}),
|
||||
loadFloorPois: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'static-hall',
|
||||
name: '地球展厅',
|
||||
floorId: 'L1',
|
||||
primaryCategory: 'touring_poi',
|
||||
primaryCategoryZh: '展厅',
|
||||
iconType: 'exhibition_hall',
|
||||
positionGltf: position(1)
|
||||
},
|
||||
{
|
||||
id: 'static-guide-point',
|
||||
name: '讲解点',
|
||||
floorId: 'L1',
|
||||
primaryCategory: 'operation_experience',
|
||||
primaryCategoryZh: '讲解点',
|
||||
iconType: 'guide',
|
||||
positionGltf: position(2)
|
||||
}
|
||||
])
|
||||
} as unknown as StaticNavAssetsProvider
|
||||
|
||||
const renderPois = await new StaticGuideModelRepository(provider).loadFloorPois('L1')
|
||||
|
||||
expect(renderPois.map((poi) => poi.id)).toEqual(['static-hall'])
|
||||
})
|
||||
})
|
||||
194
tests/unit/GuideUseCase.spec.ts
Normal file
194
tests/unit/GuideUseCase.spec.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { MuseumFloor, MuseumPoi } from '@/domain/museum'
|
||||
import type { PoiCategoryId } from '@/domain/poiCategories'
|
||||
import type { GuideRepository } from '@/repositories/GuideRepository'
|
||||
import type { GuideModelRepository } from '@/repositories/GuideModelRepository'
|
||||
import { GuideUseCase } from '@/usecases/guideUseCase'
|
||||
|
||||
const floors: MuseumFloor[] = [
|
||||
{ id: 'L1', label: '1F', order: 1 },
|
||||
{ id: 'L2', label: '2F', order: 2 }
|
||||
]
|
||||
|
||||
const quickCategoryIds: readonly PoiCategoryId[] = [
|
||||
'exhibition-hall',
|
||||
'cinema',
|
||||
'ticket-office',
|
||||
'dining',
|
||||
'shopping',
|
||||
'service-center',
|
||||
'restroom',
|
||||
'nursing-room',
|
||||
'elevator',
|
||||
'escalator'
|
||||
]
|
||||
|
||||
const createPoi = (id: string, floorId: string, categoryId: string): MuseumPoi => ({
|
||||
id,
|
||||
name: id,
|
||||
floorId,
|
||||
floorLabel: floorId === 'L2' ? '2F' : '1F',
|
||||
primaryCategory: {
|
||||
id: categoryId,
|
||||
label: categoryId,
|
||||
iconType: categoryId
|
||||
},
|
||||
categories: [],
|
||||
accessible: true
|
||||
})
|
||||
|
||||
const destinationsByFloor: Record<string, MuseumPoi[]> = {
|
||||
L1: [createPoi('space-l1-hall', 'L1', 'exhibition_hall')],
|
||||
L2: [createPoi('space-l2-hall', 'L2', 'exhibition_hall')]
|
||||
}
|
||||
|
||||
const quickResultsByFloor: Record<string, Partial<Record<PoiCategoryId, MuseumPoi[]>>> = {
|
||||
L1: {
|
||||
restroom: [createPoi('facility-l1-restroom', 'L1', 'toilet')]
|
||||
},
|
||||
L2: {
|
||||
restroom: [createPoi('facility-l2-restroom', 'L2', 'toilet')]
|
||||
}
|
||||
}
|
||||
|
||||
const keywordResultsByFloor: Record<string, MuseumPoi[]> = {
|
||||
L1: [createPoi('facility-l1-shop', 'L1', 'shop')],
|
||||
L2: [createPoi('facility-l2-shop', 'L2', 'shop')]
|
||||
}
|
||||
|
||||
const availability = quickCategoryIds.map((id, index) => ({
|
||||
id,
|
||||
count: index === quickCategoryIds.indexOf('restroom') ? 1 : 0,
|
||||
disabled: id !== 'restroom'
|
||||
}))
|
||||
|
||||
const createRepository = () => ({
|
||||
getAssetBaseUrl: vi.fn(() => ''),
|
||||
getFloors: vi.fn().mockResolvedValue(floors),
|
||||
normalizeFloorId: vi.fn((floorId: string) => ({
|
||||
'1F': 'L1',
|
||||
'2F': 'L2'
|
||||
}[floorId] || floorId)),
|
||||
listPois: vi.fn().mockResolvedValue([]),
|
||||
listSpacePoints: vi.fn().mockResolvedValue([]),
|
||||
listDestinationPois: vi.fn((floorId?: string) => (
|
||||
Promise.resolve(destinationsByFloor[floorId || 'L1'] || [])
|
||||
)),
|
||||
listQuickFindPois: vi.fn((categoryId: PoiCategoryId, floorId?: string) => (
|
||||
Promise.resolve(quickResultsByFloor[floorId || 'L1']?.[categoryId] || [])
|
||||
)),
|
||||
getQuickFindCategoryAvailability: vi.fn().mockResolvedValue(availability),
|
||||
getPoiById: vi.fn().mockResolvedValue(null),
|
||||
searchPois: vi.fn((keyword?: string, floorId?: string) => (
|
||||
Promise.resolve(keyword ? keywordResultsByFloor[floorId || 'L1'] || [] : [])
|
||||
)),
|
||||
getLocationPreview: vi.fn().mockResolvedValue(null),
|
||||
getRouteReadiness: vi.fn().mockResolvedValue({})
|
||||
})
|
||||
|
||||
const expectMapListParity = (state: { results: MuseumPoi[], visiblePoiIds: string[] }) => {
|
||||
expect(state.visiblePoiIds).toEqual(state.results.map((poi) => poi.id))
|
||||
}
|
||||
|
||||
describe('GuideUseCase point search state', () => {
|
||||
it('loads initial destinations through listDestinationPois and never searchPois', async () => {
|
||||
const repository = createRepository()
|
||||
const useCase = new GuideUseCase(
|
||||
repository as unknown as GuideRepository,
|
||||
{} as GuideModelRepository
|
||||
)
|
||||
|
||||
await expect(useCase.getInitialSearchSpacePoints('L2'))
|
||||
.resolves.toEqual(destinationsByFloor.L2)
|
||||
|
||||
expect(repository.listDestinationPois).toHaveBeenCalledWith('L2')
|
||||
expect(repository.searchPois).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates the default state from current-floor destinations and relays all ten availability entries', async () => {
|
||||
const repository = createRepository()
|
||||
const useCase = new GuideUseCase(
|
||||
repository as unknown as GuideRepository,
|
||||
{} as GuideModelRepository
|
||||
)
|
||||
|
||||
const state = await useCase.createInitialPoiSearchState('L1')
|
||||
|
||||
expect(state).toMatchObject({
|
||||
mode: 'default',
|
||||
floorId: 'L1',
|
||||
floorLabel: '1F',
|
||||
keyword: '',
|
||||
categoryId: ''
|
||||
})
|
||||
expect(state.results).toEqual(destinationsByFloor.L1)
|
||||
expectMapListParity(state)
|
||||
expect(state.categories.map((item) => ({
|
||||
id: item.definition.id,
|
||||
count: item.count,
|
||||
disabled: item.disabled
|
||||
}))).toEqual(availability)
|
||||
expect(repository.getQuickFindCategoryAvailability).toHaveBeenCalledWith('L1')
|
||||
expect(repository.listDestinationPois).toHaveBeenCalledWith('L1')
|
||||
expect(repository.searchPois).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps category, keyword, clear, and floor-change states on canonical result IDs', async () => {
|
||||
const repository = createRepository()
|
||||
const useCase = new GuideUseCase(
|
||||
repository as unknown as GuideRepository,
|
||||
{} as GuideModelRepository
|
||||
)
|
||||
|
||||
const initial = await useCase.createInitialPoiSearchState('L1')
|
||||
const category = await useCase.selectPoiSearchCategory(initial, 'restroom')
|
||||
|
||||
expect(category).toMatchObject({
|
||||
mode: 'category',
|
||||
floorId: 'L1',
|
||||
categoryId: 'restroom'
|
||||
})
|
||||
expect(category.results).toEqual(quickResultsByFloor.L1.restroom)
|
||||
expectMapListParity(category)
|
||||
expect(repository.listQuickFindPois).toHaveBeenCalledWith('restroom', 'L1')
|
||||
expect(repository.searchPois).not.toHaveBeenCalled()
|
||||
|
||||
const keyword = await useCase.searchPoiKeyword(category, 'shop')
|
||||
|
||||
expect(keyword).toMatchObject({
|
||||
mode: 'keyword',
|
||||
floorId: 'L1',
|
||||
keyword: 'shop',
|
||||
categoryId: ''
|
||||
})
|
||||
expect(keyword.results).toEqual(keywordResultsByFloor.L1)
|
||||
expectMapListParity(keyword)
|
||||
expect(repository.searchPois).toHaveBeenLastCalledWith('shop', 'L1')
|
||||
|
||||
const switchedFloor = await useCase.changePoiSearchFloor(keyword, 'L2')
|
||||
|
||||
expect(switchedFloor).toMatchObject({
|
||||
mode: 'keyword',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
keyword: 'shop',
|
||||
categoryId: ''
|
||||
})
|
||||
expect(switchedFloor.results).toEqual(keywordResultsByFloor.L2)
|
||||
expectMapListParity(switchedFloor)
|
||||
expect(repository.searchPois).toHaveBeenLastCalledWith('shop', 'L2')
|
||||
|
||||
const cleared = await useCase.clearPoiSearch(switchedFloor)
|
||||
|
||||
expect(cleared).toMatchObject({
|
||||
mode: 'default',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
keyword: '',
|
||||
categoryId: ''
|
||||
})
|
||||
expect(cleared.results).toEqual(destinationsByFloor.L2)
|
||||
expectMapListParity(cleared)
|
||||
expect(repository.listDestinationPois).toHaveBeenLastCalledWith('L2')
|
||||
})
|
||||
})
|
||||
@@ -373,6 +373,19 @@ describe('首页搜索与地图闭环', () => {
|
||||
expect(map.props('activeFloor')).toBe('L2')
|
||||
expect(map.props('indoorView')).toBe('floor')
|
||||
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
||||
|
||||
search.vm.$emit('results-change', {
|
||||
...categoryContext,
|
||||
floorId: 'L1',
|
||||
floorLabel: '1F',
|
||||
visiblePoiIds: [],
|
||||
active: true,
|
||||
pending: true
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(map.props('activeFloor')).toBe('L2')
|
||||
expect(map.props('visiblePoiIds')).toEqual([])
|
||||
})
|
||||
|
||||
it('楼层数据按高到低返回时,首次进入仍将 1F 作为自动缩放的默认目标', async () => {
|
||||
|
||||
@@ -4,11 +4,20 @@ 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'
|
||||
import type { GuidePoiSearchViewState } from '@/domain/poiSearch'
|
||||
import {
|
||||
HOME_POI_CATEGORIES,
|
||||
POI_CATEGORIES,
|
||||
type PoiCategoryId
|
||||
} from '@/domain/poiCategories'
|
||||
|
||||
const guideMocks = vi.hoisted(() => ({
|
||||
getFloors: vi.fn(),
|
||||
getInitialSearchSpacePoints: vi.fn(),
|
||||
searchPois: vi.fn()
|
||||
createInitialPoiSearchState: vi.fn(),
|
||||
selectPoiSearchCategory: vi.fn(),
|
||||
searchPoiKeyword: vi.fn(),
|
||||
clearPoiSearch: vi.fn(),
|
||||
changePoiSearchFloor: vi.fn()
|
||||
}))
|
||||
|
||||
const hostEnvironmentMocks = vi.hoisted(() => ({
|
||||
@@ -33,56 +42,184 @@ const createPoi = (
|
||||
name: string,
|
||||
floorId: string,
|
||||
floorLabel: string,
|
||||
iconType: string,
|
||||
positionGltf?: [number, number, number]
|
||||
categoryId: string,
|
||||
positionGltf: [number, number, number]
|
||||
): MuseumPoi => ({
|
||||
id,
|
||||
name,
|
||||
floorId,
|
||||
floorLabel,
|
||||
primaryCategory: {
|
||||
id: iconType,
|
||||
label: name.includes('卫生间') ? '卫生间' : name.includes('母婴') ? '母婴室' : '展厅',
|
||||
iconType
|
||||
id: categoryId,
|
||||
label: categoryId,
|
||||
iconType: categoryId
|
||||
},
|
||||
categories: [],
|
||||
positionGltf,
|
||||
accessible: true
|
||||
})
|
||||
|
||||
const poiPool: MuseumPoi[] = [
|
||||
const destinationFloor1 = [
|
||||
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])
|
||||
createPoi('space-1', '生态展区', 'floor-1', '1F', 'space_destination', [2, 0, 2])
|
||||
]
|
||||
const destinationFloor2 = [
|
||||
createPoi('hall-2', '恐龙厅', 'floor-2', '2F', 'exhibition_hall', [3, 0, 3])
|
||||
]
|
||||
const restroomFloor1 = [
|
||||
createPoi('restroom-1', '洗手间 A', 'floor-1', '1F', 'toilet', [4, 0, 4])
|
||||
]
|
||||
const restroomFloor2 = [
|
||||
createPoi('restroom-2', '洗手间 B', 'floor-2', '2F', 'toilet', [5, 0, 5])
|
||||
]
|
||||
const keywordFloor1 = [
|
||||
createPoi('dinosaur-1', '恐龙化石', 'floor-1', '1F', 'exhibition_hall', [6, 0, 6]),
|
||||
createPoi('dinosaur-shop-1', '恐龙文创店', 'floor-1', '1F', 'shop', [7, 0, 7])
|
||||
]
|
||||
|
||||
type SearchStateOptions = {
|
||||
mode?: GuidePoiSearchViewState['mode']
|
||||
floorId?: string
|
||||
keyword?: string
|
||||
categoryId?: PoiCategoryId | ''
|
||||
results?: MuseumPoi[]
|
||||
disabledCategoryIds?: PoiCategoryId[]
|
||||
}
|
||||
|
||||
const floorFor = (floorId = 'floor-1') => (
|
||||
floors.find((floor) => floor.id === floorId) || floors[0]
|
||||
)
|
||||
|
||||
const createSearchState = ({
|
||||
mode = 'default',
|
||||
floorId = 'floor-1',
|
||||
keyword = '',
|
||||
categoryId = '',
|
||||
results,
|
||||
disabledCategoryIds = []
|
||||
}: SearchStateOptions = {}): GuidePoiSearchViewState => {
|
||||
const floor = floorFor(floorId)
|
||||
const resolvedResults = results || (floor.id === 'floor-2' ? destinationFloor2 : destinationFloor1)
|
||||
const disabled = new Set(disabledCategoryIds)
|
||||
|
||||
return {
|
||||
mode,
|
||||
floorId: floor.id,
|
||||
floorLabel: floor.label,
|
||||
keyword,
|
||||
categoryId,
|
||||
results: resolvedResults,
|
||||
visiblePoiIds: resolvedResults.map((poi) => poi.id),
|
||||
categories: POI_CATEGORIES.map((definition) => ({
|
||||
definition,
|
||||
count: disabled.has(definition.id)
|
||||
? 0
|
||||
: definition.id === categoryId
|
||||
? resolvedResults.length
|
||||
: 1,
|
||||
disabled: disabled.has(definition.id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const createDeferred = <T>() => {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
const renderedResultIds = (wrapper: ReturnType<typeof mount>) => (
|
||||
wrapper.findAll('.result-row').map((row) => (
|
||||
row.attributes('data-testid').replace('poi-result-', '')
|
||||
))
|
||||
)
|
||||
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
const lastResultState = (wrapper: ReturnType<typeof mount>) => (
|
||||
(wrapper.emitted('results-change') || []).at(-1)?.[0] as {
|
||||
visiblePoiIds: string[]
|
||||
active: boolean
|
||||
floorId: string
|
||||
categoryId: string
|
||||
keyword: string
|
||||
pending?: boolean
|
||||
}
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
|
||||
guideMocks.getFloors.mockReset()
|
||||
guideMocks.createInitialPoiSearchState.mockReset()
|
||||
guideMocks.selectPoiSearchCategory.mockReset()
|
||||
guideMocks.searchPoiKeyword.mockReset()
|
||||
guideMocks.clearPoiSearch.mockReset()
|
||||
guideMocks.changePoiSearchFloor.mockReset()
|
||||
|
||||
guideMocks.getFloors.mockResolvedValue(floors)
|
||||
guideMocks.getInitialSearchSpacePoints.mockResolvedValue(poiPool)
|
||||
guideMocks.searchPois.mockImplementation(async (keyword = '') => (
|
||||
keyword
|
||||
? poiPool.filter((poi) => poi.name.includes(keyword))
|
||||
: poiPool
|
||||
guideMocks.createInitialPoiSearchState.mockImplementation(async (floorId?: string) => (
|
||||
createSearchState({ floorId })
|
||||
))
|
||||
guideMocks.selectPoiSearchCategory.mockImplementation(async (
|
||||
_state: GuidePoiSearchViewState | null,
|
||||
categoryId: PoiCategoryId,
|
||||
floorId?: string
|
||||
) => {
|
||||
const isFloor2 = floorFor(floorId).id === 'floor-2'
|
||||
const results = categoryId === 'restroom'
|
||||
? isFloor2 ? restroomFloor2 : restroomFloor1
|
||||
: []
|
||||
return createSearchState({
|
||||
mode: 'category',
|
||||
floorId,
|
||||
categoryId,
|
||||
keyword: POI_CATEGORIES.find((category) => category.id === categoryId)?.label || '',
|
||||
results
|
||||
})
|
||||
})
|
||||
guideMocks.searchPoiKeyword.mockImplementation(async (
|
||||
_state: GuidePoiSearchViewState | null,
|
||||
keyword: string,
|
||||
floorId?: string
|
||||
) => createSearchState({
|
||||
mode: 'keyword',
|
||||
floorId,
|
||||
keyword,
|
||||
results: floorFor(floorId).id === 'floor-2' ? destinationFloor2 : keywordFloor1
|
||||
}))
|
||||
guideMocks.clearPoiSearch.mockImplementation(async (
|
||||
_state: GuidePoiSearchViewState | null,
|
||||
floorId?: string
|
||||
) => createSearchState({ floorId }))
|
||||
guideMocks.changePoiSearchFloor.mockImplementation(async (
|
||||
state: GuidePoiSearchViewState | null,
|
||||
floorId: string
|
||||
) => {
|
||||
if (state?.mode === 'category') {
|
||||
return createSearchState({
|
||||
mode: 'category',
|
||||
floorId,
|
||||
categoryId: state.categoryId,
|
||||
keyword: state.keyword,
|
||||
results: state.categoryId === 'restroom' ? restroomFloor2 : []
|
||||
})
|
||||
}
|
||||
if (state?.mode === 'keyword') {
|
||||
return createSearchState({
|
||||
mode: 'keyword',
|
||||
floorId,
|
||||
keyword: state.keyword,
|
||||
results: destinationFloor2
|
||||
})
|
||||
}
|
||||
return createSearchState({ floorId })
|
||||
})
|
||||
|
||||
vi.stubGlobal('uni', {
|
||||
hideKeyboard: vi.fn(),
|
||||
navigateTo: vi.fn(),
|
||||
@@ -96,101 +233,72 @@ afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('POI 搜索面板实际渲染与状态闭环', () => {
|
||||
it('不渲染尚未支持的语音搜索入口', () => {
|
||||
describe('PoiSearchPanel 搜索状态契约', () => {
|
||||
it('固定展示 10 类快捷查找,并为收起和展开区域提供 aria-label', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
const expectedLabels = ['展厅', '影院', '售票处', '餐饮', '购物', '服务', '洗手间', '母婴', '电梯', '扶梯']
|
||||
const collapsedCategories = wrapper.findAll('.home-category-chip')
|
||||
expect(collapsedCategories).toHaveLength(10)
|
||||
expect(collapsedCategories.map((item) => item.text()).sort()).toEqual([...expectedLabels].sort())
|
||||
expect(wrapper.get('.home-category-strip').attributes('aria-label')).toBe('快捷查找')
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.findAll('.home-shortcut-grid .category-item')).toHaveLength(10)
|
||||
expect(wrapper.get('.category-scroll').attributes('aria-label')).toBe('快捷查找')
|
||||
expect(HOME_POI_CATEGORIES).toHaveLength(10)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('无结果分类具有禁用态和 aria-disabled,点击不会发起分类查询', async () => {
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(createSearchState({
|
||||
disabledCategoryIds: ['cinema']
|
||||
}))
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
const cinema = wrapper.get('[data-testid="poi-category-cinema"]')
|
||||
expect(cinema.classes()).toContain('disabled')
|
||||
expect(cinema.attributes('aria-disabled')).toBe('true')
|
||||
await cinema.trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(guideMocks.selectPoiSearchCategory).not.toHaveBeenCalled()
|
||||
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('默认列表只消费 createInitialPoiSearchState 返回的当前楼层空间目的地', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
|
||||
expect(wrapper.find('.voice-icon').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('首页收缩和展开复用同一份九项快捷入口,搜索页保留完整分类', async () => {
|
||||
const escalatorPoi = createPoi('escalator-1', '自动扶梯', 'floor-1', '1F', 'escalator', [5, 0, 5])
|
||||
guideMocks.getInitialSearchSpacePoints.mockResolvedValue([...poiPool, escalatorPoi])
|
||||
const homeWrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
const homeCategories = homeWrapper.findAll('.home-category-chip')
|
||||
expect(homeCategories).toHaveLength(9)
|
||||
expect(homeCategories.map((item) => item.text())).toEqual([
|
||||
'展览', '影院', '洗手间', '餐厅', '售票处',
|
||||
'母婴室', '服务中心', '电梯', '扶梯'
|
||||
])
|
||||
expect(homeCategories.slice(0, 5).map((item) => item.text())).toEqual([
|
||||
'展览', '影院', '洗手间', '餐厅', '售票处'
|
||||
])
|
||||
homeCategories.forEach((item) => {
|
||||
expect(item.find('.poi-category-icon').exists()).toBe(true)
|
||||
expect(item.find('.home-category-label').exists()).toBe(true)
|
||||
})
|
||||
|
||||
await homeWrapper.get('[data-testid="poi-category-escalator"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(homeWrapper.find('[data-testid="poi-result-escalator-1"]').exists()).toBe(true)
|
||||
|
||||
await homeWrapper.get('[data-testid="poi-category-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
await homeWrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
const expandedCategories = homeWrapper.findAll('.home-shortcut-grid .category-item')
|
||||
expect(expandedCategories).toHaveLength(9)
|
||||
expect(expandedCategories.map((item) => item.text())).toEqual([
|
||||
'展览', '影院', '洗手间', '餐厅', '售票处',
|
||||
'母婴室', '服务中心', '电梯', '扶梯'
|
||||
])
|
||||
expect(expandedCategories.slice(0, 5).map((item) => item.text()))
|
||||
.toEqual(homeCategories.slice(0, 5).map((item) => item.text()))
|
||||
homeWrapper.unmount()
|
||||
|
||||
const pageWrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
expect(pageWrapper.findAll('.category-item')).toHaveLength(12)
|
||||
expect(pageWrapper.text()).toContain('影院')
|
||||
expect(pageWrapper.text()).toContain('服务中心')
|
||||
expect(pageWrapper.text()).toContain('文创')
|
||||
expect(pageWrapper.text()).not.toContain('服务设施')
|
||||
pageWrapper.unmount()
|
||||
})
|
||||
|
||||
it('普通 H5 展开时保留内部标题与返回,并可分类和收起', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.get('.home-fullscreen-nav').text()).toContain('点位搜索')
|
||||
expect(wrapper.find('[data-testid="poi-search-cancel"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('.home-category-strip').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('[data-testid="poi-search-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('.home-category-strip').exists()).toBe(true)
|
||||
expect(guideMocks.createInitialPoiSearchState).toHaveBeenCalledWith('floor-1')
|
||||
expect(guideMocks.selectPoiSearchCategory).not.toHaveBeenCalled()
|
||||
expect(guideMocks.searchPoiKeyword).not.toHaveBeenCalled()
|
||||
expect(guideMocks.clearPoiSearch).not.toHaveBeenCalled()
|
||||
expect(guideMocks.changePoiSearchFloor).not.toHaveBeenCalled()
|
||||
expect(renderedResultIds(wrapper)).toEqual(['hall-1', 'space-1'])
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('内嵌微信小程序展开时不渲染内部标题与返回,并可分类和收起', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
it('快捷分类调用 selectPoiSearchCategory,列表 ID 与 results-change visiblePoiIds 完全一致', async () => {
|
||||
const initialState = createSearchState()
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(initialState)
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
@@ -200,61 +308,90 @@ describe('POI 搜索面板实际渲染与状态闭环', () => {
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('.home-fullscreen-nav').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="poi-search-cancel"]').exists()).toBe(false)
|
||||
expect(wrapper.get('.poi-search-panel').element.firstElementChild?.classList.contains('search-box'))
|
||||
.toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('.home-category-strip').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('.home-collapse-handle').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('.home-category-strip').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('展开态快捷入口退出全屏并进入当前楼层的分类结果', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('.poi-search-content').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.find('.poi-search-content').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(true)
|
||||
expect(wrapper.get('[data-testid="poi-home-category-results"]').text())
|
||||
.toContain('当前楼层 1F · 2 个点位')
|
||||
expect(uni.hideKeyboard).toHaveBeenCalled()
|
||||
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||
expect(guideMocks.selectPoiSearchCategory).toHaveBeenCalledWith(initialState, 'restroom', 'floor-1')
|
||||
expect(renderedResultIds(wrapper)).toEqual(['restroom-1'])
|
||||
expect(lastResultState(wrapper)).toMatchObject({
|
||||
active: true,
|
||||
categoryId: 'restroom',
|
||||
floorId: 'floor-1',
|
||||
categoryId: 'restroom',
|
||||
visiblePoiIds: ['restroom-1']
|
||||
})
|
||||
expect(lastResultState(wrapper).visiblePoiIds).toEqual(renderedResultIds(wrapper))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('关键词确认命中分类时仍保留全屏搜索结果', async () => {
|
||||
it('首页快捷分类请求期间清空列表并下发 pending 空地图 ID', async () => {
|
||||
const initialState = createSearchState()
|
||||
const deferredCategory = createDeferred<GuidePoiSearchViewState>()
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(initialState)
|
||||
guideMocks.selectPoiSearchCategory.mockReturnValueOnce(deferredCategory.promise)
|
||||
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(renderedResultIds(wrapper)).toEqual([])
|
||||
expect(lastResultState(wrapper)).toMatchObject({
|
||||
active: true,
|
||||
pending: true,
|
||||
visiblePoiIds: []
|
||||
})
|
||||
|
||||
deferredCategory.resolve(createSearchState({
|
||||
mode: 'category',
|
||||
categoryId: 'restroom',
|
||||
keyword: '洗手间',
|
||||
results: restroomFloor1
|
||||
}))
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(lastResultState(wrapper).visiblePoiIds).toEqual(renderedResultIds(wrapper))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('关键词确认调用 searchPoiKeyword,并把关键词结果作为地图可见 ID', async () => {
|
||||
const initialState = createSearchState()
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(initialState)
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await wrapper.get('.search-input').trigger('confirm', {
|
||||
detail: { value: '恐龙' }
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(guideMocks.searchPoiKeyword).toHaveBeenCalledWith(initialState, '恐龙', 'floor-1')
|
||||
expect(renderedResultIds(wrapper)).toEqual(['dinosaur-1', 'dinosaur-shop-1'])
|
||||
expect(lastResultState(wrapper)).toMatchObject({
|
||||
active: true,
|
||||
keyword: '恐龙',
|
||||
categoryId: '',
|
||||
visiblePoiIds: ['dinosaur-1', 'dinosaur-shop-1']
|
||||
})
|
||||
expect(lastResultState(wrapper).visiblePoiIds).toEqual(renderedResultIds(wrapper))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('输入恰好为快捷分类名称时仍走统一关键词查询', async () => {
|
||||
const initialState = createSearchState()
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(initialState)
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
@@ -270,90 +407,21 @@ describe('POI 搜索面板实际渲染与状态闭环', () => {
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.find('.poi-search-content').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('详情返回专用复位恢复默认快捷入口并清空搜索状态', 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()
|
||||
|
||||
wrapper.get('[data-testid="poi-result-list"]').element.scrollTop = 126
|
||||
await (wrapper.vm as unknown as {
|
||||
returnToCollapsedAfterDetail: () => Promise<void>
|
||||
}).returnToCollapsedAfterDetail()
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(false)
|
||||
expect(wrapper.find('.poi-search-content').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.home-category-chip')).toHaveLength(9)
|
||||
expect(wrapper.findAll('.home-category-chip.active')).toHaveLength(0)
|
||||
expect((wrapper.get('.search-input').element as HTMLInputElement).value).toBe('')
|
||||
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||
active: false,
|
||||
keyword: '',
|
||||
categoryId: '',
|
||||
visiblePoiIds: [],
|
||||
listScrollTop: 0
|
||||
})
|
||||
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()
|
||||
|
||||
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({
|
||||
expect(guideMocks.searchPoiKeyword).toHaveBeenCalledWith(initialState, '洗手间', 'floor-1')
|
||||
expect(guideMocks.selectPoiSearchCategory).not.toHaveBeenCalled()
|
||||
expect(lastResultState(wrapper)).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']
|
||||
keyword: '洗手间',
|
||||
categoryId: ''
|
||||
})
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('首页分类跟随当前地图楼层更新结果与 marker ID,并对无结果给出反馈', async () => {
|
||||
it('首页关键词请求期间清空列表并下发 pending 空地图 ID', async () => {
|
||||
const initialState = createSearchState()
|
||||
const deferredKeyword = createDeferred<GuidePoiSearchViewState>()
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(initialState)
|
||||
guideMocks.searchPoiKeyword.mockReturnValueOnce(deferredKeyword.promise)
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
@@ -363,186 +431,85 @@ describe('POI 搜索面板实际渲染与状态闭环', () => {
|
||||
})
|
||||
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('.search-box').trigger('tap')
|
||||
await wrapper.get('.search-input').trigger('confirm', {
|
||||
detail: { value: '恐龙' }
|
||||
})
|
||||
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({
|
||||
expect(renderedResultIds(wrapper)).toEqual([])
|
||||
expect(lastResultState(wrapper)).toMatchObject({
|
||||
active: true,
|
||||
pending: true,
|
||||
visiblePoiIds: []
|
||||
})
|
||||
|
||||
deferredKeyword.resolve(createSearchState({
|
||||
mode: 'keyword',
|
||||
keyword: '恐龙',
|
||||
results: keywordFloor1
|
||||
}))
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(lastResultState(wrapper).visiblePoiIds).toEqual(renderedResultIds(wrapper))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('独立搜索页切换楼层调用 changePoiSearchFloor 并渲染新楼层当前模式结果', async () => {
|
||||
const initialState = createSearchState()
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(initialState)
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
const categoryState = guideMocks.selectPoiSearchCategory.mock.results[0]?.value
|
||||
const floor2 = wrapper.findAll('.floor-tab').find((item) => item.text() === '2F')
|
||||
expect(floor2).toBeDefined()
|
||||
await floor2!.trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(guideMocks.changePoiSearchFloor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ mode: 'category', categoryId: 'restroom', floorId: 'floor-1' }),
|
||||
'floor-2'
|
||||
)
|
||||
expect(renderedResultIds(wrapper)).toEqual(['restroom-2'])
|
||||
expect(categoryState).toBeDefined()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('清除关键词调用 clearPoiSearch 并恢复默认空间目的地列表', async () => {
|
||||
const initialState = createSearchState()
|
||||
guideMocks.createInitialPoiSearchState.mockResolvedValue(initialState)
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await wrapper.get('.search-input').trigger('confirm', {
|
||||
detail: { value: '恐龙' }
|
||||
})
|
||||
await flushMountedSearch()
|
||||
const clearButton = wrapper.get('.clear-button')
|
||||
await clearButton.trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(guideMocks.clearPoiSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ mode: 'keyword', keyword: '恐龙' }),
|
||||
'floor-1'
|
||||
)
|
||||
expect(renderedResultIds(wrapper)).toEqual(['hall-1', 'space-1'])
|
||||
expect(lastResultState(wrapper)).toMatchObject({
|
||||
active: false,
|
||||
categoryId: '',
|
||||
keyword: '',
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
76
tests/unit/navAssetsAdapter.spec.ts
Normal file
76
tests/unit/navAssetsAdapter.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toMuseumPoi } from '@/data/adapters/navAssetsAdapter'
|
||||
import { resolvePoiCategory } from '@/domain/poiCategories'
|
||||
import type { StaticNavPoiPayload } from '@/data/providers/staticNavAssetsProvider'
|
||||
|
||||
const createStaticPoi = (name: string, iconType = 'elevator'): StaticNavPoiPayload => ({
|
||||
id: `poi-${name}`,
|
||||
name,
|
||||
floorId: 'L1',
|
||||
primaryCategory: 'basic_service_facility',
|
||||
primaryCategoryZh: '基础服务设施',
|
||||
iconType,
|
||||
categories: [{
|
||||
topCategory: 'basic_service_facility',
|
||||
topCategoryZh: '基础服务设施',
|
||||
subcategory: iconType,
|
||||
iconType
|
||||
}],
|
||||
positionGltf: [1, 0, 1]
|
||||
})
|
||||
|
||||
const createTouringPoi = (name: string, iconType = 'exhibition'): StaticNavPoiPayload => ({
|
||||
...createStaticPoi(name, iconType),
|
||||
primaryCategory: 'touring_poi',
|
||||
primaryCategoryZh: '展厅与参观点',
|
||||
categories: [{
|
||||
topCategory: 'touring_poi',
|
||||
topCategoryZh: '展厅与参观点',
|
||||
subcategory: iconType,
|
||||
iconType
|
||||
}]
|
||||
})
|
||||
|
||||
describe('static nav POI adapter', () => {
|
||||
it('repairs legacy static facility icons from visitor-facing names', () => {
|
||||
const stairs = toMuseumPoi(createStaticPoi('西侧楼梯'))
|
||||
const escalator = toMuseumPoi(createStaticPoi('中央扶梯'))
|
||||
const nursing = toMuseumPoi(createStaticPoi('母婴室', 'toilet'))
|
||||
const restroom = toMuseumPoi(createStaticPoi('无障碍洗手间', 'elevator'))
|
||||
|
||||
expect(stairs.primaryCategory.iconType).toBe('stairs')
|
||||
expect(resolvePoiCategory(stairs)).toBeNull()
|
||||
expect(escalator.primaryCategory.iconType).toBe('escalator')
|
||||
expect(resolvePoiCategory(escalator)?.id).toBe('escalator')
|
||||
expect(nursing.primaryCategory.iconType).toBe('mother_baby_room')
|
||||
expect(resolvePoiCategory(nursing)?.id).toBe('nursing-room')
|
||||
expect(restroom.primaryCategory.iconType).toBe('accessible_toilet')
|
||||
expect(resolvePoiCategory(restroom)?.id).toBe('restroom')
|
||||
})
|
||||
|
||||
it('repairs static generic restroom icons for service facilities without misclassifying tea rooms', () => {
|
||||
const serviceDesk = toMuseumPoi(createStaticPoi('服务台', 'restroom'))
|
||||
const locker = toMuseumPoi(createStaticPoi('存包处', 'restroom'))
|
||||
const teaRoom = toMuseumPoi(createStaticPoi('茶水间', 'restroom'))
|
||||
|
||||
expect(resolvePoiCategory(serviceDesk)?.id).toBe('service-center')
|
||||
expect(resolvePoiCategory(locker)?.id).toBe('service-center')
|
||||
expect(resolvePoiCategory(teaRoom)).toBeNull()
|
||||
})
|
||||
|
||||
it('splits generic touring fallback records into their visitor-facing semantic kinds', () => {
|
||||
const cinema = toMuseumPoi(createTouringPoi('巨幕影院'))
|
||||
const ticketOffice = toMuseumPoi(createTouringPoi('展厅售票点'))
|
||||
|
||||
expect(cinema).toMatchObject({
|
||||
kind: 'space',
|
||||
primaryCategory: { id: 'space_theater', iconType: 'theater' }
|
||||
})
|
||||
expect(resolvePoiCategory(cinema)?.id).toBe('cinema')
|
||||
expect(ticketOffice).toMatchObject({
|
||||
kind: 'facility',
|
||||
primaryCategory: { id: 'basic_service_facility', iconType: 'ticket_office' }
|
||||
})
|
||||
expect(resolvePoiCategory(ticketOffice)?.id).toBe('ticket-office')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user