Files
frontend-miniapp/tests/e2e/weak-network-fallback.spec.ts
lyf 9ea1bfab71
Some checks failed
CI / verify (push) Has been cancelled
同步 sgs-frontend-mobile 源码
2026-07-27 11:26:01 +08:00

1256 lines
53 KiB
TypeScript

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<void>
}
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<ThreeDimensionalSceneState> => 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<HTMLElement>('.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<HTMLCanvasElement>('.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<ReturnType<typeof getLiveGlbState>>, 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<TwoDimensionalViewportState> => (
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<WeakMapDensityState> => page.evaluate(() => {
const map = document.querySelector<HTMLElement>('[data-testid="weak-network-map"]')
if (!map) throw new Error('Weak network map is unavailable')
const markers = [...document.querySelectorAll<HTMLElement>('.weak-network-marker')]
const labels = [...document.querySelectorAll<HTMLElement>('.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<HTMLElement>('[data-testid="weak-network-basemap"]')
if (!image) return { outside: ['missing-basemap'], markerCount: 0 }
const imageBounds = image.getBoundingClientRect()
const markers = [...document.querySelectorAll<HTMLElement>('.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: '<!doctype html><title>clear guide cache</title>'
})
})
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<HTMLElement>('[data-testid="weak-network-surface"]')
const image = document.querySelector<HTMLElement>('[data-testid="weak-network-basemap"]')
if (!surface || !image) throw new Error('Composite WebP surface is unavailable')
const lines = [...document.querySelectorAll<HTMLElement>('.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<HTMLElement>('[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)
})