245 lines
10 KiB
TypeScript
245 lines
10 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
|
|
interface PoiCandidate {
|
|
poiId: string
|
|
floorId: string
|
|
name: string
|
|
primaryCategory: string
|
|
iconType: string
|
|
sourceObjectName?: string
|
|
mergedSourceObjectNames: string[]
|
|
positionGltf: [number, number, number]
|
|
}
|
|
|
|
interface GuideDiagnostics {
|
|
isInitialModelReady: () => boolean
|
|
getReport: () => {
|
|
activeView: 'overview' | 'floor' | 'multi'
|
|
floorId: string
|
|
visibleVisualMarkerWithoutTextCount: number
|
|
}
|
|
getFloors: () => Array<{ floorId: string; label: string }>
|
|
switchFloor: (floorId: string) => Promise<unknown>
|
|
getFloorPois: (floorId: string) => Promise<PoiCandidate[]>
|
|
resetToViewBaseline: (options: { view: 'floor'; floorId: string; reason: 'floor-reset' }) => Promise<unknown>
|
|
showOverview: () => Promise<unknown>
|
|
focusTargetPoi: (request: PoiCandidate & { requestId: string }) => Promise<unknown>
|
|
clearTargetFocus: () => void
|
|
getPoiFocusState: (poiId?: string) => {
|
|
selectedPoiId: string
|
|
modelHighlightRootCount: number
|
|
modelHighlightRootNames: string[]
|
|
modelHighlightMaterials: Array<{
|
|
attached: boolean
|
|
color: string | null
|
|
hasMap: boolean
|
|
}>
|
|
baseAffordanceCount: number
|
|
pulseAffordanceCount: number
|
|
glowAffordanceCount: number
|
|
}
|
|
}
|
|
|
|
const openFirstFloor = async (page: Page) => {
|
|
await page.goto('/?guide-render=3d#/pages/index/index?tab=guide')
|
|
await page.waitForURL(/guide-render=3d.*tab=guide/, { timeout: 30_000 })
|
|
await expect.poll(async () => page.evaluate(() => (
|
|
(window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false
|
|
)), {
|
|
timeout: 180_000
|
|
}).toBe(true)
|
|
|
|
const floorId = await page.evaluate(async () => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
if (!api) throw new Error('ThreeMap diagnostics unavailable')
|
|
const floor = api.getFloors().find((candidate) => candidate.label === '1F')
|
|
if (!floor) throw new Error('1F is unavailable')
|
|
await api.switchFloor(floor.floorId)
|
|
return floor.floorId
|
|
})
|
|
await expect.poll(async () => page.evaluate(() => {
|
|
const report = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__?.getReport()
|
|
return report ? `${report.activeView}:${report.floorId}` : ''
|
|
}), { timeout: 60_000 }).toBe(`floor:${floorId}`)
|
|
return floorId
|
|
}
|
|
|
|
test('1F devices render text labels and highlight only their exact model roots', async ({ page }) => {
|
|
const floorId = await openFirstFloor(page)
|
|
const pois = await page.evaluate(async (requestedFloorId) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
if (!api) throw new Error('ThreeMap diagnostics unavailable')
|
|
return api.getFloorPois(requestedFloorId)
|
|
}, floorId)
|
|
|
|
const cases = [
|
|
{ poiId: '5686', label: '闸机', root: 'L1_闸机_01' },
|
|
{ poiId: '5694', label: '导览屏', root: 'L1_导览屏_02' },
|
|
{ poiId: '5692', label: '安检机', root: 'L1_安检机_02' },
|
|
{ poiId: '5611', label: '电梯', root: 'L1_H_电梯16' },
|
|
{ poiId: '352502409710427180', label: '自助售卖机', root: 'L1_自助售卖机' },
|
|
{ poiId: '352502410238908344', label: '影院服务台', root: 'L1_影院服务台', icon: 'service-center' },
|
|
{ poiId: '5591', label: '售票机', root: 'L1_售票机' },
|
|
{ poiId: 'space-2066935374889922562', label: '茶水间', root: 'L1_茶水间' }
|
|
] as const
|
|
|
|
for (const target of cases) {
|
|
const poi = pois.find((candidate) => candidate.poiId === target.poiId)
|
|
expect(poi, `missing 1F POI ${target.poiId}`).toBeTruthy()
|
|
expect(poi?.sourceObjectName).toBe(target.root)
|
|
|
|
const label = page.locator(`[data-poi-label-kind="ambient"][data-poi-id="${target.poiId}"]`)
|
|
await expect(label).toHaveCount(1, { timeout: 30_000 })
|
|
await expect(label.locator('.three-poi-dom-label__title')).toHaveText(target.label)
|
|
if ('icon' in target) await expect(label).toHaveAttribute('data-poi-icon', target.icon)
|
|
|
|
await page.evaluate(async (candidate) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
if (!api) throw new Error('ThreeMap diagnostics unavailable')
|
|
await api.focusTargetPoi({ ...candidate, requestId: `device-focus-${candidate.poiId}` })
|
|
}, poi!)
|
|
|
|
await expect.poll(() => page.evaluate((poiId) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
const state = api?.getPoiFocusState(poiId)
|
|
return state ? {
|
|
selectedPoiId: state.selectedPoiId,
|
|
roots: state.modelHighlightRootNames,
|
|
rootCount: state.modelHighlightRootCount,
|
|
base: state.baseAffordanceCount,
|
|
pulse: state.pulseAffordanceCount,
|
|
glow: state.glowAffordanceCount
|
|
} : null
|
|
}, target.poiId), { timeout: 30_000 }).toEqual({
|
|
selectedPoiId: target.poiId,
|
|
roots: [target.root],
|
|
rootCount: 1,
|
|
base: 0,
|
|
pulse: 0,
|
|
glow: 0
|
|
})
|
|
|
|
if (target.poiId === '5591') {
|
|
await expect.poll(() => page.evaluate((poiId) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
return api?.getPoiFocusState(poiId).modelHighlightMaterials || []
|
|
}, target.poiId), { timeout: 30_000 }).toEqual([
|
|
expect.objectContaining({
|
|
attached: true,
|
|
color: 'f2e600',
|
|
hasMap: false
|
|
})
|
|
])
|
|
}
|
|
|
|
if (target.poiId === '352502410238908344') {
|
|
await expect.poll(() => page.evaluate(() => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
return api?.getReport().visibleVisualMarkerWithoutTextCount ?? -1
|
|
}), { timeout: 30_000 }).toBe(0)
|
|
}
|
|
}
|
|
|
|
await page.locator('.search-box').click()
|
|
await page.locator('.search-input input').fill('导览屏')
|
|
await page.locator('.search-input input').press('Enter')
|
|
await expect(page.getByTestId('poi-result-5693')).toHaveCount(1, { timeout: 30_000 })
|
|
await expect(page.getByTestId('poi-result-5694')).toHaveCount(1, { timeout: 30_000 })
|
|
|
|
await page.evaluate(() => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
api?.clearTargetFocus()
|
|
})
|
|
})
|
|
|
|
test('clicking a visible POI label selects that label instead of an underlying hit target', async ({ page }) => {
|
|
const floorId = await openFirstFloor(page)
|
|
const target = await page.evaluate(async (requestedFloorId) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
if (!api) throw new Error('ThreeMap diagnostics unavailable')
|
|
return (await api.getFloorPois(requestedFloorId)).find((poi) => poi.name === '文创_01') || null
|
|
}, floorId)
|
|
|
|
expect(target).not.toBeNull()
|
|
const label = page.locator(
|
|
`[data-poi-label-kind="ambient"][data-poi-id="${target?.poiId}"]:visible`
|
|
)
|
|
await expect(label).toHaveCount(1, { timeout: 30_000 })
|
|
|
|
const bounds = await label.boundingBox()
|
|
expect(bounds).not.toBeNull()
|
|
await page.mouse.click(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2)
|
|
|
|
await expect.poll(() => page.evaluate((poiId) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
return poiId ? api?.getPoiFocusState(poiId).selectedPoiId || '' : ''
|
|
}, target!.poiId), { timeout: 30_000 }).toBe(target!.poiId)
|
|
|
|
await expect.poll(() => page.evaluate((poiId) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
return poiId ? api?.getPoiFocusState(poiId).modelHighlightRootNames || [] : []
|
|
}, target!.poiId), { timeout: 30_000 }).toEqual(['L1_文创_01'])
|
|
})
|
|
|
|
test('secondary services use one text label and do not expose old visual markers', async ({ page }) => {
|
|
const floorId = await openFirstFloor(page)
|
|
const targets = await page.evaluate(async (requestedFloorId) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
if (!api) throw new Error('ThreeMap diagnostics unavailable')
|
|
const pois = await api.getFloorPois(requestedFloorId)
|
|
return ['存包处', '轮椅及儿童车租赁'].map((name) => (
|
|
pois.find((poi) => poi.name === name) || null
|
|
))
|
|
}, floorId)
|
|
|
|
for (const target of targets) {
|
|
expect(target, 'missing 1F secondary service POI').not.toBeNull()
|
|
await page.evaluate(async (candidate) => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
if (!api || !candidate) throw new Error('ThreeMap diagnostics or service unavailable')
|
|
await api.focusTargetPoi({ ...candidate, requestId: `service-focus-${candidate.poiId}` })
|
|
}, target)
|
|
|
|
const label = page.locator(
|
|
`[data-poi-label-kind="ambient"][data-poi-id="${target!.poiId}"]:visible`
|
|
)
|
|
await expect(label).toHaveCount(1, { timeout: 30_000 })
|
|
await expect(label.locator('.three-poi-dom-label__title')).toHaveText(target!.name)
|
|
await expect.poll(() => page.evaluate(() => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
return api?.getReport().visibleVisualMarkerWithoutTextCount ?? -1
|
|
}), { timeout: 30_000 }).toBe(0)
|
|
}
|
|
})
|
|
|
|
test('floor switcher exists only in an indoor floor view', async ({ page }) => {
|
|
await page.goto('/')
|
|
await page.waitForURL(/tab=guide/, { timeout: 30_000 })
|
|
await expect.poll(async () => page.evaluate(() => (
|
|
(window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false
|
|
)), { timeout: 180_000 }).toBe(true)
|
|
|
|
await expect.poll(() => page.evaluate(() => {
|
|
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
|
|
.__GUIDE_3D_VISUAL_STABILITY__
|
|
return api?.getReport().activeView || ''
|
|
}), { timeout: 30_000 }).toBe('overview')
|
|
await expect(page.locator('.floor-switcher')).toHaveCount(0)
|
|
})
|