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 getFloorPois: (floorId: string) => Promise resetToViewBaseline: (options: { view: 'floor'; floorId: string; reason: 'floor-reset' }) => Promise showOverview: () => Promise focusTargetPoi: (request: PoiCandidate & { requestId: string }) => Promise 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) })