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

210 lines
8.5 KiB
TypeScript

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