Restore the conditions eight converted QA scripts were waiting for

The harness conversion replaced pauses that stood in for real conditions
with camera readiness: the cables and infrastructure idle checks started
measuring while the overlay was still fading in after moveEnd, directions
sampled before its maneuver dots had ground cells, three scripts clicked
before the loading cover was gone, the credits check hit-tested during UI
transitions, and the oblique traffic flight lost its height cap. Each now
waits for its condition; idle checks observe without requesting frames
and record the reasons of the frames they count; readiness waits for the
startup cover and no longer requests frames while polling.
This commit is contained in:
German Khokhlov committed 2026-11-10 20:14:50 +04:00
1 parent 2c62e1e378
commit b5cc4feacb
11 files changed
+480 -51

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
/** Observe a quiet request-mode globe, then measure it without requesting frames. */
export function measureIdleRender(
page,
{ quietMs = 600, sampleMs = 5000, timeoutMs = 15000 } = {},
) {
return page.evaluate(
({ quietMs, sampleMs, timeoutMs }) =>
new Promise((resolve, reject) => {
const { engine } = window.__godsEyeView;
const started = performance.now();
let lastActivity = started;
let measuringAt = null;
let renders = 0;
let framesWithPendingTiles = 0;
let tilesLoadedBefore = false;
const settlingRequests = {};
const requests = {};
const original = engine.render.request;
const observedRequest = function (reason = 'globe-engine') {
const now = performance.now();
lastActivity = now;
const counts = measuringAt === null ? settlingRequests : requests;
const row = (counts[reason] ??= {
count: 0,
firstMs: now - started,
lastMs: 0,
});
row.count++;
row.lastMs = now - started;
return original.apply(this, arguments);
};
engine.render.request = observedRequest;
const remove = engine.frames.postRender.addEventListener(() => {
lastActivity = performance.now();
if (measuringAt === null) return;
renders++;
if (!engine.surface.tilesLoaded()) framesWithPendingTiles++;
});
const cleanup = () => {
clearInterval(timer);
remove();
if (engine.render.request === observedRequest)
engine.render.request = original;
};
const timer = setInterval(() => {
if (window.__godsEyeView.engine !== engine || engine.isDestroyed()) {
cleanup();
reject(new Error('Idle observation interrupted: engine changed'));
return;
}
const now = performance.now();
const tilesLoaded = engine.surface.tilesLoaded();
if (measuringAt === null) {
if (
!tilesLoaded ||
engine.camera.flightActive() ||
engine.render.isContinuous()
)
lastActivity = now;
if (now - lastActivity >= quietMs) {
measuringAt = now;
tilesLoadedBefore = tilesLoaded;
} else if (now - started >= timeoutMs) {
cleanup();
reject(
new Error(
`Readiness timeout: idle render; ${JSON.stringify({ tilesLoaded, settlingRequests })}`,
),
);
}
return;
}
if (now - measuringAt < sampleMs) return;
cleanup();
const governor =
window.__godsEyeView.getRenderGovernorDiagnostics?.();
resolve({
renders,
settleMs: Math.round(measuringAt - started),
settlingRequests,
requests,
tilesLoadedBefore,
tilesLoadedAfter: tilesLoaded,
framesWithPendingTiles,
mode: governor?.mode,
holds: governor?.holds,
});
}, 50);
}),
{ quietMs, sampleMs, timeoutMs },
);
}
+112
View File
@@ -0,0 +1,112 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { measureIdleRender } from './idle.mjs';
function fixture(t) {
const engine = createFakeEngine();
const previous = globalThis.window;
globalThis.window = { __godsEyeView: { engine } };
let now = 0;
t.mock.method(performance, 'now', () => now);
t.mock.timers.enable({ apis: ['setInterval'] });
t.after(() => {
if (previous === undefined) delete globalThis.window;
else globalThis.window = previous;
engine.destroy();
});
return {
engine,
page: { evaluate: (fn, options) => fn(options) },
tick(ms) {
now += ms;
t.mock.timers.tick(ms);
},
};
}
test('idle measurement waits out fades, records reasons, and never requests a frame', async (t) => {
const { engine, page, tick } = fixture(t);
const original = engine.render.request;
const pending = measureIdleRender(page, { quietMs: 100, sampleMs: 200 });
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
tick(50);
engine.render.request('hud');
engine.fake.frame();
tick(200);
const result = await pending;
assert.equal(result.settleMs, 150);
assert.equal(result.renders, 1);
assert.equal(result.settlingRequests['world-overlay'].count, 2);
assert.equal(result.requests.hud.count, 1);
assert.equal(result.framesWithPendingTiles, 0);
assert.equal(engine.fake.callsOf('render.request').length, 3);
assert.equal(engine.render.request, original);
assert.equal(engine.frames.postRender.numberOfListeners, 0);
tick(1000);
assert.equal(engine.fake.callsOf('render.request').length, 3);
});
test('persistent render demand fails with its reason and releases instrumentation', async (t) => {
const { engine, page, tick } = fixture(t);
const original = engine.render.request;
const pending = measureIdleRender(page, { quietMs: 100, timeoutMs: 300 });
const rejected = assert.rejects(pending, /idle render.*world-overlay/);
for (let i = 0; i < 6; i++) {
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
}
await rejected;
assert.equal(engine.render.request, original);
assert.equal(engine.frames.postRender.numberOfListeners, 0);
assert.equal(engine.fake.callsOf('render.request').length, 6);
});
test('continuous mode and pending tiles cannot masquerade as idle', async (t) => {
for (const condition of ['continuous', 'tiles']) {
await t.test(condition, async (t) => {
const { engine, page, tick } = fixture(t);
if (condition === 'continuous') engine.render.setContinuous(true);
else t.mock.method(engine.surface, 'tilesLoaded', () => false);
const pending = measureIdleRender(page, { quietMs: 100, timeoutMs: 300 });
const rejected = assert.rejects(
pending,
/Readiness timeout: idle render/,
);
tick(300);
await rejected;
assert.equal(engine.fake.callsOf('render.request').length, 0);
});
}
});
test('engine destruction stops the idle observer', async (t) => {
const { engine, page, tick } = fixture(t);
const original = engine.render.request;
const pending = measureIdleRender(page);
const rejected = assert.rejects(pending, /engine changed/);
engine.destroy();
tick(50);
await rejected;
assert.equal(engine.render.request, original);
});
test('renders during the sample stay counted even when demand becomes continuous', async (t) => {
const { engine, page, tick } = fixture(t);
const pending = measureIdleRender(page, { quietMs: 100, sampleMs: 500 });
tick(100);
for (let i = 0; i < 10; i++) {
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
}
const result = await pending;
assert.equal(result.renders, 10);
assert.equal(result.requests['world-overlay'].count, 10);
});
+10 -7
View File
@@ -66,13 +66,18 @@ export async function whenSettled(
) {
const started = Date.now();
try {
await page.waitForFunction(() => window.__godsEyeView?.engine, {
timeout: timeoutMs,
polling: 50,
});
const handle = await page.waitForFunction(
() => {
if (!window.__godsEyeView?.engine) return false;
const cover = globalThis.document?.querySelector('#loading-screen');
return !cover || getComputedStyle(cover).visibility === 'hidden';
},
{ timeout: timeoutMs, polling: 50 },
);
await handle?.dispose();
} catch {
throw new Error(
'Readiness timeout: engineStarted, initialViewRestored, frameDrawn, noFlight, cameraInputsEnabled, cameraPoseStable; observed: inputsEnabled=unknown, following=unknown, cockpitActive=unknown, lastPoseDelta=null',
'Readiness timeout: engineStarted, initialViewRestored, startupUiReady, frameDrawn, noFlight, cameraInputsEnabled, cameraPoseStable; observed: inputsEnabled=unknown, following=unknown, cockpitActive=unknown, lastPoseDelta=null',
);
}
return page.evaluate(
@@ -205,7 +210,6 @@ export async function whenTilesLoaded(page, { timeoutMs = 30000 } = {}) {
() => {
const engine = window.__godsEyeView?.engine;
if (!engine || engine.isDestroyed()) return false;
engine.render.request('qa-tile-readiness');
return engine.surface.tilesLoaded();
},
{ timeout: timeoutMs, polling: 100 },
@@ -217,7 +221,6 @@ export async function whenUiSettled(page) {
const handle = await page.waitForFunction(
() => {
const app = window.__godsEyeView;
app?.engine?.render.request('qa-ui-readiness');
return (
!app?.styleManager?.transitions?.size &&
document
+55
View File
@@ -4,6 +4,7 @@ import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { createSettledState } from '../../src/app/qaProbe.js';
import {
waitForSettled,
whenSettled,
whenFramesDrawn,
whenTilesLoaded,
whenUiSettled,
@@ -252,3 +253,57 @@ test('scenario polling waits for evidence and identifies an unmet condition', as
/broken predicate/,
);
});
test('camera readiness also waits for the startup cover to stop intercepting clicks', async (t) => {
pageEngine(t);
const previous = {
document: globalThis.document,
getComputedStyle: globalThis.getComputedStyle,
};
let visibility = 'visible';
globalThis.document = { querySelector: () => ({}) };
globalThis.getComputedStyle = () => ({ visibility });
t.after(() => {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) delete globalThis[key];
else globalThis[key] = value;
}
});
let disposed = false;
await whenSettled({
async waitForFunction(predicate) {
assert.equal(predicate(), false, 'a started engine still has a cover');
visibility = 'hidden';
assert.equal(predicate(), true);
return {
dispose: async () => {
disposed = true;
},
};
},
async evaluate() {
assert.equal(disposed, true);
},
});
});
test('tile and UI polling never request globe frames', async (t) => {
const { engine } = pageEngine(t);
const previous = globalThis.document;
globalThis.document = { getAnimations: () => [] };
t.after(() => {
if (previous === undefined) delete globalThis.document;
else globalThis.document = previous;
});
t.mock.method(engine.render, 'request', () => {
assert.fail('passive readiness requested a frame');
});
const page = {
async waitForFunction(predicate) {
assert.equal(predicate(), true);
return { dispose: async () => {} };
},
};
await whenTilesLoaded(page);
await whenUiSettled(page);
});
+69
View File
@@ -0,0 +1,69 @@
import { evaluateWithGeo } from './geometry.mjs';
import { whenSettled, whenTilesLoaded } from './ready.mjs';
/** Preserve the oblique fixture's native collision and flight-arc settings. */
export function moveTrafficCamera(page, view, seconds) {
return evaluateWithGeo(
page,
async (geo, view, duration) => {
const { engine, viewer } = window.__godsEyeView;
const native = engine.kind === 'cesium';
const camera = native ? viewer.camera : engine.camera;
// These low-altitude fixture poses must not be lifted by native collision.
if (native)
viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
camera.cancelFlight();
await new Promise((resolve) =>
camera.flyTo({
destination: geo.cartesian3.fromDegrees(
view.lon,
view.lat,
view.height,
),
orientation: {
heading: ((view.heading ?? 0) * Math.PI) / 180,
pitch: ((view.pitch ?? -45) * Math.PI) / 180,
roll: 0,
},
duration,
// An automatic high arc crosses the traffic layer's activation ceiling.
maximumHeight: Math.max(view.height, engine.camera.height()),
complete: resolve,
cancel: resolve,
}),
);
},
view,
seconds,
);
}
/** A quiet loader can still be waiting for its camera-change debounce. */
export async function whenTrafficSettled(page) {
await whenTilesLoaded(page);
await whenSettled(page);
const handle = await page.waitForFunction(
() => {
const { engine, dataManager } = window.__godsEyeView;
const pose = engine.camera.pose();
const viewKey = [
pose.cartographic.latitude,
pose.cartographic.longitude,
pose.cartographic.height / 10000,
pose.heading,
pose.pitch,
]
.map((value) => Number(value || 0).toFixed(7))
.join(',');
const stats = dataManager.layers.get('traffic').module.getStats();
return (
stats.roadBounds?.coverage?.viewKey === viewKey &&
stats.count > 0 &&
!stats.loading &&
!stats.surfacePending
);
},
{ timeout: 60000, polling: 100 },
);
await handle.dispose();
}
+116
View File
@@ -0,0 +1,116 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { moveTrafficCamera, whenTrafficSettled } from './traffic.mjs';
function fixture(t) {
const engine = createFakeEngine({ kind: 'cesium' });
engine.camera.setView({
destination: { x: 6379137, y: 0, z: 0 },
orientation: { heading: 0, pitch: -Math.PI / 12, roll: 0 },
});
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
globalThis.window = { __godsEyeView: { engine } };
globalThis.document = { querySelector: () => null };
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
if (previousDocument === undefined) delete globalThis.document;
else globalThis.document = previousDocument;
engine.destroy();
});
return globalThis.window.__godsEyeView;
}
test('oblique fixture preserves collision and maximum flight height on Cesium', async (t) => {
const app = fixture(t);
let flight;
let cancelled = false;
app.viewer = {
scene: { screenSpaceCameraController: { enableCollisionDetection: true } },
camera: {
cancelFlight() {
cancelled = true;
},
flyTo(options) {
flight = options;
options.complete();
},
},
};
await moveTrafficCamera(
{ evaluate: (fn, ...args) => fn(...args) },
{ lon: 55.037, lat: 24.964, height: 276, pitch: -15 },
1.4,
);
assert.equal(cancelled, true);
assert.equal(
app.viewer.scene.screenSpaceCameraController.enableCollisionDetection,
false,
);
assert.ok(Math.abs(flight.maximumHeight - 1000) < 1e-6);
assert.equal(flight.duration, 1.4);
assert.equal(flight.orientation.pitch, -Math.PI / 12);
});
test('traffic readiness rejects old coverage, pending debounce and partial populations', async (t) => {
const app = fixture(t);
const stats = {
count: 0,
loading: false,
surfacePending: 0,
roadBounds: null,
};
app.dataManager = {
layers: new Map([['traffic', { module: { getStats: () => stats } }]]),
};
let calls = 0;
let disposed = 0;
await whenTrafficSettled({
evaluate: async () => {},
async waitForFunction(predicate) {
if (++calls < 3) assert.equal(predicate(), true);
else {
assert.equal(
predicate(),
false,
'idle before the debounce is not loaded',
);
stats.count = 100;
stats.roadBounds = { coverage: { viewKey: 'old view' } };
assert.equal(predicate(), false, 'old roads do not cover this camera');
stats.roadBounds.coverage.viewKey =
'0.0000000,0.0000000,0.1000000,0.0000000,-0.2617994';
stats.loading = true;
assert.equal(predicate(), false, 'partial road population');
stats.loading = false;
stats.surfacePending = 1;
assert.equal(predicate(), false, 'unresolved surface');
stats.surfacePending = 0;
assert.equal(predicate(), true);
}
return {
dispose: async () => {
disposed++;
},
};
},
});
assert.equal(disposed, 3);
});
test('traffic loading timeouts fail the scenario', async (t) => {
fixture(t);
let calls = 0;
await assert.rejects(
whenTrafficSettled({
evaluate: async () => {},
async waitForFunction() {
if (++calls === 3) throw new Error('traffic loading timeout');
return { dispose: async () => {} };
},
}),
/traffic loading timeout/,
);
});