同步 sgs-frontend-mobile 源码
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-27 11:26:01 +08:00
parent 575dca430f
commit 9ea1bfab71
181 changed files with 24395 additions and 5212 deletions

View File

@@ -116,20 +116,6 @@ const getReport = async (page: Page) => {
return state!.report
}
const FLOOR_CAMERA_BASELINE = {
position: [315.2911, 385.8819, 234.6114] as const,
target: [19.7236, -65.382, 42.1148] as const,
zoom: 1,
distance: 572.7601
}
const assertFloorCameraBaseline = (camera: CameraReport) => {
expect(getVectorDelta(camera.position, FLOOR_CAMERA_BASELINE.position)).toBeLessThan(1e-4)
expect(getVectorDelta(camera.target, FLOOR_CAMERA_BASELINE.target)).toBeLessThan(1e-4)
expect(camera.zoom).toBe(FLOOR_CAMERA_BASELINE.zoom)
expect(Math.abs(camera.distance - FLOOR_CAMERA_BASELINE.distance)).toBeLessThan(1e-3)
}
const openGuide = async (page: Page) => {
await page.goto('/')
await page.waitForURL(/tab=guide/, { timeout: 30_000 })
@@ -169,23 +155,20 @@ const getMaximumCameraDelta = (before: CameraReport, after: CameraReport) => Mat
Math.abs(before.distance - after.distance)
)
const getMaximumAnchorDeltaPx = (before: AnchorReport[], after: AnchorReport[]) => Math.max(
...before.map((anchor) => {
const next = after.find((candidate) => candidate.id === anchor.id)
expect(next, `missing ${anchor.id}`).toBeDefined()
return Math.hypot(anchor.screen.x - next!.screen.x, anchor.screen.y - next!.screen.y)
})
)
const assertStableTransition = (before: VisualReport, after: VisualReport) => {
const assertStableTransition = (after: VisualReport) => {
expect(after.isCameraTweening).toBe(false)
expect(after.modelRoot, 'the active model must stay committed').not.toBeNull()
expect(after.modelRoot!.maxError).toBeLessThan(1e-12)
expect(getMaximumCameraDelta(before.camera, after.camera)).toBeLessThan(1e-6)
expect(after.anchors).toHaveLength(3)
expect(getMaximumAnchorDeltaPx(before.anchors, after.anchors)).toBeLessThanOrEqual(2)
}
const getFloorBaseline = async (page: Page, floorId: string) => page.evaluate((targetFloorId) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
return api.getFloorBaseline(targetFloorId)
}, floorId)
const screenshot = async (page: Page, testInfo: TestInfo, name: string) => {
await page.screenshot({ path: testInfo.outputPath(`${name}.png`) })
}
@@ -213,7 +196,6 @@ const waitForTransition = async (
}
const switchFloor = async (page: Page, testInfo: TestInfo, floorId: string, name: string) => {
const before = await getReport(page)
await screenshot(page, testInfo, `${name}-before`)
const transition = page.evaluate(async (id) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
@@ -224,11 +206,15 @@ const switchFloor = async (page: Page, testInfo: TestInfo, floorId: string, name
await waitForTransition(page, transition, testInfo, name)
const after = await getReport(page)
await screenshot(page, testInfo, `${name}-after`)
assertStableTransition(before, after)
assertStableTransition(after)
expect(after.activeView).toBe('floor')
expect(after.floorId).toBe(floorId)
const baseline = await getFloorBaseline(page, floorId)
expect(baseline, `missing ${floorId} floor baseline`).not.toBeNull()
expect(getMaximumCameraDelta(baseline!, after.camera)).toBeLessThan(1e-6)
}
const showOverview = async (page: Page, testInfo: TestInfo, name: string) => {
const before = await getReport(page)
await screenshot(page, testInfo, `${name}-before`)
const transition = page.evaluate(async () => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
@@ -239,7 +225,8 @@ const showOverview = async (page: Page, testInfo: TestInfo, name: string) => {
await waitForTransition(page, transition, testInfo, name)
const after = await getReport(page)
await screenshot(page, testInfo, `${name}-after`)
assertStableTransition(before, after)
assertStableTransition(after)
expect(after.activeView).toBe('overview')
}
const exitOverviewWithZoomInput = async (
@@ -272,6 +259,7 @@ const exitOverviewWithZoomInput = async (
}
await expect.poll(async () => (await getReport(page)).activeView, { timeout: 10_000 }).toBe('overview')
await expect.poll(async () => (await getReport(page)).isCameraTweening, { timeout: 10_000 }).toBe(false)
const after = await getReport(page)
await screenshot(page, testInfo, `${name}-after`)
@@ -316,20 +304,18 @@ const getVectorDelta = (left: { x: number; y: number; z: number }, right: [numbe
)
const waitForPoiFocusToSettle = async (page: Page, testInfo: TestInfo, name: string) => {
let sawTween = false
let stableFrames = 0
for (let frame = 0; frame < 180; frame += 1) {
const report = await getReport(page)
expect(report.modelRoot, `blank POI focus frame ${frame}`).not.toBeNull()
expect(report.modelRoot!.maxError).toBeLessThan(1e-12)
sawTween ||= report.isCameraTweening
stableFrames = report.isCameraTweening ? 0 : stableFrames + 1
if (frame === 0) {
await screenshot(page, testInfo, `${name}-during`)
}
if (sawTween && stableFrames >= 3) return report
if (report.focus && stableFrames >= 3) return report
await page.waitForTimeout(40)
}
@@ -379,6 +365,40 @@ test('ordinary exterior and floor switches preserve the GLB_METER camera project
await exitOverviewWithZoomInput(page, testInfo, 'lminus1-exterior-touch-exit', 'touch')
})
test('delayed exterior zoom button presses still enter the indoor view at the fixed threshold', async ({ page }) => {
await openGuide(page)
expect((await getReport(page)).activeView).toBe('overview')
await page.getByText('+', { exact: true }).click()
await page.waitForTimeout(2_200)
await page.getByText('+', { exact: true }).click()
await expect.poll(async () => (await getReport(page)).activeView, { timeout: 20_000 }).toBe('floor')
})
test('3F exterior round trip commits the floor at its visual-center baseline', async ({ page }, testInfo) => {
await openGuide(page)
const state = await getApiState(page)
expect(state).not.toBeNull()
const l3 = resolveRequiredFloorId(state!.floors, 'L3')
await switchFloor(page, testInfo, l3, 'l3-round-trip-enter')
const baseline = await getFloorBaseline(page, l3)
expect(baseline).not.toBeNull()
await showOverview(page, testInfo, 'l3-round-trip-overview')
await page.getByText('+', { exact: true }).click()
await expect.poll(async () => (await getReport(page)).activeView, { timeout: 20_000 }).toBe('floor')
await expect.poll(async () => (await getReport(page)).isCameraTweening, { timeout: 10_000 }).toBe(false)
const reentered = await getReport(page)
await screenshot(page, testInfo, 'l3-round-trip-reentered')
expect(reentered.floorId).toBe(l3)
expect(reentered.isCameraTweening).toBe(false)
expect(reentered.modelRoot).not.toBeNull()
expect(getMaximumCameraDelta(baseline!, reentered.camera)).toBeLessThan(1e-6)
})
test('queued cross-floor search POI focus keeps raw GLB_METER coordinates and does not rebound', async ({ page }, testInfo) => {
await openGuide(page)
@@ -486,15 +506,8 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag
await api.resetToViewBaseline({ view: 'floor', floorId, reason: 'floor-reset' })
}, floor.floorId)
const first = await getReport(page)
const baseline = await page.evaluate((floorId) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
return api.getFloorBaseline(floorId)
}, floor.floorId)
const baseline = await getFloorBaseline(page, floor.floorId)
expect(baseline, `missing floor baseline ${floor.floorId}`).not.toBeNull()
assertFloorCameraBaseline(baseline!)
assertFloorCameraBaseline(first.camera)
expect(first.modelRoot!.maxError).toBeLessThan(1e-12)
expect(getMaximumCameraDelta(baseline!, first.camera)).toBeLessThan(1e-6)
await page.evaluate(async (floorId) => {
@@ -506,7 +519,6 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag
const second = await getReport(page)
expect(second.activeView).toBe('floor')
expect(second.floorId).toBe(floor.floorId)
assertFloorCameraBaseline(second.camera)
expect(getMaximumCameraDelta(first.camera, second.camera)).toBeLessThan(1e-6)
expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6)
}
@@ -541,7 +553,12 @@ test.describe('mobile POI card close restores the floor baseline', () => {
expect(facility, 'expected a visible facility POI from the active guide data').toBeDefined()
expect(hall, 'expected a visible hall POI from the active guide data').toBeDefined()
for (const [kind, marker] of [['facility', facility!], ['hall', hall!]] as const) {
// Floor-change completion triggers background neighbor preloading. Let it
// finish before checking that selecting an already-rendered marker does
// not itself start another model request.
await page.waitForTimeout(1_500)
for (const marker of [facility!, hall!]) {
const baseline = await page.evaluate((floorId) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
@@ -549,16 +566,14 @@ test.describe('mobile POI card close restores the floor baseline', () => {
}, marker.floorId)
expect(baseline).not.toBeNull()
let modelRequestsAfterBaseline = 0
page.on('request', (request) => {
const onRequest = (request: { url: () => string }) => {
if (/\.(glb|gltf)(?:$|\?)/i.test(request.url())) modelRequestsAfterBaseline += 1
})
}
page.on('request', onRequest)
await page.mouse.click(marker.screen.x, marker.screen.y)
await expect.poll(async () => (await getReport(page)).activeFocusPoiId, { timeout: 10_000 }).toBe(marker.poiId)
await expect(page.locator('.guide-poi-card')).toBeVisible()
if (kind === 'facility') {
await page.locator('.poi-card-collapsed').click()
}
await page.locator('.poi-card-close').click()
await expect(page.locator('.guide-poi-card')).toBeHidden()
await expect.poll(async () => (await getReport(page)).activeFocusPoiId, { timeout: 10_000 }).toBe('')
@@ -571,6 +586,7 @@ test.describe('mobile POI card close restores the floor baseline', () => {
expect(afterClose.floorId).toBe(marker.floorId)
expect(getMaximumCameraDelta(baseline!, afterClose.camera)).toBeLessThan(1e-6)
expect(modelRequestsAfterBaseline).toBe(0)
page.off('request', onRequest)
await page.waitForTimeout(300)
const afterIdle = await getReport(page)
expect(getMaximumCameraDelta(afterClose.camera, afterIdle.camera)).toBeLessThan(1e-6)