mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Merge branch 'german/refactor-e12-script-regressions' into german/navara-refactor
This commit is contained in:
commit
555906da6b
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