import { expect, test, type Page } from '@playwright/test' interface VisiblePoi { poiId: string floorId: string name: string screen: { x: number; y: number } } interface GuideDiagnostics { isInitialModelReady: () => boolean getReport: () => { activeView: 'overview' | 'floor' | 'multi' floorId: string isCameraTweening: boolean } getFloors: () => Array<{ floorId: string; label: string }> switchFloor: (floorId: string) => Promise getVisiblePoiScreenPositions: () => VisiblePoi[] } interface GuidePerformanceApi { summary: () => { operations: Array<{ kind: string name: string count: number successCount: number failureCount: number p50Ms?: number p95Ms?: number maxMs?: number }> } } const openGuideAtFirstFloor = 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}:${report.isCameraTweening}` : '' }), { timeout: 120_000 }).toBe(`floor:${floorId}:false`) await expect.poll(async () => page.evaluate(() => ( (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }) .__GUIDE_3D_VISUAL_STABILITY__?.getVisiblePoiScreenPositions().length || 0 )), { timeout: 30_000 }).toBeGreaterThan(1) return floorId } const findVisiblePoi = async (page: Page, name: string) => { const deadline = Date.now() + 30_000 let poi: VisiblePoi | null = null while (!poi && Date.now() < deadline) { poi = await page.evaluate((targetName) => { const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }) .__GUIDE_3D_VISUAL_STABILITY__ return api?.getVisiblePoiScreenPositions().find((candidate) => candidate.name === targetName) || null }, name) if (!poi) await page.waitForTimeout(100) } expect(poi, `visible POI ${name} is required for route flow`).not.toBeNull() return poi! } const clickPoiLabel = async (page: Page, poi: VisiblePoi) => { const selector = `[data-poi-label-kind="ambient"][data-poi-id="${poi.poiId}"]` const canvas = page.locator('.three-canvas-wrapper canvas').first() await expect(canvas).toBeVisible({ timeout: 30_000 }) const deadline = Date.now() + 30_000 while (Date.now() < deadline) { const labelCenter = await page.locator(selector).first().evaluate((element) => { const bounds = element.getBoundingClientRect() const style = getComputedStyle(element) if ( style.visibility !== 'visible' || style.opacity === '0' || bounds.width <= 0 || bounds.height <= 0 ) return null return { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2 } }).catch(() => null) const canvasBounds = labelCenter ? await canvas.boundingBox() : null if (labelCenter && canvasBounds) { const position = { x: labelCenter.x - canvasBounds.x, y: labelCenter.y - canvasBounds.y } if ( position.x >= 0 && position.x <= canvasBounds.width && position.y >= 0 && position.y <= canvasBounds.height ) { await canvas.click({ position, timeout: 5_000 }) return } } await page.waitForTimeout(100) } throw new Error(`label for ${poi.name} did not provide a stable map click point`) } test('plans a same-floor route from a real map start and restores the browse floor on close', async ({ page }) => { test.setTimeout(240_000) const floorId = await openGuideAtFirstFloor(page) const destination = await findVisiblePoi(page, '售票机') const start = await findVisiblePoi(page, '影院服务台') expect(destination.poiId).not.toBe(start.poiId) expect(destination.floorId).toBe(floorId) expect(start.floorId).toBe(floorId) await clickPoiLabel(page, destination) await expect(page.locator('.guide-poi-card')).toBeVisible({ timeout: 30_000 }) await page.locator('.poi-action[data-action-id="navigate"]').click() await expect(page.locator('.route-planner-panel')).toBeVisible({ timeout: 30_000 }) await expect(page.locator('.route-start-card')).toBeVisible({ timeout: 30_000 }) await clickPoiLabel(page, start) await expect(page.locator('.route-confirm-mask')).toBeVisible({ timeout: 30_000 }) await page.locator('.route-confirm-action.primary').click() const routeReadyOrError = page.locator('.route-ready-card, .route-state-card.error') await expect(routeReadyOrError).toBeVisible({ timeout: 120_000 }) await expect(page.locator('.route-ready-card'), 'same-floor route should be generated').toBeVisible() await expect(page.locator('.route-ready-summary')).toContainText('米') const performance = await page.evaluate(() => ( (window as Window & { __SGS_GUIDE_PERFORMANCE__?: GuidePerformanceApi }) .__SGS_GUIDE_PERFORMANCE__?.summary() )) const routePlan = performance?.operations.find((operation) => ( operation.kind === 'interaction' && operation.name === 'route-plan' )) expect(routePlan, 'route-plan performance record is required').toMatchObject({ count: 1, successCount: 1, failureCount: 0 }) console.log(`[route-performance] ${JSON.stringify(routePlan)}`) await page.locator('.route-panel-close').click() await expect(page.locator('.route-planner-panel')).toBeHidden({ timeout: 30_000 }) await expect(page.locator('.guide-poi-card')).toBeVisible({ timeout: 30_000 }) 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: 30_000 }).toBe(`floor:${floorId}`) }) test('shows a recoverable route error when the route endpoint fails', async ({ page }) => { test.setTimeout(240_000) await page.route('**/app-api/gis/sdk/routes/plan', async (route) => { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ code: 500, msg: 'simulated route failure' }) }) }) const floorId = await openGuideAtFirstFloor(page) const destination = await findVisiblePoi(page, '售票机') const start = await findVisiblePoi(page, '影院服务台') await clickPoiLabel(page, destination) await expect(page.locator('.guide-poi-card')).toBeVisible({ timeout: 30_000 }) await page.locator('.poi-action[data-action-id="navigate"]').click() await expect(page.locator('.route-planner-panel')).toBeVisible({ timeout: 30_000 }) await clickPoiLabel(page, start) await expect(page.locator('.route-confirm-mask')).toBeVisible({ timeout: 30_000 }) await page.locator('.route-confirm-action.primary').click() await expect(page.locator('.route-state-card.error')).toBeVisible({ timeout: 60_000 }) await expect(page.locator('.route-state-card.error')).toContainText('路线服务暂时不可用,请稍后重试') await expect(page.locator('.route-state-card.error')).not.toContainText('/app-api') await expect(page.locator('.route-state-card.error')).not.toContainText('500') await expect(page.locator('.route-state-card.error')).not.toContainText('body=') await expect(page.locator('.route-ready-card')).toHaveCount(0) await page.locator('.route-panel-close').click() await expect(page.locator('.route-planner-panel')).toBeHidden({ timeout: 30_000 }) 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: 30_000 }).toBe(`floor:${floorId}`) })