From 49b284952825d14ae0004c99cd74aedac157c92a Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 11 Nov 2026 02:07:36 +0400 Subject: [PATCH] Wait for the directions route flight instead of three seconds The FLY check slept a fixed 3 s and then expected a route flight, which passed or failed on the same commit. It now waits, up to 15 s, for the layer to report the route motion and the camera to move. --- scripts/qa-directions.mjs | 29 +++++++++++-- scripts/qa-directions.test.mjs | 75 ++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 scripts/qa-directions.test.mjs diff --git a/scripts/qa-directions.mjs b/scripts/qa-directions.mjs index d593c6a05..0347556e1 100644 --- a/scripts/qa-directions.mjs +++ b/scripts/qa-directions.mjs @@ -159,6 +159,28 @@ const setMode = async (mode) => { await clickChip(`mode-${mode}`); }; +const waitForFlight = (before) => + pollPage( + page, + (origin) => { + const { engine, dataManager } = window.__godsEyeView; + const motion = dataManager.layers + .get('directions') + .module.getFlightProgress(); + const position = engine.camera.pose().position; + return ( + motion?.kind === 'route' && + motion.traveledM > 0 && + Math.hypot( + position.x - origin.x, + position.y - origin.y, + position.z - origin.z, + ) > 50 + ); + }, + { timeoutMs: 15000, intervalMs: 50, args: [before] }, + ); + const waitForRoute = () => page.waitForFunction( () => @@ -665,12 +687,12 @@ try { // ── 7. FLY, a mid-flight frame, and a cancel that actually stops it. ───── const beforeFly = await cameraPose(); check('the FLY chip is offered once a route exists', await clickChip('fly')); - await sleep(3000); + const flightStarted = await waitForFlight(beforeFly); const flying = await layerState(); const midFly = await cameraPose(); check( 'FLY starts a route flight', - flying.cameraMotion?.kind === 'route', + flightStarted && flying.cameraMotion?.kind === 'route', JSON.stringify(flying.cameraMotion), ); check( @@ -699,8 +721,9 @@ try { await setMode('car'); await waitForMode('Drive'); await whenUiSettled(page); + const beforeRefly = await cameraPose(); check('FLY is offered again after the reroute', await clickChip('fly')); - await sleep(2000); + check('the second FLY starts before CLEAR', await waitForFlight(beforeRefly)); await clickChip('clear'); await sleep(500); diff --git a/scripts/qa-directions.test.mjs b/scripts/qa-directions.test.mjs new file mode 100644 index 000000000..1d2103bb0 --- /dev/null +++ b/scripts/qa-directions.test.mjs @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import vm from 'node:vm'; +import { pollPage } from './qa/ready.mjs'; +import { createFakeEngine } from '../src/globe/testing/fakeEngine.js'; + +// Evaluate the browser wait without launching the executable scenario. +const source = readFileSync( + new URL('./qa-directions.mjs', import.meta.url), + 'utf8', +); +const waitSource = source.slice( + source.indexOf('const waitForFlight ='), + source.indexOf('const waitForRoute ='), +); + +for (const startsAt of [50, 12000, null]) { + test(`directions FLY waits for owned route progress (${startsAt ?? 'never'} ms)`, async (t) => { + const engine = createFakeEngine(); + t.after(() => engine.destroy()); + let motion = null; + let disposed = 0; + let observedAt; + const before = engine.camera.pose().position; + const waitForFlight = vm.runInNewContext(`${waitSource}\nwaitForFlight`, { + pollPage, + window: { + __godsEyeView: { + engine, + dataManager: { + layers: new Map([ + ['directions', { module: { getFlightProgress: () => motion } }], + ]), + }, + }, + }, + page: { + async waitForFunction(check, options, origin) { + const predicate = () => check(origin); + assert.equal(options.timeout, 15000); + assert.equal(options.polling, 50); + assert.equal(predicate(), false, 'no owned flight yet'); + motion = { kind: 'route', traveledM: 0 }; + assert.equal(predicate(), false, 'arming is not a mid-flight frame'); + motion = { kind: 'orbit', traveledM: 20 }; + assert.equal(predicate(), false, 'another camera motion cannot pass'); + motion = { kind: 'route', traveledM: 20 }; + assert.equal(predicate(), false, 'the camera must have moved too'); + engine.fake.setPose({ position: { ...before, y: before.y + 60 } }); + for (let time = 0; time < options.timeout; time += options.polling) { + motion = + startsAt !== null && time >= startsAt + ? { kind: 'route', traveledM: 20 } + : null; + if (predicate()) { + observedAt = time; + return { + dispose: async () => { + disposed++; + }, + }; + } + } + const error = new Error('flight did not start'); + error.name = 'TimeoutError'; + throw error; + }, + }, + }); + assert.equal(await waitForFlight(before), startsAt !== null); + assert.equal(observedAt, startsAt ?? undefined); + assert.equal(disposed, startsAt === null ? 0 : 1); + }); +}