This commit is contained in:
244
tests/e2e/device-label-highlight.spec.ts
Normal file
244
tests/e2e/device-label-highlight.spec.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user