210 lines
8.5 KiB
TypeScript
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}`)
|
|
})
|