Merge branch 'german/refactor-e12-script-regressions' into german/navara-refactor

This commit is contained in:
German Khokhlov committed 2026-11-10 20:58:16 +04:00
commit 555906da6b
11 files changed
+480 -51

No files matched your search

+3 -6
View File
@@ -2,6 +2,7 @@
// Cesium-only probes: native DataSource counts and scene-render timing measure the Cesium cable adapter.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled } from './qa/ready.mjs';
import { measureIdleRender } from './qa/idle.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
/**
* qa-cables-overlay — submarine-cable label path measurement + contract gate.
@@ -184,12 +185,8 @@ try {
: 'scene.render cost, cables ON, 10s orbit', frameCost);
// ── d. parked idle honesty (labels must not force continuous render) ──
await whenSettled(page); // orbit stop + fades settle
const idle = await page.evaluate(() => new Promise((resolve) => {
let renders = 0;
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => { renders += 1; });
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
}));
await whenSettled(page);
const idle = await measureIdleRender(page);
report(control
? 'parked idle, control (postRender fires / 5s)'
: 'parked idle with cables ON (postRender fires / 5s)', idle);
+9 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
// Cesium-only probes: native primitive inspection independently verifies the Cesium route geometry.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled, whenUiSettled, whenTilesLoaded } from './qa/ready.mjs';
import { whenSettled, whenUiSettled, pollPage } from './qa/ready.mjs';
/**
* Browser proof of the Directions layer: real chip clicks, real globe clicks,
* a real route from the real proxy, a real camera flight.
@@ -779,7 +779,14 @@ try {
if (placed) {
await waitForRoute();
// Let the shared ground-floor cells warm and the dots re-anchor.
await whenTilesLoaded(page);
await pollPage(
page,
() => {
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
return stepCount > 0 && stepsShown.length === stepCount;
},
{ timeoutMs: 60_000 },
);
const denver = await evaluateWithGeo(page, async (qaGeo) => {
const Cartographic = qaGeo.cartographic;
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
+2 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/** Persistent inline attribution through actual OSM layer display lifetimes. */
import { whenSettled } from './qa/ready.mjs';
import { whenSettled, whenUiSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
@@ -53,6 +53,7 @@ try {
);
const shot = async (name, osm, tiles) => {
await whenSettled(page);
await whenUiSettled(page);
const state = await page.$eval('#cesium-credits', (e) => {
const nodes = [
...e.querySelectorAll('a,img,.cesium-credit-expand-link'),
+2 -21
View File
@@ -27,6 +27,7 @@ import { evaluateWithGeo } from './qa/geometry.mjs';
* FAIL. Commits nothing.
*/
import { whenSettled } from './qa/ready.mjs';
import { measureIdleRender } from './qa/idle.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
@@ -297,27 +298,7 @@ try {
// ── parked-idle honesty: the LOD walk must not force continuous render ─
await whenSettled(page);
const idle = await page.evaluate(async () => {
// The debug handle's own governor read (no /src import: runs on a build).
const { getRenderGovernorDiagnostics } = window.__godsEyeView;
return new Promise((resolve) => {
const startedAt = Date.now();
const tilesLoadedBefore = window.__godsEyeView.engine.surface.tilesLoaded();
let renders = 0;
let framesWithPendingTiles = 0;
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
renders += 1;
if (!window.__godsEyeView.engine.surface.tilesLoaded()) framesWithPendingTiles += 1;
});
setTimeout(() => {
remove();
const governor = getRenderGovernorDiagnostics();
resolve({ renders, tilesLoadedBefore, tilesLoadedAfter: window.__godsEyeView.engine.surface.tilesLoaded(),
framesWithPendingTiles, mode: governor.mode, holds: governor.holds,
requests: governor.recentRequests.filter(request => request.at >= startedAt) });
}, 5_000);
});
});
const idle = await measureIdleRender(page);
report(CONTROL
? 'parked idle, control (postRender fires / 5s)'
: 'parked idle with infra ON (postRender fires / 5s)', idle);
+10 -14
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env node
/** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */
import { whenSettled } from './qa/ready.mjs';
import {
moveTrafficCamera as moveCamera,
whenTrafficSettled,
} from './qa/traffic.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import {
moveCamera,
createScreencast,
installFrameProbe,
} from './qa-journey-recorder.mjs';
@@ -96,19 +99,7 @@ try {
};
});
const settle = async () => {
await whenSettled(page);
await page
.waitForFunction(
() => {
const s = window.__godsEyeView.dataManager.layers
.get('traffic')
.module.getStats();
return !s.loading;
},
{ timeout: 15000 },
)
.catch(() => {});
await whenSettled(page);
await whenTrafficSettled(page);
return snap();
};
for (const [name, lat, lon, height] of [
@@ -306,6 +297,11 @@ try {
fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2));
await closeBrowser(browser);
}
console.log(JSON.stringify({
failures: result.failures,
errors: result.errors,
forbidden: result.forbidden,
}));
if (
!baseline &&
(result.failures.length || result.errors.length || result.forbidden.length)
+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/,
);
});