mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +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
@@ -2,6 +2,7 @@
|
|||||||
// Cesium-only probes: native DataSource counts and scene-render timing measure the Cesium cable adapter.
|
// Cesium-only probes: native DataSource counts and scene-render timing measure the Cesium cable adapter.
|
||||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||||
import { whenSettled } from './qa/ready.mjs';
|
import { whenSettled } from './qa/ready.mjs';
|
||||||
|
import { measureIdleRender } from './qa/idle.mjs';
|
||||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||||
/**
|
/**
|
||||||
* qa-cables-overlay — submarine-cable label path measurement + contract gate.
|
* qa-cables-overlay — submarine-cable label path measurement + contract gate.
|
||||||
@@ -184,12 +185,8 @@ try {
|
|||||||
: 'scene.render cost, cables ON, 10s orbit', frameCost);
|
: 'scene.render cost, cables ON, 10s orbit', frameCost);
|
||||||
|
|
||||||
// ── d. parked idle honesty (labels must not force continuous render) ──
|
// ── d. parked idle honesty (labels must not force continuous render) ──
|
||||||
await whenSettled(page); // orbit stop + fades settle
|
await whenSettled(page);
|
||||||
const idle = await page.evaluate(() => new Promise((resolve) => {
|
const idle = await measureIdleRender(page);
|
||||||
let renders = 0;
|
|
||||||
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => { renders += 1; });
|
|
||||||
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
|
|
||||||
}));
|
|
||||||
report(control
|
report(control
|
||||||
? 'parked idle, control (postRender fires / 5s)'
|
? 'parked idle, control (postRender fires / 5s)'
|
||||||
: 'parked idle with cables ON (postRender fires / 5s)', idle);
|
: 'parked idle with cables ON (postRender fires / 5s)', idle);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Cesium-only probes: native primitive inspection independently verifies the Cesium route geometry.
|
// Cesium-only probes: native primitive inspection independently verifies the Cesium route geometry.
|
||||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
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,
|
* Browser proof of the Directions layer: real chip clicks, real globe clicks,
|
||||||
* a real route from the real proxy, a real camera flight.
|
* a real route from the real proxy, a real camera flight.
|
||||||
@@ -779,7 +779,14 @@ try {
|
|||||||
if (placed) {
|
if (placed) {
|
||||||
await waitForRoute();
|
await waitForRoute();
|
||||||
// Let the shared ground-floor cells warm and the dots re-anchor.
|
// 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 denver = await evaluateWithGeo(page, async (qaGeo) => {
|
||||||
const Cartographic = qaGeo.cartographic;
|
const Cartographic = qaGeo.cartographic;
|
||||||
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
|
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/** Persistent inline attribution through actual OSM layer display lifetimes. */
|
/** 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 { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
@@ -53,6 +53,7 @@ try {
|
|||||||
);
|
);
|
||||||
const shot = async (name, osm, tiles) => {
|
const shot = async (name, osm, tiles) => {
|
||||||
await whenSettled(page);
|
await whenSettled(page);
|
||||||
|
await whenUiSettled(page);
|
||||||
const state = await page.$eval('#cesium-credits', (e) => {
|
const state = await page.$eval('#cesium-credits', (e) => {
|
||||||
const nodes = [
|
const nodes = [
|
||||||
...e.querySelectorAll('a,img,.cesium-credit-expand-link'),
|
...e.querySelectorAll('a,img,.cesium-credit-expand-link'),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { evaluateWithGeo } from './qa/geometry.mjs';
|
|||||||
* FAIL. Commits nothing.
|
* FAIL. Commits nothing.
|
||||||
*/
|
*/
|
||||||
import { whenSettled } from './qa/ready.mjs';
|
import { whenSettled } from './qa/ready.mjs';
|
||||||
|
import { measureIdleRender } from './qa/idle.mjs';
|
||||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -297,27 +298,7 @@ try {
|
|||||||
|
|
||||||
// ── parked-idle honesty: the LOD walk must not force continuous render ─
|
// ── parked-idle honesty: the LOD walk must not force continuous render ─
|
||||||
await whenSettled(page);
|
await whenSettled(page);
|
||||||
const idle = await page.evaluate(async () => {
|
const idle = await measureIdleRender(page);
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
report(CONTROL
|
report(CONTROL
|
||||||
? 'parked idle, control (postRender fires / 5s)'
|
? 'parked idle, control (postRender fires / 5s)'
|
||||||
: 'parked idle with infra ON (postRender fires / 5s)', idle);
|
: 'parked idle with infra ON (postRender fires / 5s)', idle);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */
|
/** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */
|
||||||
import { whenSettled } from './qa/ready.mjs';
|
import { whenSettled } from './qa/ready.mjs';
|
||||||
|
import {
|
||||||
|
moveTrafficCamera as moveCamera,
|
||||||
|
whenTrafficSettled,
|
||||||
|
} from './qa/traffic.mjs';
|
||||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import {
|
import {
|
||||||
moveCamera,
|
|
||||||
createScreencast,
|
createScreencast,
|
||||||
installFrameProbe,
|
installFrameProbe,
|
||||||
} from './qa-journey-recorder.mjs';
|
} from './qa-journey-recorder.mjs';
|
||||||
@@ -96,19 +99,7 @@ try {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const settle = async () => {
|
const settle = async () => {
|
||||||
await whenSettled(page);
|
await whenTrafficSettled(page);
|
||||||
await page
|
|
||||||
.waitForFunction(
|
|
||||||
() => {
|
|
||||||
const s = window.__godsEyeView.dataManager.layers
|
|
||||||
.get('traffic')
|
|
||||||
.module.getStats();
|
|
||||||
return !s.loading;
|
|
||||||
},
|
|
||||||
{ timeout: 15000 },
|
|
||||||
)
|
|
||||||
.catch(() => {});
|
|
||||||
await whenSettled(page);
|
|
||||||
return snap();
|
return snap();
|
||||||
};
|
};
|
||||||
for (const [name, lat, lon, height] of [
|
for (const [name, lat, lon, height] of [
|
||||||
@@ -306,6 +297,11 @@ try {
|
|||||||
fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2));
|
fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2));
|
||||||
await closeBrowser(browser);
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
failures: result.failures,
|
||||||
|
errors: result.errors,
|
||||||
|
forbidden: result.forbidden,
|
||||||
|
}));
|
||||||
if (
|
if (
|
||||||
!baseline &&
|
!baseline &&
|
||||||
(result.failures.length || result.errors.length || result.forbidden.length)
|
(result.failures.length || result.errors.length || result.forbidden.length)
|
||||||
|
|||||||
@@ -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();
|
const started = Date.now();
|
||||||
try {
|
try {
|
||||||
await page.waitForFunction(() => window.__godsEyeView?.engine, {
|
const handle = await page.waitForFunction(
|
||||||
timeout: timeoutMs,
|
() => {
|
||||||
polling: 50,
|
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 {
|
} catch {
|
||||||
throw new Error(
|
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(
|
return page.evaluate(
|
||||||
@@ -205,7 +210,6 @@ export async function whenTilesLoaded(page, { timeoutMs = 30000 } = {}) {
|
|||||||
() => {
|
() => {
|
||||||
const engine = window.__godsEyeView?.engine;
|
const engine = window.__godsEyeView?.engine;
|
||||||
if (!engine || engine.isDestroyed()) return false;
|
if (!engine || engine.isDestroyed()) return false;
|
||||||
engine.render.request('qa-tile-readiness');
|
|
||||||
return engine.surface.tilesLoaded();
|
return engine.surface.tilesLoaded();
|
||||||
},
|
},
|
||||||
{ timeout: timeoutMs, polling: 100 },
|
{ timeout: timeoutMs, polling: 100 },
|
||||||
@@ -217,7 +221,6 @@ export async function whenUiSettled(page) {
|
|||||||
const handle = await page.waitForFunction(
|
const handle = await page.waitForFunction(
|
||||||
() => {
|
() => {
|
||||||
const app = window.__godsEyeView;
|
const app = window.__godsEyeView;
|
||||||
app?.engine?.render.request('qa-ui-readiness');
|
|
||||||
return (
|
return (
|
||||||
!app?.styleManager?.transitions?.size &&
|
!app?.styleManager?.transitions?.size &&
|
||||||
document
|
document
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
|||||||
import { createSettledState } from '../../src/app/qaProbe.js';
|
import { createSettledState } from '../../src/app/qaProbe.js';
|
||||||
import {
|
import {
|
||||||
waitForSettled,
|
waitForSettled,
|
||||||
|
whenSettled,
|
||||||
whenFramesDrawn,
|
whenFramesDrawn,
|
||||||
whenTilesLoaded,
|
whenTilesLoaded,
|
||||||
whenUiSettled,
|
whenUiSettled,
|
||||||
@@ -252,3 +253,57 @@ test('scenario polling waits for evidence and identifies an unmet condition', as
|
|||||||
/broken predicate/,
|
/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