mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +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.
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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