import { expect, test, type Page, type Route, type TestInfo } from '@playwright/test' interface ToggleCameraReport { position: { x: number; y: number; z: number } target: { x: number; y: number; z: number } up: { x: number; y: number; z: number } quaternion: { x: number; y: number; z: number; w: number } fov: number zoom: number near: number far: number distance: number } interface ExteriorLabelPosition { label: string x: number y: number } interface ThreeDimensionalSceneState { camera: ToggleCameraReport labels: ExteriorLabelPosition[] } interface TwoDimensionalViewportState { scene: 'overview' | 'floor' floorId: string centerX: number centerZ: number visibleWorldSpan: number referenceVisibleWorldSpan: number visibilityTier: 'tight' | 'balanced' | 'full' revision: number } interface WeakMapDensityState { tier: 'tight' | 'balanced' | 'full' markerCount: number labelCount: number facilityMarkerCount: number overlappingLabels: string[] } interface LiveGlbDiagnosticsReport { camera: ToggleCameraReport actualRendererPath: 'three-d' | 'live-glb-top' | 'webp-fallback' renderLoopRunning: boolean sceneUuid: string | null activeModelUuid: string | null controls: { enableRotate: boolean enablePan: boolean enableZoom: boolean } | null activeView: 'overview' | 'floor' | 'multi' floorId: string isCameraTweening: boolean } interface LiveGlbDiagnostics { isInitialModelReady: () => boolean getReport: () => LiveGlbDiagnosticsReport getFloors: () => Array<{ floorId: string; label: string }> getFloorBaseline: (floorId: string) => ToggleCameraReport | null getAmbientPoiLabelStates: () => Array<{ poiId: string floorId: string text: string visible: boolean }> switchFloor: (floorId: string) => Promise } interface WeakCompositionFloor { floorId: string floorCode: string projection: { screenRight: { x: number; z: number } } } const EXPECTED_EXTERIOR_LABELS = [ '停车场入口', '停车场出口', '停车场', '室外下沉广场', '红花潭路', '文祥路', '大巴区' ].sort() const waitForThreeDimensionalScene = async (page: Page, expectOverviewLabels = true) => { await expect(page.locator('.three-canvas-wrapper canvas')).toBeVisible({ timeout: 120_000 }) await expect.poll(async () => page.evaluate(() => ( (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: { isInitialModelReady: () => boolean } }).__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false )), { timeout: 120_000 }).toBe(true) if (expectOverviewLabels) { await expect(page.locator('.three-poi-dom-label--overview').first()).toBeAttached({ timeout: 60_000 }) } } const getThreeDimensionalSceneState = async (page: Page): Promise => page.evaluate(() => { const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: { getReport: () => { camera: ToggleCameraReport } } }).__GUIDE_3D_VISUAL_STABILITY__ if (!api) throw new Error('Three-dimensional guide diagnostics unavailable') return { camera: api.getReport().camera, labels: [...document.querySelectorAll('.three-poi-dom-label--overview')] .map((element) => { const bounds = element.getBoundingClientRect() return { label: element.querySelector('.three-poi-dom-label__title')?.textContent?.trim() || '', x: bounds.left + bounds.width / 2, y: bounds.bottom } }) } }) const getCameraDelta = (before: ToggleCameraReport, after: ToggleCameraReport) => Math.max( Math.hypot( before.position.x - after.position.x, before.position.y - after.position.y, before.position.z - after.position.z ), Math.hypot( before.target.x - after.target.x, before.target.y - after.target.y, before.target.z - after.target.z ), Math.abs(before.distance - after.distance) ) const getFullCameraDelta = (before: ToggleCameraReport, after: ToggleCameraReport) => Math.max( getCameraDelta(before, after), Math.hypot(before.up.x - after.up.x, before.up.y - after.up.y, before.up.z - after.up.z), Math.hypot( before.quaternion.x - after.quaternion.x, before.quaternion.y - after.quaternion.y, before.quaternion.z - after.quaternion.z, before.quaternion.w - after.quaternion.w ), Math.abs(before.fov - after.fov), Math.abs(before.zoom - after.zoom), Math.abs(before.near - after.near), Math.abs(before.far - after.far) ) const getLiveGlbState = async (page: Page) => page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: LiveGlbDiagnostics __LIVE_GLB_TOP_TEST_CANVAS__?: HTMLCanvasElement }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') const canvas = document.querySelector('.three-canvas-wrapper canvas') if (!canvas) throw new Error('ThreeMap canvas unavailable') const diagnosticsWindow = window as Window & { __LIVE_GLB_TOP_TEST_CANVAS__?: HTMLCanvasElement } diagnosticsWindow.__LIVE_GLB_TOP_TEST_CANVAS__ ||= canvas return { report: diagnostics.getReport(), floors: diagnostics.getFloors(), labels: diagnostics.getAmbientPoiLabelStates(), sameCanvas: diagnosticsWindow.__LIVE_GLB_TOP_TEST_CANVAS__ === canvas } }) const waitForLiveGlbGuide = async (page: Page) => { await page.setViewportSize({ width: 390, height: 844 }) 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__?: LiveGlbDiagnostics }) .__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false )), { timeout: 180_000 }).toBe(true) await expect(page.locator('.three-canvas-wrapper canvas')).toBeVisible({ timeout: 30_000 }) } const switchLiveGlbFloor = async (page: Page, label: '1F' | '3F') => { const floorId = await page.evaluate(async (floorLabel) => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: LiveGlbDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') const floor = diagnostics.getFloors().find((candidate) => candidate.label === floorLabel) if (!floor) throw new Error(`${floorLabel} is unavailable`) await diagnostics.switchFloor(floor.floorId) return floor.floorId }, label) await expect.poll(async () => { const { report } = await getLiveGlbState(page) return `${report.activeView}:${report.floorId}:${report.isCameraTweening}` }, { timeout: 180_000 }).toBe(`floor:${floorId}:false`) return floorId } const expectLabelsOnlyFromFloor = (state: Awaited>, floorId: string) => { expect(state.labels.length).toBeGreaterThan(0) expect(new Set(state.labels.map((label) => label.floorId))).toEqual(new Set([floorId])) expect(state.labels.some((label) => label.visible && Boolean(label.text.trim()))).toBe(true) } const waitForLabelsOnlyFromFloor = async (page: Page, floorId: string) => { await expect.poll(async () => { const state = await getLiveGlbState(page) return ( state.labels.length > 0 && state.labels.every((label) => label.floorId === floorId) && state.labels.some((label) => label.visible && Boolean(label.text.trim())) ) }, { timeout: 60_000 }).toBe(true) return getLiveGlbState(page) } const dragLiveGlbCanvas = async (page: Page, deltaX = 48, deltaY = 32) => { const canvas = page.locator('.three-canvas-wrapper canvas') const bounds = await canvas.boundingBox() expect(bounds, 'live GLB canvas must be available for panning').not.toBeNull() const x = bounds!.x + bounds!.width * 0.45 const y = bounds!.y + bounds!.height * 0.45 await page.mouse.move(x, y) await page.mouse.down() await page.mouse.move(x + deltaX, y + deltaY, { steps: 8 }) await page.mouse.up() } const getLiveTopScreenRight = (camera: ToggleCameraReport) => { const { x, y, z, w } = camera.quaternion const right = { x: 1 - 2 * (y * y + z * z), z: 2 * (x * z - w * y) } const length = Math.hypot(right.x, right.z) return { x: right.x / length, z: right.z / length } } const getWeakCompositionMetrics = (options: { live: LiveGlbDiagnosticsReport weak: TwoDimensionalViewportState floor: WeakCompositionFloor width: number height: number liveAspect: number }) => { const liveRight = getLiveTopScreenRight(options.live.camera) const weakRight = options.floor.projection.screenRight const dot = Math.max(-1, Math.min(1, liveRight.x * weakRight.x + liveRight.z * weakRight.z)) const directionDegrees = Math.acos(dot) * 180 / Math.PI const deltaX = options.live.camera.target.x - options.weak.centerX const deltaZ = options.live.camera.target.z - options.weak.centerZ const weakDown = { x: -weakRight.z, z: weakRight.x } const centerOffsetPixels = Math.hypot( (deltaX * weakRight.x + deltaZ * weakRight.z) / options.weak.visibleWorldSpan * options.width, (deltaX * weakDown.x + deltaZ * weakDown.z) / options.weak.visibleWorldSpan * options.width ) const liveVisibleWorldSpan = 2 * options.live.camera.distance * Math.tan(options.live.camera.fov * Math.PI / 360) * options.liveAspect return { centerOffsetPixels, centerOffsetRatio: centerOffsetPixels / Math.min(options.width, options.height), directionDegrees, representativeScaleRatio: liveVisibleWorldSpan / options.weak.visibleWorldSpan } } const expectSameThreeDimensionalLabelLayout = ( expected: ExteriorLabelPosition[], actual: ExteriorLabelPosition[] ) => { const expectedByLabel = new Map(expected.map((label) => [label.label, label])) const actualByLabel = new Map(actual.map((label) => [label.label, label])) expect([...actualByLabel.keys()].sort()).toEqual([...expectedByLabel.keys()].sort()) for (const [label, expectedPosition] of expectedByLabel) { const actualPosition = actualByLabel.get(label) expect(actualPosition, `${label} must exist in both render modes`).toBeDefined() expect(Math.hypot( expectedPosition.x - actualPosition!.x, expectedPosition.y - actualPosition!.y ), `${label} anchor moved after restoring 3D`).toBeLessThan(2.5) } } const getTwoDimensionalViewport = async (page: Page): Promise => ( page.getByTestId('weak-network-map').evaluate((element) => { const value = (name: string) => Number(element.getAttribute(name)) return { scene: element.getAttribute('data-viewport-scene') as 'overview' | 'floor', floorId: element.getAttribute('data-viewport-floor-id') || '', centerX: value('data-viewport-center-x'), centerZ: value('data-viewport-center-z'), visibleWorldSpan: value('data-viewport-visible-world-span'), referenceVisibleWorldSpan: value('data-reference-visible-world-span'), visibilityTier: element.getAttribute('data-visibility-tier') as 'tight' | 'balanced' | 'full', revision: value('data-viewport-revision') } }) ) const expectValidTwoDimensionalViewport = (viewport: TwoDimensionalViewportState) => { expect(['overview', 'floor']).toContain(viewport.scene) expect(Number.isFinite(viewport.centerX)).toBe(true) expect(Number.isFinite(viewport.centerZ)).toBe(true) expect(Number.isFinite(viewport.visibleWorldSpan)).toBe(true) expect(viewport.visibleWorldSpan).toBeGreaterThan(0) expect(viewport.referenceVisibleWorldSpan).toBeGreaterThan(0) expect(['tight', 'balanced', 'full']).toContain(viewport.visibilityTier) } const getWeakMapDensityState = async (page: Page): Promise => page.evaluate(() => { const map = document.querySelector('[data-testid="weak-network-map"]') if (!map) throw new Error('Weak network map is unavailable') const markers = [...document.querySelectorAll('.weak-network-marker')] const labels = [...document.querySelectorAll('.weak-network-marker-label')] const facilityIcons = new Set(['restroom', 'elevator', 'escalator', 'stairs']) const overlappingLabels: string[] = [] for (let leftIndex = 0; leftIndex < labels.length; leftIndex += 1) { const left = labels[leftIndex].getBoundingClientRect() for (let rightIndex = leftIndex + 1; rightIndex < labels.length; rightIndex += 1) { const right = labels[rightIndex].getBoundingClientRect() if ( left.left < right.right - 1 && left.right > right.left + 1 && left.top < right.bottom - 1 && left.bottom > right.top + 1 ) { overlappingLabels.push( `${labels[leftIndex].textContent?.trim() || leftIndex}/${labels[rightIndex].textContent?.trim() || rightIndex}` ) } } } return { tier: map.dataset.visibilityTier as 'tight' | 'balanced' | 'full', markerCount: markers.length, labelCount: labels.length, facilityMarkerCount: markers.filter((marker) => facilityIcons.has(marker.dataset.poiIcon || '')).length, overlappingLabels } }) const expectMarkersInsideBasemap = async (page: Page) => { const result = await page.evaluate(() => { const image = document.querySelector('[data-testid="weak-network-basemap"]') if (!image) return { outside: ['missing-basemap'], markerCount: 0 } const imageBounds = image.getBoundingClientRect() const markers = [...document.querySelectorAll('.weak-network-marker')] const outside = markers .filter((marker) => { const bounds = marker.getBoundingClientRect() const x = bounds.left + bounds.width / 2 const y = bounds.top + bounds.height / 2 return x < imageBounds.left - 1 || x > imageBounds.right + 1 || y < imageBounds.top - 1 || y > imageBounds.bottom + 1 }) .map((marker) => marker.dataset.label || marker.dataset.poiId || 'unknown') return { outside, markerCount: markers.length } }) expect(result.markerCount).toBeGreaterThan(0) expect(result.outside).toEqual([]) } const screenshot = async (page: Page, testInfo: TestInfo, name: string) => { await page.screenshot({ path: testInfo.outputPath(`${name}.png`) }) } const clearGuideModelCacheForTest = async (page: Page) => { const clearPath = `/__guide-cache-clear-${Date.now()}` await page.route(`**${clearPath}`, async (route) => { await route.fulfill({ status: 200, contentType: 'text/html', body: 'clear guide cache' }) }) await page.goto(clearPath) await page.evaluate(async () => { localStorage.removeItem('sgs.guide.model-cache-index.v1') if ('caches' in window) { await caches.delete('sgs-guide-model-v1') } }) await page.unroute(`**${clearPath}`) } const clickGuideZoom = async (page: Page, direction: 'in' | 'out') => { await page.getByTestId(direction === 'in' ? 'guide-zoom-in' : 'guide-zoom-out').click() await page.waitForTimeout(130) return getTwoDimensionalViewport(page) } const reachTwoDimensionalBoundary = async ( page: Page, direction: 'in' | 'out', expectedScene: 'overview' | 'floor' ) => { const start = await getTwoDimensionalViewport(page) let changedSteps = 0 for (let attempt = 0; attempt < 10; attempt += 1) { const before = await getTwoDimensionalViewport(page) const after = await clickGuideZoom(page, direction) expect(after.scene).toBe(expectedScene) if (Math.abs(after.visibleWorldSpan - before.visibleWorldSpan) < 1e-9) { return { start, boundary: after, changedSteps } } changedSteps += 1 } throw new Error(`2D ${expectedScene} viewport did not reach its ${direction} boundary`) } const enterDefaultFloorFromOverview = async (page: Page) => { const overview = await getTwoDimensionalViewport(page) expect(overview.scene).toBe('overview') expect(overview.visibleWorldSpan / 1.25).toBeLessThanOrEqual( overview.referenceVisibleWorldSpan * 0.85 ) await clickGuideZoom(page, 'in') await expect.poll(async () => (await getTwoDimensionalViewport(page)).scene, { timeout: 60_000 }).toBe('floor') await expect(page.locator('.floor-switcher')).toBeVisible({ timeout: 30_000 }) return getTwoDimensionalViewport(page) } const dragTwoDimensionalMap = async (page: Page, deltaX = 42, deltaY = 28) => { const surfaceBox = await page.getByTestId('weak-network-surface').boundingBox() expect(surfaceBox).not.toBeNull() if (!surfaceBox) return getTwoDimensionalViewport(page) const startX = surfaceBox.x + surfaceBox.width * 0.25 const startY = surfaceBox.y + surfaceBox.height * 0.35 await page.mouse.move(startX, startY) await page.mouse.down() await page.mouse.move( startX + deltaX, startY + deltaY, { steps: 3 } ) await page.mouse.up() await page.waitForTimeout(100) return getTwoDimensionalViewport(page) } const selectFloor = async (page: Page, label: '1F' | '2F' | '3F') => { const floor = page.locator('.floor-item').filter({ hasText: label }).first() await expect(floor).toBeVisible({ timeout: 30_000 }) await floor.click() await expect(floor).toHaveClass(/active/, { timeout: 60_000 }) await expect(page.getByTestId('weak-network-basemap').locator('img')).toHaveAttribute( 'src', new RegExp(`/${label.replace('F', '') === '1' ? 'L1' : `L${label.replace('F', '')}`}\\.[^/]+\\.ortho-yaw54-v2\\.compact-floor-center-v1\\.webp$`, 'i'), { timeout: 60_000 } ) const viewport = await getTwoDimensionalViewport(page) expect(viewport.scene).toBe('floor') expect(viewport.floorId).not.toBe('') const markerFloorIds = await page.locator('.weak-network-marker').evaluateAll((markers) => ( markers.map((marker) => marker.getAttribute('data-floor-id')) )) expect(markerFloorIds.length).toBeGreaterThan(0) expect(new Set(markerFloorIds)).toEqual(new Set([viewport.floorId])) await expectMarkersInsideBasemap(page) return viewport } const captureFloorScreenshots = async ( page: Page, testInfo: TestInfo, viewport: { width: number; height: number }, prefix: string ) => { await page.setViewportSize(viewport) await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await enterDefaultFloorFromOverview(page) for (const floor of ['1F', '2F', '3F'] as const) { await selectFloor(page, floor) await screenshot(page, testInfo, `${prefix}-${floor.toLowerCase()}`) } } const verifyExteriorRenderModeToggle = async ( page: Page, testInfo: TestInfo, viewport: { width: number; height: number }, artifactPrefix: string ) => { await page.setViewportSize(viewport) await page.goto('/') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('3D') await waitForThreeDimensionalScene(page) const before = await getThreeDimensionalSceneState(page) expect(before.labels.map((label) => label.label).sort()).toEqual(EXPECTED_EXTERIOR_LABELS) await screenshot(page, testInfo, `${artifactPrefix}-3d-before`) await page.getByTestId('indoor-render-mode-toggle').click() await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('2D') await expect(page.getByTestId('weak-network-map')).toHaveCount(0) await expect(page.locator('.three-canvas-wrapper canvas')).toBeVisible() await expect(page.locator('.three-canvas-wrapper')).toHaveAttribute('data-actual-renderer', 'live-glb-top') await expect(page.locator('.three-canvas-wrapper')).toHaveAttribute('data-render-loop-state', 'active-3d') const topView = await getThreeDimensionalSceneState(page) expect(topView.labels.map((label) => label.label).sort()).toEqual(EXPECTED_EXTERIOR_LABELS) await screenshot(page, testInfo, `${artifactPrefix}-2d`) await page.waitForTimeout(120) await page.getByTestId('indoor-render-mode-toggle').click() await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('3D') await waitForThreeDimensionalScene(page) await expect(page.getByTestId('weak-network-map')).toHaveCount(0) await expect(page.locator('.three-poi-dom-label--overview:visible').first()).toBeVisible({ timeout: 60_000 }) const after = await getThreeDimensionalSceneState(page) expect(getCameraDelta(before.camera, after.camera)).toBeLessThan(1e-6) expectSameThreeDimensionalLabelLayout(before.labels, after.labels) await screenshot(page, testInfo, `${artifactPrefix}-3d-after`) } test('opens the developer two-dimensional basemap preview directly', async ({ page }) => { await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-map')).toBeVisible({ timeout: 60_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 30_000 }) await expect(page.locator('.app-launch-overlay')).toHaveCount(0, { timeout: 30_000 }) }) test('chooses the two-dimensional guide before requesting GLB assets on a constrained network', async ({ page }) => { await page.addInitScript(() => { Object.defineProperty(Navigator.prototype, 'connection', { configurable: true, value: { effectiveType: '4g', saveData: false, downlink: 0.8, rtt: 650 } }) }) let glbRequested = false page.on('request', (request) => { if (/\.glb(?:$|\?)/i.test(request.url())) glbRequested = true }) await page.goto('/') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('2D') await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveCount(1) expect(glbRequested).toBe(false) }) test('preserves exterior labels and camera context across a normal 3D/2D toggle', async ({ page }, testInfo) => { await verifyExteriorRenderModeToggle( page, testInfo, { width: 430, height: 932 }, 'exterior-430x932' ) }) test('live GLB indoor L1 top view preserves renderer identity and supports zoom and pan', async ({ page }) => { await waitForLiveGlbGuide(page) const l1FloorId = await switchLiveGlbFloor(page, '1F') const threeDimensional = await getLiveGlbState(page) expect(threeDimensional.report.actualRendererPath).toBe('three-d') expect(threeDimensional.report.sceneUuid).not.toBeNull() expect(threeDimensional.report.activeModelUuid).not.toBeNull() await page.getByTestId('indoor-render-mode-toggle').click() await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('2D') await expect.poll(async () => (await getLiveGlbState(page)).report.actualRendererPath, { timeout: 30_000 }).toBe('live-glb-top') const topView = await getLiveGlbState(page) await expect(page.getByTestId('weak-network-map')).toHaveCount(0) expect(topView.sameCanvas).toBe(true) expect(topView.report.sceneUuid).toBe(threeDimensional.report.sceneUuid) expect(topView.report.activeModelUuid).toBe(threeDimensional.report.activeModelUuid) expect(topView.report.renderLoopRunning).toBe(true) expect(topView.report.controls).toMatchObject({ enableRotate: false, enablePan: true, enableZoom: true }) expect(topView.report.camera.target.x).toBeCloseTo(threeDimensional.report.camera.target.x, 6) expect(topView.report.camera.target.z).toBeCloseTo(threeDimensional.report.camera.target.z, 6) expect(topView.report.camera.distance).toBeCloseTo(threeDimensional.report.camera.distance, 6) expect(Math.hypot( topView.report.camera.position.x - topView.report.camera.target.x, topView.report.camera.position.z - topView.report.camera.target.z )).toBeLessThan(0.01) expect(topView.report.camera.position.y).toBeGreaterThan(topView.report.camera.target.y) expectLabelsOnlyFromFloor(topView, l1FloorId) await page.getByTestId('guide-zoom-in').click() await expect.poll(async () => (await getLiveGlbState(page)).report.camera.distance, { timeout: 15_000 }).toBeLessThan(topView.report.camera.distance - 0.1) const zoomed = await getLiveGlbState(page) await dragLiveGlbCanvas(page) await expect.poll(async () => { const current = (await getLiveGlbState(page)).report.camera.target return Math.hypot( current.x - zoomed.report.camera.target.x, current.z - zoomed.report.camera.target.z ) }, { timeout: 15_000 }).toBeGreaterThan(0.1) const panned = await getLiveGlbState(page) expect(panned.report.actualRendererPath).toBe('live-glb-top') expect(panned.sameCanvas).toBe(true) expectLabelsOnlyFromFloor(panned, l1FloorId) }) test('live GLB indoor L1 to L3 keeps top view and restores the L3 baseline', async ({ page }, testInfo) => { await waitForLiveGlbGuide(page) await switchLiveGlbFloor(page, '1F') const l1ThreeDimensional = await getLiveGlbState(page) await page.getByTestId('indoor-render-mode-toggle').click() await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('2D') await expect.poll(async () => (await getLiveGlbState(page)).report.actualRendererPath, { timeout: 30_000 }).toBe('live-glb-top') const l1Top = await getLiveGlbState(page) expect(l1Top.sameCanvas).toBe(true) expect(l1Top.report.sceneUuid).toBe(l1ThreeDimensional.report.sceneUuid) await screenshot(page, testInfo, 'live-glb-indoor-l1-top-390x844') const l3FloorId = await switchLiveGlbFloor(page, '3F') await expect.poll(async () => (await getLiveGlbState(page)).report.actualRendererPath, { timeout: 30_000 }).toBe('live-glb-top') const l3Top = await getLiveGlbState(page) await expect(page.getByTestId('weak-network-map')).toHaveCount(0) expect(l3Top.sameCanvas).toBe(true) expect(l3Top.report.sceneUuid).toBe(l1ThreeDimensional.report.sceneUuid) expect(l3Top.report.activeModelUuid).not.toBe(l1ThreeDimensional.report.activeModelUuid) expect(l3Top.report.floorId).toBe(l3FloorId) expect(l3Top.report.controls).toMatchObject({ enableRotate: false, enablePan: true, enableZoom: true }) expect(Math.hypot( l3Top.report.camera.position.x - l3Top.report.camera.target.x, l3Top.report.camera.position.z - l3Top.report.camera.target.z )).toBeLessThan(0.01) const l3TopWithLabels = await waitForLabelsOnlyFromFloor(page, l3FloorId) expectLabelsOnlyFromFloor(l3TopWithLabels, l3FloorId) await screenshot(page, testInfo, 'live-glb-indoor-l3-top-390x844') const l3Baseline = await page.evaluate((floorId) => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: LiveGlbDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') return diagnostics.getFloorBaseline(floorId) }, l3FloorId) expect(l3Baseline, 'L3 baseline must exist before restoring 3D').not.toBeNull() await page.getByTestId('indoor-render-mode-toggle').click() await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('3D') await expect.poll(async () => { const { report } = await getLiveGlbState(page) return `${report.actualRendererPath}:${report.floorId}:${report.isCameraTweening}` }, { timeout: 30_000 }).toBe(`three-d:${l3FloorId}:false`) const restored = await getLiveGlbState(page) expect(restored.sameCanvas).toBe(true) expect(restored.report.sceneUuid).toBe(l1ThreeDimensional.report.sceneUuid) expect(restored.report.floorId).toBe(l3FloorId) expect(getFullCameraDelta(l3Baseline!, restored.report.camera)).toBeLessThan(1e-6) expectLabelsOnlyFromFloor(restored, l3FloorId) }) test('weak L1 and L3 composition matches the live GLB top view at 390x844', async ({ page }, testInfo) => { await waitForLiveGlbGuide(page) const l1FloorId = await switchLiveGlbFloor(page, '1F') await page.getByTestId('indoor-render-mode-toggle').click() await expect.poll(async () => (await getLiveGlbState(page)).report.actualRendererPath, { timeout: 30_000 }).toBe('live-glb-top') const l1Live = await getLiveGlbState(page) const liveCanvas = await page.locator('.three-canvas-wrapper canvas').boundingBox() expect(liveCanvas).not.toBeNull() await screenshot(page, testInfo, 'composition-live-l1-390x844') const l3FloorId = await switchLiveGlbFloor(page, '3F') await expect.poll(async () => (await getLiveGlbState(page)).report.actualRendererPath, { timeout: 30_000 }).toBe('live-glb-top') const l3Live = await getLiveGlbState(page) await screenshot(page, testInfo, 'composition-live-l3-390x844') await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await enterDefaultFloorFromOverview(page) const l1Weak = await selectFloor(page, '1F') const weakSurface = await page.getByTestId('weak-network-surface').boundingBox() expect(weakSurface).not.toBeNull() await screenshot(page, testInfo, 'composition-weak-l1-390x844') const manifest = await page.evaluate(async () => { const response = await fetch('/static/guide-floor-basemaps/manifest.json', { cache: 'no-cache' }) if (!response.ok) throw new Error(`Basemap manifest failed: ${response.status}`) return response.json() as Promise<{ floors: WeakCompositionFloor[] }> }) const l1Manifest = manifest.floors.find((floor) => floor.floorId === l1FloorId) expect(l1Manifest, 'L1 weak basemap entry must exist').toBeDefined() const l1Metrics = getWeakCompositionMetrics({ live: l1Live.report, weak: l1Weak, floor: l1Manifest!, width: weakSurface!.width, height: weakSurface!.height, liveAspect: liveCanvas!.width / liveCanvas!.height }) const l3Weak = await selectFloor(page, '3F') await screenshot(page, testInfo, 'composition-weak-l3-390x844') const l3Manifest = manifest.floors.find((floor) => floor.floorId === l3FloorId) expect(l3Manifest, 'L3 weak basemap entry must exist').toBeDefined() const l3Metrics = getWeakCompositionMetrics({ live: l3Live.report, weak: l3Weak, floor: l3Manifest!, width: weakSurface!.width, height: weakSurface!.height, liveAspect: liveCanvas!.width / liveCanvas!.height }) await testInfo.attach('weak-live-composition-metrics.json', { body: Buffer.from(JSON.stringify({ L1: l1Metrics, L3: l3Metrics }, null, 2)), contentType: 'application/json' }) console.info(`weak/live composition metrics ${JSON.stringify({ L1: l1Metrics, L3: l3Metrics })}`) for (const [floor, metrics] of [['L1', l1Metrics], ['L3', l3Metrics]] as const) { expect(metrics.centerOffsetRatio, `${floor} center deviation must be under 5% of the short side`) .toBeLessThan(0.05) expect(metrics.directionDegrees, `${floor} direction delta must be under 8 degrees`) .toBeLessThan(8) expect(metrics.representativeScaleRatio, `${floor} representative scale must stay comparable`) .toBeGreaterThanOrEqual(0.8) expect(metrics.representativeScaleRatio, `${floor} representative scale must stay comparable`) .toBeLessThanOrEqual(1.2) } }) test('keeps the final 3D renderer and camera after rapid render-mode changes', async ({ page }) => { await page.goto('/') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await waitForThreeDimensionalScene(page) const before = await getThreeDimensionalSceneState(page) const toggle = page.getByTestId('indoor-render-mode-toggle') for (const expectedMode of ['2D', '3D', '2D', '3D']) { await toggle.click() await expect(toggle).toHaveText(expectedMode) await page.waitForTimeout(110) } await waitForThreeDimensionalScene(page) await expect(page.getByTestId('weak-network-map')).toHaveCount(0) await expect(page.locator('.three-canvas-wrapper')).toHaveAttribute('data-render-loop-state', 'active-3d') const after = await getThreeDimensionalSceneState(page) expect(getCameraDelta(before.camera, after.camera)).toBeLessThan(1e-6) }) test('keeps the exterior projection aligned on a compact mobile viewport', async ({ page }, testInfo) => { await verifyExteriorRenderModeToggle( page, testInfo, { width: 390, height: 844 }, 'exterior-390x844' ) }) test('supports semantic 2D zoom, drag, floor switching, and a shared 3D target', async ({ page }) => { const abortGlbRequest = async (route: Route) => { await route.abort('failed') } await page.route(/\.glb(?:$|\?)/i, abortGlbRequest) await page.goto('/?guide-render=2d#/pages/index/index?tab=guide') await page.waitForURL(/guide-render=2d.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-map')).toBeVisible({ timeout: 60_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 30_000 }) await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('2D') await expect(page.locator('.floor-switcher')).toHaveCount(0) await enterDefaultFloorFromOverview(page) await selectFloor(page, '2F') const beforeButtonZoom = await getTwoDimensionalViewport(page) const afterButtonZoom = await clickGuideZoom(page, 'in') expect(afterButtonZoom.visibleWorldSpan).toBeLessThan(beforeButtonZoom.visibleWorldSpan) const surface = page.getByTestId('weak-network-surface') const surfaceBox = await surface.boundingBox() expect(surfaceBox).not.toBeNull() if (surfaceBox) { await surface.hover({ position: { x: surfaceBox.width / 2, y: surfaceBox.height / 2 } }) await page.mouse.wheel(0, -480) } await expect.poll(async () => (await getTwoDimensionalViewport(page)).visibleWorldSpan) .toBeLessThan(afterButtonZoom.visibleWorldSpan) const beforeDrag = await getTwoDimensionalViewport(page) const afterDrag = await dragTwoDimensionalMap(page) expect(Math.hypot( afterDrag.centerX - beforeDrag.centerX, afterDrag.centerZ - beforeDrag.centerZ )).toBeGreaterThan(0.1) await expectMarkersInsideBasemap(page) await page.unroute(/\.glb(?:$|\?)/i, abortGlbRequest) await page.getByTestId('indoor-render-mode-toggle').click() await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('3D') await waitForThreeDimensionalScene(page, false) await expect(page.getByTestId('weak-network-map')).toHaveCount(0) const threeDimensional = await getThreeDimensionalSceneState(page) expect(Math.hypot( threeDimensional.camera.target.x - afterDrag.centerX, threeDimensional.camera.target.z - afterDrag.centerZ )).toBeLessThan(2) }) test('uses tight, balanced, and full WebP label density at 390x844', async ({ page }, testInfo) => { await page.setViewportSize({ width: 390, height: 844 }) await page.goto('/?guide-render=2d#/pages/index/index?tab=guide') await page.waitForURL(/guide-render=2d.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await enterDefaultFloorFromOverview(page) const tight = await getWeakMapDensityState(page) expect(tight.tier).toBe('tight') expect(tight.facilityMarkerCount).toBe(0) expect(tight.overlappingLabels).toEqual([]) await screenshot(page, testInfo, 'webp-label-lod-tight-390x844') await clickGuideZoom(page, 'in') await clickGuideZoom(page, 'in') const balanced = await getWeakMapDensityState(page) expect(balanced.tier).toBe('balanced') expect(balanced.markerCount).toBeGreaterThan(tight.markerCount) expect(balanced.labelCount).toBeGreaterThan(tight.labelCount) expect(balanced.facilityMarkerCount).toBeGreaterThan(0) expect(balanced.overlappingLabels).toEqual([]) await screenshot(page, testInfo, 'webp-label-lod-balanced-390x844') await clickGuideZoom(page, 'in') await clickGuideZoom(page, 'in') const full = await getWeakMapDensityState(page) expect(full.tier).toBe('full') expect(full.markerCount).toBeGreaterThan(balanced.markerCount) expect(full.labelCount).toBeGreaterThan(balanced.labelCount) expect(full.facilityMarkerCount).toBeGreaterThanOrEqual(balanced.facilityMarkerCount) expect(full.overlappingLabels).toEqual([]) await screenshot(page, testInfo, 'webp-label-lod-full-390x844') }) test('renders a same-floor route in 2D and keeps route navigation on its floor', async ({ page }) => { test.setTimeout(240_000) await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await enterDefaultFloorFromOverview(page) const floorViewport = await selectFloor(page, '1F') const destination = page.locator('.weak-network-marker[data-label="售票机"]').first() const start = page.locator('.weak-network-marker[data-label="影院服务台"]').first() await expect(destination).toBeVisible({ timeout: 30_000 }) await expect(start).toBeVisible({ timeout: 30_000 }) await destination.dispatchEvent('click') 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-start-card')).toBeVisible({ timeout: 30_000 }) await start.dispatchEvent('click') await expect(page.locator('.route-confirm-mask')).toBeVisible({ timeout: 30_000 }) await page.locator('.route-confirm-action.primary').click() await expect(page.locator('.route-ready-card'), '2D same-floor route should be generated') .toBeVisible({ timeout: 120_000 }) await expect(page.locator('.route-ready-summary')).toContainText('米') await expect(page.locator('.weak-network-route-line').first()).toBeVisible({ timeout: 30_000 }) const routeViewport = await getTwoDimensionalViewport(page) expect(routeViewport.scene).toBe('floor') expect(routeViewport.floorId).toBe(floorViewport.floorId) expect(new Set(await page.locator('.weak-network-marker').evaluateAll((markers) => ( markers.map((marker) => marker.getAttribute('data-floor-id')) )))).toEqual(new Set([floorViewport.floorId])) await page.locator('.route-primary-action').click() await expect(page.locator('.route-sim-step-bar')).toBeVisible({ timeout: 30_000 }) await reachTwoDimensionalBoundary(page, 'out', 'floor') await clickGuideZoom(page, 'out') await clickGuideZoom(page, 'out') expect((await getTwoDimensionalViewport(page)).scene).toBe('floor') }) test('renders the real L1 exterior L1 route on the EXTERIOR_L1 WebP without fake floor transfers', async ({ page }, testInfo) => { test.setTimeout(300_000) const l1FloorId = '2065808921272119298' const exteriorFloorId = '2065808919904776194' const realRouteResponse = await page.request.post( 'http://127.0.0.1:48080/app-api/gis/sdk/routes/plan', { data: { startFloorId: l1FloorId, startX: -59.617386, startY: 16.816814, endFloorId: l1FloorId, endX: 61.8, endY: 45.3, wheelchair: 0, verticalTransferPolicy: 'PREFER_ELEVATOR_ESCALATOR' } } ) expect(realRouteResponse.ok()).toBe(true) const routeEnvelope = await realRouteResponse.json() as { code: number data: { distance: number segments: Array<{ floorId: string transferType: string pathPoints?: unknown[] | null }> } } expect(routeEnvelope.code).toBe(0) expect(routeEnvelope.data.distance).toBeCloseTo(177.9, 1) expect(routeEnvelope.data.segments.filter((segment) => segment.pathPoints?.length).map((segment) => segment.floorId)) .toEqual([l1FloorId, exteriorFloorId, l1FloorId]) expect(routeEnvelope.data.segments.filter((segment) => segment.transferType === 'ENTRANCE_EXIT')) .toHaveLength(2) let uiRouteRequestCount = 0 await page.route('**/app-api/gis/sdk/routes/plan', async (route) => { uiRouteRequestCount += 1 await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(routeEnvelope) }) }) await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await enterDefaultFloorFromOverview(page) await selectFloor(page, '1F') const destination = page.locator('.weak-network-marker[data-label="文创盖章打卡"]').first() const start = page.locator('.weak-network-marker[data-label="展厅售票点"]').first() await expect(destination).toBeVisible({ timeout: 30_000 }) await expect(start).toBeVisible({ timeout: 30_000 }) await destination.dispatchEvent('click') 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-start-card')).toBeVisible({ timeout: 30_000 }) await start.dispatchEvent('click') await expect(page.locator('.route-confirm-mask')).toBeVisible({ timeout: 30_000 }) await page.locator('.route-confirm-action.primary').click() await expect(page.locator('.route-ready-card')).toBeVisible({ timeout: 120_000 }) expect(uiRouteRequestCount).toBe(1) const weakMap = page.getByTestId('weak-network-map') await expect(weakMap).toHaveAttribute('data-active-basemap-floor-code', 'EXTERIOR_L1') await expect(weakMap).toHaveAttribute('data-active-basemap-role', 'SAME_GROUND_COMPOSITE') await expect(weakMap).toHaveAttribute('data-viewport-scene', 'floor') await expect(weakMap).toHaveAttribute('data-viewport-floor-id', 'EXTERIOR_L1') await expect(weakMap).toHaveAttribute( 'data-active-basemap-coverage', `${exteriorFloorId},${l1FloorId}` ) await expect(page.getByTestId('weak-network-basemap').locator('img')).toHaveAttribute( 'src', /\/EXTERIOR_L1\.[^/]+\.ortho-yaw54-v2\.compact-floor-center-v1\.webp$/i ) await expect(page.locator('.weak-network-route-line')).toHaveCount(22) await expect(page.getByTestId('weak-network-floor')).toHaveCount(0) const geometry = await page.evaluate(() => { const surface = document.querySelector('[data-testid="weak-network-surface"]') const image = document.querySelector('[data-testid="weak-network-basemap"]') if (!surface || !image) throw new Error('Composite WebP surface is unavailable') const lines = [...document.querySelectorAll('.weak-network-route-line')] const points = lines.flatMap((line) => [ { x: Number(line.dataset.routeStartX), y: Number(line.dataset.routeStartY), floors: line.dataset.routeFloorIds || '' }, { x: Number(line.dataset.routeEndX), y: Number(line.dataset.routeEndY), floors: line.dataset.routeFloorIds || '' } ]) const xs = points.map((point) => point.x) const ys = points.map((point) => point.y) return { width: surface.clientWidth, height: surface.clientHeight, imageBounds: image.getBoundingClientRect().toJSON(), points, routeBounds: { width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) } } }) expect(geometry.width).toBeGreaterThan(0) expect(geometry.height).toBeGreaterThan(0) expect(geometry.routeBounds.width).toBeGreaterThan(40) expect(geometry.routeBounds.height).toBeGreaterThan(80) expect(new Set(geometry.points.map((point) => point.floors))) .toEqual(new Set([`${l1FloorId},${exteriorFloorId}`])) for (const point of geometry.points) { expect(Number.isFinite(point.x)).toBe(true) expect(Number.isFinite(point.y)).toBe(true) expect(point.x).toBeGreaterThanOrEqual(0) expect(point.x).toBeLessThanOrEqual(geometry.width) expect(point.y).toBeGreaterThanOrEqual(0) expect(point.y).toBeLessThanOrEqual(geometry.height) } await expectMarkersInsideBasemap(page) await screenshot(page, testInfo, 'webp-composite-route-preview-390x844') const beforeZoom = await getTwoDimensionalViewport(page) const expanded = await clickGuideZoom(page, 'out') expect(expanded.visibleWorldSpan).toBeGreaterThan(beforeZoom.visibleWorldSpan) const restoredZoom = await clickGuideZoom(page, 'in') expect(restoredZoom.visibleWorldSpan).toBeLessThan(expanded.visibleWorldSpan) await expect(weakMap).toHaveAttribute('data-active-basemap-floor-code', 'EXTERIOR_L1') const beforeDrag = await getTwoDimensionalViewport(page) const afterDrag = await dragTwoDimensionalMap(page) expect(Math.hypot( afterDrag.centerX - beforeDrag.centerX, afterDrag.centerZ - beforeDrag.centerZ )).toBeGreaterThan(0.1) await expect(weakMap).toHaveAttribute('data-active-basemap-floor-code', 'EXTERIOR_L1') await expectMarkersInsideBasemap(page) await page.locator('.route-primary-action').click() await expect(page.locator('.route-sim-step-bar')).toBeVisible({ timeout: 30_000 }) await expect(page.getByTestId('weak-network-route-progress')).toBeVisible({ timeout: 30_000 }) await page.waitForTimeout(1_200) await expect(weakMap).toHaveAttribute('data-active-basemap-floor-code', 'EXTERIOR_L1') await expect(weakMap).toHaveAttribute('data-viewport-floor-id', 'EXTERIOR_L1') const progress = await page.getByTestId('weak-network-route-progress').evaluate((element) => { const surface = document.querySelector('[data-testid="weak-network-surface"]')! const bounds = element.getBoundingClientRect() const surfaceBounds = surface.getBoundingClientRect() return { x: bounds.left + bounds.width / 2 - surfaceBounds.left, y: bounds.top + bounds.height / 2 - surfaceBounds.top, width: surfaceBounds.width, height: surfaceBounds.height } }) expect(progress.x).toBeGreaterThanOrEqual(0) expect(progress.x).toBeLessThanOrEqual(progress.width) expect(progress.y).toBeGreaterThanOrEqual(0) expect(progress.y).toBeLessThanOrEqual(progress.height) await screenshot(page, testInfo, 'webp-composite-route-simulation-390x844') await page.locator('.route-sim-close').click() await expect(page.locator('.route-sim-step-bar')).toHaveCount(0) await expect(page.locator('.weak-network-route-line')).toHaveCount(0) }) test('enters at the shared threshold, applies cooldown, and blocks selected-floor auto exit', async ({ page }) => { await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await enterDefaultFloorFromOverview(page) const floorAtEntry = await getTwoDimensionalViewport(page) expect(floorAtEntry.scene).toBe('floor') await clickGuideZoom(page, 'out') await clickGuideZoom(page, 'out') expect((await getTwoDimensionalViewport(page)).scene).toBe('floor') await page.waitForTimeout(1600) await clickGuideZoom(page, 'out') await clickGuideZoom(page, 'out') await expect.poll(async () => (await getTwoDimensionalViewport(page)).scene, { timeout: 60_000 }).toBe('overview') await page.waitForTimeout(1600) await enterDefaultFloorFromOverview(page) const marker = page.locator('.weak-network-marker').first() await expect(marker).toBeVisible({ timeout: 30_000 }) await marker.click() await expect(marker).toHaveClass(/weak-network-marker--selected/) await page.waitForTimeout(1600) await clickGuideZoom(page, 'out') await clickGuideZoom(page, 'out') expect((await getTwoDimensionalViewport(page)).scene).toBe('floor') }) test('captures L1, L2, and L3 top-view basemaps at 390x844', async ({ page }, testInfo) => { await captureFloorScreenshots(page, testInfo, { width: 390, height: 844 }, 'floors-390x844') }) test('captures L1, L2, and L3 top-view basemaps at 430x932', async ({ page }, testInfo) => { await captureFloorScreenshots(page, testInfo, { width: 430, height: 932 }, 'floors-430x932') }) test('keeps indoor floor labels usable when every GLB request fails', async ({ page }) => { await clearGuideModelCacheForTest(page) await page.route(/\.glb(?:$|\?)/i, async (route) => { await route.abort('failed') }) await page.goto('/') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-map')).toBeVisible({ timeout: 180_000 }) await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('2D') await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 30_000 }) await expect(page.getByTestId('weak-network-neutral-grid')).toHaveCount(0) await expect(page.locator('.floor-switcher')).toHaveCount(0) await expect(page.locator('.weak-network-marker').first()).toBeVisible({ timeout: 30_000 }) await expect(page.locator('.weak-network-empty')).toHaveCount(0) await enterDefaultFloorFromOverview(page) await expect(page.locator('.weak-network-marker').first()).toBeVisible({ timeout: 60_000 }) const activeFloor = page.locator('.floor-item.active').first() const initialFloor = await activeFloor.innerText() const nextFloor = page.locator('.floor-item').filter({ hasNotText: initialFloor }).first() await expect(nextFloor).toBeVisible({ timeout: 30_000 }) await nextFloor.click() await expect(nextFloor).toHaveClass(/active/, { timeout: 60_000 }) }) test('keeps the 2D basemap interactive when every floor POI request fails', async ({ page }) => { let failedPoiRequests = 0 await page.route(/\/app-api\/gis\/sdk\/floors\/[^/?]+\/(?:bundle|pois|spaces|business-pois|navigable-places)(?:\?|$)/, async (route) => { failedPoiRequests += 1 await route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 503, msg: 'simulated POI outage' }) }) }) await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) const beforeZoom = await getTwoDimensionalViewport(page) expect(beforeZoom.scene).toBe('overview') await enterDefaultFloorFromOverview(page) await expect(page.locator('.weak-network-empty')).toBeVisible({ timeout: 30_000 }) const secondFloor = page.locator('.floor-item').filter({ hasText: '2F' }).first() await secondFloor.click() await expect(secondFloor).toHaveClass(/active/, { timeout: 60_000 }) await expect(page.getByTestId('weak-network-basemap').locator('img')).toHaveAttribute( 'src', /\/L2\.[^/]+\.ortho-yaw54-v2\.compact-floor-center-v1\.webp$/i, { timeout: 60_000 } ) await expect(page.locator('.weak-network-empty')).toBeVisible({ timeout: 30_000 }) expect(failedPoiRequests).toBeGreaterThan(0) }) test('rejects an obsolete basemap projection and recovers after a valid reload', async ({ page }) => { const manifestPattern = '**/static/guide-floor-basemaps/manifest.json' await page.route(manifestPattern, async (route) => { const response = await route.fetch() const manifest = await response.json() await route.fulfill({ response, json: { ...manifest, render: { ...manifest.render, projectionVersion: 'oblique-v0' } } }) }) await page.goto('/?weak-map-preview=1#/pages/index/index?tab=guide') await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-neutral-grid')).toBeVisible({ timeout: 60_000 }) await expect(page.getByTestId('weak-network-basemap')).toHaveCount(0) await page.unroute(manifestPattern) await page.reload() await page.waitForURL(/weak-map-preview=1.*tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 60_000 }) await expect(page.getByTestId('weak-network-neutral-grid')).toHaveCount(0) }) test('falls back to the two-dimensional guide when a GLB request stops making progress', async ({ page }) => { await clearGuideModelCacheForTest(page) await page.route(/\.glb(?:$|\?)/i, async (route) => { await new Promise((resolve) => setTimeout(resolve, 20_000)) await route.continue() }) const startedAt = Date.now() await page.goto('/') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-map')).toBeVisible({ timeout: 20_000 }) expect(Date.now() - startedAt).toBeLessThan(9_000) await expect(page.getByTestId('indoor-render-mode-toggle')).toHaveText('2D') await expect(page.getByTestId('weak-network-basemap')).toBeVisible({ timeout: 30_000 }) }) test('reuses a persisted model after the next page load loses network access', async ({ page }) => { await page.goto('/') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await expect.poll(async () => page.evaluate(async () => { const cache = await caches.open('sgs-guide-model-v1') return (await cache.keys()).length }), { timeout: 180_000 }).toBeGreaterThan(0) await page.route(/\.glb(?:$|\?)/i, async (route) => { await route.abort('failed') }) await page.reload() await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await expect(page.getByTestId('weak-network-map')).toHaveCount(0) await expect.poll(async () => page.evaluate(() => ( (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: { isInitialModelReady: () => boolean } }).__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false )), { timeout: 180_000 }).toBe(true) })