mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Navara now matches Cesium where the review found it diverging: a flight holds the camera inputs, camera.changed counts turns as Cesium does, render requests made during a frame survive it, follow pinches zoom, ground widths follow the pixel ratio and Cyber sonar reaches glyph caches and models. On Cesium, a staged imagery swap keeps loading tiles, setView replaces a running flight, and a cleared weather detail goes away. The contract gains MoverSet.failed, maps.terrainRevision, render.maxTextureSize and surface.sampleTerrainMostDetailed on every engine, so layers retry failed models, key road heights by terrain, size weather textures to the GPU and place Bhote Koshi cards on detailed terrain. Engine switches keep the live view, follows, focus, layer options and cards: a pick hands over the exact pose, applies no address options and pans no carried-over card; a history visit still applies its entry. Layers put GhostMaps pins, cable landings, OSM Places and directions on the measured surface without per-frame sampling, draw the Cockpit contact dot, hide disabled CCTV on attach, and keep ALPR running outside secure contexts. KML imports draw raised polygon fills, rotated and coloured ground overlays and north-referenced icon headings, and report files with nothing to draw. QA scripts read the contract instead of Cesium internals (test:track is 109/109 again), wait for the camera to rest before clicking, and give cold switches and parked engines realistic time. Docs follow the code.
288 lines
10 KiB
JavaScript
288 lines
10 KiB
JavaScript
// Contract: failure injection in both directions, cold and warm, and a slow
|
|
// first frame. Each case starts on a fresh page with a session (earthquakes
|
|
// and flights, one aircraft selected) and switches with real input on the
|
|
// picker.
|
|
//
|
|
// - A failed switch rolls back to the engine that was running, with the
|
|
// same session (manager, every layer model, enabled layers, selection),
|
|
// the address still naming it and no history entry; an ordinary pick
|
|
// afterwards succeeds and adds exactly one entry.
|
|
// cold: the incoming engine cannot get a WebGL context;
|
|
// warm: the parked incoming engine fails to resume.
|
|
// - A first frame slower than 10 s (animation frames held for 12 s) does not
|
|
// roll back: the switch completes, the cover goes after the 10 s wait, one
|
|
// history entry, the session intact, and the frame arrives.
|
|
//
|
|
// node scripts/qa-strict-failure-matrix.mjs http://127.0.0.1:4208
|
|
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createStrict,
|
|
localPreview,
|
|
waitForEngine,
|
|
} from './qa-strict-harness.mjs';
|
|
import {
|
|
assertSession,
|
|
installFirstFrameProbe,
|
|
installSessionFixtures,
|
|
openApp,
|
|
rememberEngine,
|
|
SESSION_HASH,
|
|
sessionState,
|
|
startSession,
|
|
} from './qa-strict-session.mjs';
|
|
|
|
const base = localPreview(process.argv[2]);
|
|
const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
|
|
const strict = createStrict({ origin: base.origin, layers: [] });
|
|
const results = [];
|
|
strict.extra.contracts = results;
|
|
await strict.run({}, async (b) => {
|
|
// A fresh page on `start` with the session; `warm` visits the other
|
|
// engine first so it is parked.
|
|
const open = async (start, warm) => {
|
|
const p = await b.newPage();
|
|
await p.setViewport({ width: 1280, height: 800 });
|
|
p.errors = [];
|
|
p.on('pageerror', (e) => p.errors.push(e.message));
|
|
await installSessionFixtures(p, { flights: 20 });
|
|
await installFirstFrameProbe(p);
|
|
// Flights on with the first fixture aircraft tracked (the address's
|
|
// layer options), so the session carries a selection.
|
|
await openApp(
|
|
p,
|
|
base.origin,
|
|
start,
|
|
SESSION_HASH.replace(/&l=$/, '&l=f&lo=f.e.1_f.t.a00000'),
|
|
);
|
|
await startSession(p, ['earthquakes', 'flights']);
|
|
await p.waitForFunction(
|
|
() =>
|
|
window.__godsEyeView.styleManager.getAircraftTrackingTarget()?.id ===
|
|
'a00000',
|
|
{ timeout: 30000 },
|
|
);
|
|
if (warm) {
|
|
await strict.pick(p, other(start));
|
|
await waitForEngine(p, other(start));
|
|
await rememberEngine(p);
|
|
await strict.pick(p, start);
|
|
await waitForEngine(p, start);
|
|
await rememberEngine(p);
|
|
// The warm-up's Navara→Cesium switch drops the tracked aircraft (a
|
|
// defect of its own, reported); track it
|
|
// again so this case tests the failure path with a selection.
|
|
await p.evaluate(() =>
|
|
window.__godsEyeView.dataManager.layers
|
|
.get('flights')
|
|
.module.trackById('a00000', { origin: 'user' }),
|
|
);
|
|
await p.waitForFunction(
|
|
() =>
|
|
window.__godsEyeView.styleManager.getAircraftTrackingTarget()?.id ===
|
|
'a00000',
|
|
{ timeout: 30000 },
|
|
);
|
|
}
|
|
return p;
|
|
};
|
|
const selection = (p) =>
|
|
p.evaluate(
|
|
() =>
|
|
window.__godsEyeView?.styleManager.getAircraftTrackingTarget()?.id ??
|
|
null,
|
|
);
|
|
|
|
for (const [start, temperature] of [
|
|
['navara', 'cold'],
|
|
['cesium', 'cold'],
|
|
['cesium', 'warm'],
|
|
['navara', 'warm'],
|
|
]) {
|
|
const target = other(start);
|
|
const label = `${temperature} ${start}→${target} failure`;
|
|
const p = await open(start, temperature === 'warm');
|
|
const before = await sessionState(p);
|
|
const selected = await selection(p);
|
|
if (temperature === 'cold')
|
|
await p.evaluate(() => {
|
|
const original = HTMLCanvasElement.prototype.getContext;
|
|
HTMLCanvasElement.prototype.getContext = function (type, ...args) {
|
|
// The running globe already holds its context; only a new
|
|
// globe asks for one.
|
|
if (/webgl/i.test(String(type))) {
|
|
window.__qaRefused = (window.__qaRefused ?? 0) + 1;
|
|
throw new Error('QA: no WebGL context for the new globe');
|
|
}
|
|
return original.call(this, type, ...args);
|
|
};
|
|
window.__qaRestore = () => {
|
|
HTMLCanvasElement.prototype.getContext = original;
|
|
};
|
|
});
|
|
else
|
|
await p.evaluate((target) => {
|
|
const parked = window.__qaEngines.get(target);
|
|
const request = parked.render.request;
|
|
parked.render.request = function (reason, ...args) {
|
|
if (reason === 'engine-resume' && !window.__qaRestored) {
|
|
window.__qaRefused = (window.__qaRefused ?? 0) + 1;
|
|
throw new Error('QA: the parked globe failed to resume');
|
|
}
|
|
return request.call(this, reason, ...args);
|
|
};
|
|
window.__qaRestore = () => {
|
|
window.__qaRestored = true;
|
|
};
|
|
}, target);
|
|
await strict.pick(p, target);
|
|
await waitForEngine(p, start);
|
|
const refused = await p.evaluate(() => window.__qaRefused ?? 0);
|
|
const rolled = await assertSession(p, start, label + ': rollback');
|
|
// The tracked aircraft, given up to 10 s to be re-acquired.
|
|
const kept = await p
|
|
.waitForFunction(
|
|
(id) =>
|
|
window.__godsEyeView?.styleManager.getAircraftTrackingTarget()?.id ===
|
|
id,
|
|
{ timeout: 10000, polling: 100 },
|
|
selected,
|
|
)
|
|
.then(() => selected)
|
|
.catch(() => selection(p));
|
|
await p.evaluate(() => window.__qaRestore());
|
|
// An ordinary pick afterwards.
|
|
await strict.pick(p, target);
|
|
await waitForEngine(p, target);
|
|
const retried = await assertSession(p, target, label + ': retry');
|
|
const afterRetry = await p
|
|
.waitForFunction(
|
|
(id) =>
|
|
window.__godsEyeView?.styleManager.getAircraftTrackingTarget()?.id ===
|
|
id,
|
|
{ timeout: 10000, polling: 100 },
|
|
selected,
|
|
)
|
|
.then(() => selected)
|
|
.catch(() => selection(p));
|
|
const row = {
|
|
case: label,
|
|
refused,
|
|
rollbackTarget: rolled.engine,
|
|
historyAfterFailure: rolled.history - before.history,
|
|
selectionKept: kept === selected,
|
|
selectionAfterRollback: kept,
|
|
historyAfterRetry: retried.history - before.history,
|
|
selectionAfterRetry: afterRetry,
|
|
errors: p.errors.length,
|
|
};
|
|
row.failed = [
|
|
refused > 0 || 'the injected failure did not happen',
|
|
row.rollbackTarget === start || 'rolled back elsewhere',
|
|
row.historyAfterFailure === 0 || 'the failed switch changed the history',
|
|
selected === 'a00000' || 'no tracked aircraft before the failure',
|
|
row.selectionKept || 'the tracked aircraft is lost after the rollback',
|
|
row.historyAfterRetry === 1 || 'the retry did not add one entry',
|
|
row.selectionAfterRetry === selected ||
|
|
'the tracked aircraft is lost after the retry',
|
|
].filter((v) => v !== true);
|
|
results.push(row);
|
|
console.log(row.failed.length ? 'FAIL' : 'PASS', JSON.stringify(row));
|
|
await p.close();
|
|
}
|
|
|
|
// Slow first frame: animation frames held for 12 s from the person's
|
|
// input (a stalled GPU or compositor), cold and warm.
|
|
for (const [start, temperature] of [
|
|
['cesium', 'cold'],
|
|
['navara', 'warm'],
|
|
]) {
|
|
const target = other(start);
|
|
const label = `${temperature} ${start}→${target} first frame after 12 s`;
|
|
const p = await open(start, temperature === 'warm');
|
|
const before = await sessionState(p);
|
|
await p.evaluate((target) => {
|
|
window.__qaSlow = { frame: window.__qaFirstFrame(target) };
|
|
window.__qaSlow.frame.then((t) => (window.__qaSlow.drawn = t));
|
|
const select = document.querySelector('#globe-engine');
|
|
// From the change on, every animation frame waits until 12 s after it.
|
|
select.addEventListener(
|
|
'change',
|
|
() => {
|
|
const until = performance.now() + 12000;
|
|
window.__qaSlow.at = performance.now();
|
|
const raf = window.requestAnimationFrame.bind(window);
|
|
window.requestAnimationFrame = (callback) =>
|
|
raf((time) => {
|
|
const wait = until - performance.now();
|
|
if (wait <= 0) return callback(time);
|
|
setTimeout(() => raf(callback), wait);
|
|
});
|
|
window.__qaSlow.release = () => {
|
|
window.requestAnimationFrame = raf;
|
|
};
|
|
},
|
|
{ once: true, capture: true },
|
|
);
|
|
new MutationObserver((_, observer) => {
|
|
if (
|
|
window.__qaSlow.at &&
|
|
!window.__qaSlow.coverGone &&
|
|
!document.querySelector('.engine-switch-frame') &&
|
|
!document.querySelector(
|
|
'#globe-engine:is([disabled], [aria-busy="true"])',
|
|
)
|
|
) {
|
|
window.__qaSlow.coverGone = performance.now();
|
|
observer.disconnect();
|
|
}
|
|
}).observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['disabled'],
|
|
});
|
|
}, target);
|
|
await strict.pick(p, target, { settle: false });
|
|
await p.waitForFunction(
|
|
(target) =>
|
|
window.__godsEyeView?.engine?.kind === target &&
|
|
!document.querySelector(
|
|
'#globe-engine:is([disabled], [aria-busy="true"])',
|
|
) &&
|
|
!document.querySelector('.engine-switch-frame'),
|
|
{ polling: 200, timeout: 60000 },
|
|
target,
|
|
);
|
|
const done = await assertSession(p, target, label);
|
|
const timing = await p.evaluate(async () => {
|
|
const frame = await window.__qaSlow.frame;
|
|
return {
|
|
coverGoneMs: Math.round(window.__qaSlow.coverGone - window.__qaSlow.at),
|
|
firstFrameMs: Math.round(frame.presented - window.__qaSlow.at),
|
|
};
|
|
});
|
|
const row = {
|
|
case: label,
|
|
engine: done.engine,
|
|
history: done.history - before.history,
|
|
...timing,
|
|
errors: p.errors.length,
|
|
};
|
|
results.push(row);
|
|
assert.equal(row.history, 1, 'one history entry, no rollback');
|
|
assert.ok(
|
|
row.coverGoneMs >= 9500 && row.coverGoneMs < 12500,
|
|
'the cover goes after the 10 s first-frame wait ' + JSON.stringify(row),
|
|
);
|
|
assert.ok(row.firstFrameMs >= 12000, 'the frame arrives after the stall');
|
|
console.log('PASS', JSON.stringify(row));
|
|
await p.close();
|
|
}
|
|
assert.deepEqual(
|
|
results.filter((row) => row.failed?.length),
|
|
[],
|
|
'failure-injection cases that broke the contract',
|
|
);
|
|
});
|