@@ -30,6 +30,14 @@ interface VisualReport {
|
||||
activeView: string
|
||||
floorId: string
|
||||
isCameraTweening: boolean
|
||||
poiFocusCameraAnimationCount: number
|
||||
cameraSnapshotRestoreCount: number
|
||||
focus: {
|
||||
poiId: string
|
||||
floorId: string
|
||||
positionGltf: [number, number, number]
|
||||
markerPosition: { x: number; y: number; z: number } | null
|
||||
} | null
|
||||
}
|
||||
|
||||
interface FloorReport {
|
||||
@@ -38,11 +46,27 @@ interface FloorReport {
|
||||
modelMatchKeys: string[]
|
||||
}
|
||||
|
||||
interface PoiFocusCandidate {
|
||||
poiId: string
|
||||
floorId: string
|
||||
name: string
|
||||
positionGltf: [number, number, number]
|
||||
}
|
||||
|
||||
interface VisualStabilityApi {
|
||||
getReport: () => VisualReport
|
||||
getFloors: () => FloorReport[]
|
||||
switchFloor: (floorId: string) => Promise<void>
|
||||
showOverview: () => Promise<void>
|
||||
getFloorPois: (floorId: string) => Promise<PoiFocusCandidate[]>
|
||||
focusTargetPoi: (request: {
|
||||
requestId: number | string
|
||||
poiId: string
|
||||
floorId: string
|
||||
name?: string
|
||||
positionGltf: [number, number, number]
|
||||
}) => Promise<unknown>
|
||||
clearTargetFocus: () => void
|
||||
}
|
||||
|
||||
const getApiState = async (page: Page) => {
|
||||
@@ -219,6 +243,43 @@ const resolveRequiredFloorId = (floors: FloorReport[], requested: string) => {
|
||||
return floor!.floorId
|
||||
}
|
||||
|
||||
const getFloorPois = async (page: Page, floorId: string) => {
|
||||
const pois = await page.evaluate(async (id) => {
|
||||
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.getFloorPois(id)
|
||||
}, floorId)
|
||||
expect(pois.length, `floor ${floorId} must contain a positioned POI`).toBeGreaterThan(0)
|
||||
return pois
|
||||
}
|
||||
|
||||
const getVectorDelta = (left: { x: number; y: number; z: number }, right: [number, number, number]) => (
|
||||
Math.hypot(left.x - right[0], left.y - right[1], left.z - right[2])
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
await page.waitForTimeout(40)
|
||||
}
|
||||
|
||||
throw new Error(`${name} did not settle after one POI focus animation`)
|
||||
}
|
||||
|
||||
test('ordinary exterior and floor switches preserve the GLB_METER camera projection', async ({ page }, testInfo) => {
|
||||
await page.goto('/')
|
||||
await Promise.all([
|
||||
@@ -235,14 +296,14 @@ test('ordinary exterior and floor switches preserve the GLB_METER camera project
|
||||
const lMinus2 = resolveRequiredFloorId(state!.floors, 'L-2')
|
||||
const l1 = resolveRequiredFloorId(state!.floors, 'L1')
|
||||
const l1Point5 = resolveRequiredFloorId(state!.floors, 'L1.5')
|
||||
const l2 = resolveRequiredFloorId(state!.floors, 'L2')
|
||||
const l3 = resolveRequiredFloorId(state!.floors, 'L3')
|
||||
const lMinus1 = resolveRequiredFloorId(state!.floors, 'L-1')
|
||||
const l4 = resolveRequiredFloorId(state!.floors, 'L4')
|
||||
const l5 = resolveRequiredFloorId(state!.floors, 'L5')
|
||||
|
||||
await switchFloor(page, testInfo, l1, 'exterior-l1')
|
||||
await switchFloor(page, testInfo, l2, 'l1-l2')
|
||||
await switchFloor(page, testInfo, l3, 'l2-l3')
|
||||
await switchFloor(page, testInfo, l1, 'l3-l1')
|
||||
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1')
|
||||
await switchFloor(page, testInfo, l4, 'lminus1-l4')
|
||||
await switchFloor(page, testInfo, l1, 'l4-l1')
|
||||
await showOverview(page, testInfo, 'l1-exterior')
|
||||
|
||||
await switchFloor(page, testInfo, lMinus2, 'exterior-lminus2')
|
||||
@@ -254,14 +315,87 @@ test('ordinary exterior and floor switches preserve the GLB_METER camera project
|
||||
await showOverview(page, testInfo, 'l1point5-exterior')
|
||||
|
||||
await switchFloor(page, testInfo, l1, 'exterior-l1-auto-exit')
|
||||
await switchFloor(page, testInfo, l2, 'l1-l2-auto-exit')
|
||||
await exitOverviewWithZoomInput(page, testInfo, 'l2-exterior-button-exit', 'button')
|
||||
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1-auto-exit')
|
||||
await exitOverviewWithZoomInput(page, testInfo, 'lminus1-exterior-button-exit', 'button')
|
||||
|
||||
await switchFloor(page, testInfo, l1, 'exterior-l1-wheel-exit')
|
||||
await switchFloor(page, testInfo, l2, 'l1-l2-wheel-exit')
|
||||
await exitOverviewWithZoomInput(page, testInfo, 'l2-exterior-wheel-exit', 'wheel')
|
||||
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1-wheel-exit')
|
||||
await exitOverviewWithZoomInput(page, testInfo, 'lminus1-exterior-wheel-exit', 'wheel')
|
||||
|
||||
await switchFloor(page, testInfo, l1, 'exterior-l1-touch-exit')
|
||||
await switchFloor(page, testInfo, l2, 'l1-l2-touch-exit')
|
||||
await exitOverviewWithZoomInput(page, testInfo, 'l2-exterior-touch-exit', 'touch')
|
||||
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1-touch-exit')
|
||||
await exitOverviewWithZoomInput(page, testInfo, 'lminus1-exterior-touch-exit', 'touch')
|
||||
})
|
||||
|
||||
test('queued cross-floor search POI focus keeps raw GLB_METER coordinates and does not rebound', async ({ page }, testInfo) => {
|
||||
await page.goto('/')
|
||||
await Promise.all([
|
||||
page.waitForURL(/tab=guide/, { timeout: 30_000 }),
|
||||
page.getByText('馆内', { exact: true }).click()
|
||||
])
|
||||
await expect.poll(async () => (await getApiState(page)) !== null, { timeout: 180_000 }).toBe(true)
|
||||
|
||||
const state = await getApiState(page)
|
||||
expect(state).not.toBeNull()
|
||||
const l1 = resolveRequiredFloorId(state!.floors, 'L1')
|
||||
const lMinus1 = resolveRequiredFloorId(state!.floors, 'L-1')
|
||||
const [targetPoi] = await getFloorPois(page, lMinus1)
|
||||
|
||||
await switchFloor(page, testInfo, l1, 'search-focus-exterior-l1')
|
||||
const beforeFocus = await getReport(page)
|
||||
await screenshot(page, testInfo, 'search-focus-before')
|
||||
|
||||
const focusTransition = page.evaluate(async (request) => {
|
||||
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
|
||||
.__GUIDE_3D_VISUAL_STABILITY__
|
||||
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
|
||||
await api.focusTargetPoi(request)
|
||||
}, {
|
||||
requestId: 'e2e-cross-floor-search-focus',
|
||||
poiId: targetPoi.poiId,
|
||||
floorId: targetPoi.floorId,
|
||||
name: targetPoi.name,
|
||||
positionGltf: targetPoi.positionGltf
|
||||
})
|
||||
|
||||
let focusRequestCompleted = false
|
||||
void focusTransition.then(() => { focusRequestCompleted = true })
|
||||
for (let frame = 0; !focusRequestCompleted && frame < 180; frame += 1) {
|
||||
const report = await getReport(page)
|
||||
expect(report.modelRoot, `blank cross-floor POI load frame ${frame}`).not.toBeNull()
|
||||
expect(report.modelRoot!.maxError).toBeLessThan(1e-12)
|
||||
await page.waitForTimeout(40)
|
||||
}
|
||||
expect(focusRequestCompleted, 'cross-floor POI focus exceeded the sampling window').toBe(true)
|
||||
await focusTransition
|
||||
|
||||
const afterFocus = await waitForPoiFocusToSettle(page, testInfo, 'search-focus')
|
||||
await screenshot(page, testInfo, 'search-focus-after')
|
||||
|
||||
expect(afterFocus.activeView).toBe('floor')
|
||||
expect(afterFocus.floorId).toBe(lMinus1)
|
||||
expect(afterFocus.focus).not.toBeNull()
|
||||
expect(afterFocus.focus!.poiId).toBe(targetPoi.poiId)
|
||||
expect(afterFocus.focus!.positionGltf).toEqual(targetPoi.positionGltf)
|
||||
expect(getVectorDelta(afterFocus.camera.target, targetPoi.positionGltf)).toBeLessThan(1e-6)
|
||||
expect(afterFocus.focus!.markerPosition).not.toBeNull()
|
||||
expect(getVectorDelta(afterFocus.focus!.markerPosition!, targetPoi.positionGltf)).toBeLessThan(1e-6)
|
||||
expect(afterFocus.cameraSnapshotRestoreCount - beforeFocus.cameraSnapshotRestoreCount).toBe(1)
|
||||
expect(afterFocus.poiFocusCameraAnimationCount - beforeFocus.poiFocusCameraAnimationCount).toBe(1)
|
||||
|
||||
await page.waitForTimeout(240)
|
||||
const afterIdle = await getReport(page)
|
||||
expect(afterIdle.isCameraTweening).toBe(false)
|
||||
expect(afterIdle.poiFocusCameraAnimationCount).toBe(afterFocus.poiFocusCameraAnimationCount)
|
||||
expect(getMaximumCameraDelta(afterFocus.camera, afterIdle.camera)).toBeLessThan(1e-6)
|
||||
|
||||
await page.evaluate(() => {
|
||||
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
|
||||
.__GUIDE_3D_VISUAL_STABILITY__
|
||||
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
|
||||
api.clearTargetFocus()
|
||||
})
|
||||
|
||||
await switchFloor(page, testInfo, l1, 'search-focus-clear-lminus1-l1')
|
||||
await showOverview(page, testInfo, 'search-focus-clear-l1-exterior')
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('点位详情实际渲染与返回闭环', () => {
|
||||
const sheet = wrapper.get('[data-testid="facility-detail-sheet"]')
|
||||
const mapShell = wrapper.getComponent(GuideMapShellStub)
|
||||
expect(sheet.text()).toContain('二层卫生间')
|
||||
expect(sheet.text()).toContain('类别卫生间')
|
||||
expect(sheet.text()).toContain('类别洗手间')
|
||||
expect(sheet.text()).toContain('楼层2F')
|
||||
expect(sheet.text()).not.toContain('可无障碍到达')
|
||||
expect(sheet.text()).not.toContain('展示点位')
|
||||
|
||||
Reference in New Issue
Block a user