mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
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:
1 parent
2c62e1e378
commit
b5cc4feacb
11 files changed
+480
-51
No files matched your search
@@ -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 },
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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/,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user