Measure engine coupling and run the contract suite on the real engines

scripts/report-engine-coupling.mjs counts the engine-specific code left
outside src/globe/cesium and src/globe/navara: layer renderer lines per
engine, Cesium imports, `viewer` mentions, engine-name checks, raw Navara
reads, Navara ground passes, Cesium SDK loads and ScreenSpaceEventHandler
users. engineCoupling.test.mjs fails npm test when a count grows past
scripts/engine-coupling-baseline.json, so each step towards drawing
through the globe contract can only lower them.

scripts/qa-contract-suite.mjs runs src/globe/testing/contractSuite.mjs in
headless Chrome on the Cesium and Navara of a keyless dev server, which
until now only ran on the fake engine. The page imports the suite from the
dev server, starts at a fixed view, waits for the camera to rest and lets
the pointer through the panels. Cesium passes 57 of 57 checks, Navara 64
of 67; docs/CURRENT-STATE.md names the three that fail.
This commit is contained in:
German Khokhlov committed 2026-11-07 01:08:15 +04:00
1 parent 92c8f8d9da
commit 9daa4bd2db
6 files changed
+448 -3

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* Run the globe contract suite (src/globe/testing/contractSuite.mjs) in
* headless Chrome against the real engines of a running app, the way
* contract.test.mjs runs it against the fake engine.
*
* GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4241
* node scripts/qa-contract-suite.mjs http://localhost:4241 [--engine=cesium,navara] [--json=FILE]
*
* Serve the app keyless from the dev server: Google and Cesium ion requests
* are refused here anyway, so neither engine draws photoreal tiles. The page
* imports the suite from the dev server, so it shares the app's modules (an
* engine event is recognised by the module that made it). It gets a real
* pointer (the pick checks) and no map sources (the maps checks would ask for
* photoreal tiles). Exits 1 when any check fails on any engine.
*/
import fs from 'node:fs';
import puppeteer from 'puppeteer';
const BLOCKED_HOSTS =
/(^|\.)(googleapis\.com|google\.com|gstatic\.com|cesium\.com)$/;
const args = process.argv.slice(2);
const origin = args.find((arg) => !arg.startsWith('--'));
const option = (name) =>
args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
if (!origin) {
console.error(
'Usage: node scripts/qa-contract-suite.mjs <origin> [--engine=cesium,navara] [--json=FILE]',
);
process.exit(2);
}
const engines = (option('engine') ?? 'cesium,navara').split(',');
async function runOn(browser, kind) {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
const pageErrors = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
await page.setRequestInterception(true);
page.on('request', (request) => {
const { hostname } = new URL(request.url());
if (BLOCKED_HOSTS.test(hostname)) request.abort();
else request.continue();
});
// A view in the link: the app starts there instead of flying to a city.
await page.goto(
`${origin}/?welcome=0&engine=${kind}#v=2&lat=10&lon=20&alt=1000000&heading=0&pitch=-90&map=osm&l=`,
);
await page.waitForFunction(
(expected) =>
window.__godsEyeView?.engine?.kind === expected &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
// A cold dev server compiles Navara's modules on the first visit.
{ timeout: 300_000, polling: 250 },
kind,
);
// The suite owns the globe: no camera flight running, and the app's panels
// let the pointer through to the canvas.
await page.waitForFunction(
() => !window.__godsEyeView.engine.camera.flightActive(),
{ timeout: 60_000, polling: 250 },
);
await page.addStyleTag({
content:
'* { pointer-events: none !important; } canvas { pointer-events: auto !important; }',
});
// A real left click and pointer move at a canvas position.
await page.exposeFunction('__contractPointer', async (type, x, y) => {
const box = await page.evaluate(() => {
const rect =
window.__godsEyeView.engine.host.canvas.getBoundingClientRect();
return { left: rect.left, top: rect.top };
});
const at = [box.left + x, box.top + y];
if (type === 'click') await page.mouse.click(...at);
else {
await page.mouse.move(at[0] - 1, at[1]);
await page.mouse.move(...at);
}
});
const results = await page.evaluate(async () => {
const { engine } = window.__godsEyeView;
const waitFrames = (count) =>
new Promise((resolve) => {
let left = count;
const remove = engine.frames.postRender.addEventListener(() => {
if (--left > 0) return;
remove();
resolve();
});
engine.render.request('contract-suite');
});
const pointer = {
click: (x, y) => window.__contractPointer('click', x, y),
move: (x, y) => window.__contractPointer('move', x, y),
};
const { runContractSuite } =
await import('/src/globe/testing/contractSuite.mjs');
const outcome = await runContractSuite(engine, {
waitFrames,
pointer,
});
return outcome.map(({ name, ok, error }) => ({
name,
ok,
error: error ? String(error.message ?? error) : undefined,
}));
});
await page.close();
return { engine: kind, results, pageErrors };
}
const browser = await puppeteer.launch({
headless: true,
// A Navara page can hold its main thread for minutes while it starts.
protocolTimeout: 600_000,
args: ['--use-angle=metal', '--enable-gpu', '--no-sandbox'],
});
const report = [];
try {
for (const kind of engines) {
const run = await runOn(browser, kind);
report.push(run);
const failed = run.results.filter(({ ok }) => !ok);
console.log(
`${kind}: ${run.results.length - failed.length}/${run.results.length} checks pass`,
);
for (const { name, error } of failed)
console.log(` FAIL ${name}: ${error}`);
for (const message of run.pageErrors)
console.log(` page error: ${message}`);
}
} finally {
await browser.close();
}
const json = option('json');
if (json) fs.writeFileSync(json, `${JSON.stringify(report, null, 2)}\n`);
process.exit(
report.every(({ results }) => results.every(({ ok }) => ok)) ? 0 : 1,
);