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

152 lines
6.3 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
interface CameraReport {
position: { x: number; y: number; z: number }
target: { x: number; y: number; z: number }
fov: number
distance: number
}
interface GuideDiagnostics {
isInitialModelReady: () => boolean
getReport: () => { camera: CameraReport; activeView: string; isCameraTweening: boolean }
setCameraCalibration: (values: { yaw: number }) => void
getFloors: () => Array<{ floorId: string; label: string }>
switchFloor: (floorId: string) => Promise<void>
zoomCamera: (direction: 'in' | 'out', options?: { source?: 'button' | 'gesture' }) => void
}
const getCameraReport = (page: Page) => page.evaluate(() => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable')
return diagnostics.getReport().camera
})
const getYawDegrees = (camera: CameraReport) => (
Math.atan2(
camera.position.x - camera.target.x,
camera.position.z - camera.target.z
) * 180 / Math.PI
)
const getElevationDegrees = (camera: CameraReport) => {
const x = camera.position.x - camera.target.x
const y = camera.position.y - camera.target.y
const z = camera.position.z - camera.target.z
const distance = Math.hypot(x, y, z)
return Math.asin(y / distance) * 180 / Math.PI
}
test('development camera calibration panel applies and resets exterior camera values', async ({ page }) => {
await page.goto('/?camera-calibration=1')
await page.waitForURL(/tab=guide/, { timeout: 30_000 })
await expect(page.locator('.camera-calibration-panel')).toBeVisible({ timeout: 30_000 })
await page.locator('[data-camera-calibration-action="open"]').click()
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 before = await getCameraReport(page)
const yawSlider = page.locator('[data-camera-calibration="yaw"]')
await expect(yawSlider).toBeVisible()
await page.evaluate(() => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable')
diagnostics.setCameraCalibration({ yaw: 35 })
})
await expect.poll(async () => Math.abs(getYawDegrees(await getCameraReport(page)) - 35), {
timeout: 10_000
}).toBeLessThan(0.01)
await page.locator('[data-camera-calibration-action="reset"]').click()
await expect.poll(async () => Math.abs(getYawDegrees(await getCameraReport(page)) - 54), {
timeout: 10_000
}).toBeLessThan(0.01)
const after = await getCameraReport(page)
expect(after.fov).toBeCloseTo(before.fov, 6)
expect(after.distance).toBeGreaterThan(0)
const floorId = await page.evaluate(() => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
const firstFloor = diagnostics?.getFloors().find((floor) => floor.label === '1F')
|| diagnostics?.getFloors()[0]
if (!diagnostics || !firstFloor) throw new Error('Floor diagnostics unavailable')
void diagnostics.switchFloor(firstFloor.floorId)
return firstFloor.floorId
})
await expect.poll(async () => page.evaluate(() => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
return diagnostics?.getReport() || null
}), { timeout: 90_000 }).toMatchObject({
activeView: 'floor',
isCameraTweening: false
})
const indoor = await getCameraReport(page)
expect(getYawDegrees(indoor)).toBeCloseTo(54, 6)
expect(getElevationDegrees(indoor)).toBeCloseTo(42, 6)
expect(indoor.fov).toBeCloseTo(42, 6)
// Interior uses the exterior's visual target instead of each floor model's
// bounding-box center, so the building cannot drift on the handoff.
expect(indoor.target.x).toBeCloseTo(after.target.x, 6)
expect(indoor.target.y).toBeCloseTo(after.target.y, 6)
expect(indoor.target.z).toBeCloseTo(after.target.z, 6)
expect(floorId).toBeTruthy()
const floorTargets = await page.evaluate(() => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable')
return diagnostics.getFloors().filter((floor) => ['1F', 'MF', '3F', '4F', '5F'].includes(floor.label))
})
expect(floorTargets.map((floor) => floor.label).sort()).toEqual(['1F', '3F', '4F', '5F', 'MF'])
for (const targetFloor of floorTargets) {
await page.evaluate(async (targetFloorId) => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable')
await diagnostics.switchFloor(targetFloorId)
}, targetFloor.floorId)
await expect.poll(async () => page.evaluate(() => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
return diagnostics?.getReport().isCameraTweening || false
}), { timeout: 90_000 }).toBe(false)
const currentFloorCamera = await getCameraReport(page)
expect(currentFloorCamera.distance).toBeCloseTo(indoor.distance, 6)
expect(getYawDegrees(currentFloorCamera)).toBeCloseTo(54, 6)
expect(getElevationDegrees(currentFloorCamera)).toBeCloseTo(42, 6)
}
let expectedZoomDistance = indoor.distance
for (let count = 0; count < 5; count += 1) {
expectedZoomDistance = Math.max(indoor.distance * 0.2, expectedZoomDistance * 0.72)
await page.evaluate(() => {
const diagnostics = (window as Window & {
__GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics
}).__GUIDE_3D_VISUAL_STABILITY__
if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable')
diagnostics.zoomCamera('in', { source: 'button' })
})
await expect.poll(async () => (await getCameraReport(page)).distance, { timeout: 10_000 })
.toBeCloseTo(expectedZoomDistance, 6)
}
})