Give the contract suite a clear pointer path and real canvases in the app

In the app the world overlay draws on a canvas of its own above the
globe, so letting every canvas take the pointer sent the suite's clicks
there. Only the engine's canvas takes it now. Without makeCanvas the
pick check's sprite atlas was a plain object no real engine draws, so its
sprites could not be picked; the page now hands the suite real 2D
canvases. --only=prefix,... runs a subset of the checks. With both, Cesium
passes 63 of 63 checks and Navara 66 of 66.
This commit is contained in:
German Khokhlov committed 2026-11-07 02:39:02 +04:00
1 parent 35e70ec280
commit 3e261d1baf
2 files changed
+68 -51

No files matched your search

+6 -8
View File
@@ -181,14 +181,12 @@ one application, and only the globe-bound parts move to the next engine.
in for tests, and `src/globe/testing/contractSuite.mjs` holds the checks in for tests, and `src/globe/testing/contractSuite.mjs` holds the checks
every engine must pass. `npm test` runs them on the fake engine; every engine must pass. `npm test` runs them on the fake engine;
`node scripts/qa-contract-suite.mjs <origin>` runs them in headless Chrome `node scripts/qa-contract-suite.mjs <origin>` runs them in headless Chrome
on the real Cesium and Navara of a keyless dev server. On November 7, 2026 on the real Cesium and Navara of a keyless dev server, with a real pointer
Cesium passed 57 of 57 and Navara 64 of 67: a resting move raised no and real canvases; checks of state the app drives itself (contact sonar)
`moveEnd`, a click payload never came, and contact sonar lost an angle-only are skipped and named. On November 7, 2026 Cesium passed 63 of 63 and
change inside the app. `movers` and `lines` are required of every engine: Navara 66 of 66. `movers` and `lines` are required of every engine: Cesium
Cesium draws them with billboards, points, models and polyline primitives draws them with billboards, points, models and polyline primitives
(`src/globe/cesium/movers.js`, `lines.js`). The 57 Cesium checks above ran (`src/globe/cesium/movers.js`, `lines.js`).
without them; in Node their checks pass on Cesium without pixels or a
pointer, and the browser run with them is still to come.
- **Engine coupling.** `node scripts/report-engine-coupling.mjs` counts the - **Engine coupling.** `node scripts/report-engine-coupling.mjs` counts the
engine-specific code outside the engines: renderer lines per engine, Cesium engine-specific code outside the engines: renderer lines per engine, Cesium
imports, `viewer` mentions, engine-name checks, raw Navara reads, Navara imports, `viewer` mentions, engine-name checks, raw Navara reads, Navara
+62 -43
View File
@@ -11,8 +11,8 @@
* are refused here anyway, so neither engine draws photoreal tiles. The page * 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 * 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 * 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 * pointer (the pick checks), real 2D canvases for sprite atlases, and no map
* photoreal tiles). Exits 1 when any check fails on any engine. * sources (the maps checks would ask for photoreal tiles). Exits 1 when any check fails on any engine.
*/ */
import fs from 'node:fs'; import fs from 'node:fs';
import puppeteer from 'puppeteer'; import puppeteer from 'puppeteer';
@@ -26,11 +26,13 @@ const option = (name) =>
args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3); args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
if (!origin) { if (!origin) {
console.error( console.error(
'Usage: node scripts/qa-contract-suite.mjs <origin> [--engine=cesium,navara] [--json=FILE]', 'Usage: node scripts/qa-contract-suite.mjs <origin> [--engine=cesium,navara] [--only=prefix,...] [--json=FILE]',
); );
process.exit(2); process.exit(2);
} }
const engines = (option('engine') ?? 'cesium,navara').split(','); const engines = (option('engine') ?? 'cesium,navara').split(',');
// --only=prefix,prefix runs just the checks whose names start with one.
const only = option('only')?.split(',') ?? null;
// Checks of state the running app drives itself, by name prefix, with the // Checks of state the running app drives itself, by name prefix, with the
// owner. The app sets contact sonar on every frame (src/cyberSonarEngine.js), // owner. The app sets contact sonar on every frame (src/cyberSonarEngine.js),
@@ -68,9 +70,17 @@ async function runOn(browser, kind) {
() => !window.__godsEyeView.engine.camera.flightActive(), () => !window.__godsEyeView.engine.camera.flightActive(),
{ timeout: 60_000, polling: 250 }, { timeout: 60_000, polling: 250 },
); );
// Only the engine's canvas takes the pointer: the world overlay draws on a
// canvas of its own above it.
await page.evaluate(() =>
window.__godsEyeView.engine.host.canvas.setAttribute(
'data-contract-target',
'',
),
);
await page.addStyleTag({ await page.addStyleTag({
content: content:
'* { pointer-events: none !important; } canvas { pointer-events: auto !important; }', '* { pointer-events: none !important; } [data-contract-target] { pointer-events: auto !important; }',
}); });
// A real left click and pointer move at a canvas position. // A real left click and pointer move at a canvas position.
await page.exposeFunction('__contractPointer', async (type, x, y) => { await page.exposeFunction('__contractPointer', async (type, x, y) => {
@@ -86,47 +96,56 @@ async function runOn(browser, kind) {
await page.mouse.move(...at); await page.mouse.move(...at);
} }
}); });
const results = await page.evaluate(async (appOwned) => { const results = await page.evaluate(
const { engine } = window.__godsEyeView; async (appOwned, only) => {
const waitFrames = (count) => const { engine } = window.__godsEyeView;
new Promise((resolve) => { const waitFrames = (count) =>
let left = count; new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => { let left = count;
if (--left > 0) return; const remove = engine.frames.postRender.addEventListener(() => {
remove(); if (--left > 0) return;
resolve(); remove();
resolve();
});
engine.render.request('contract-suite');
}); });
engine.render.request('contract-suite'); const pointer = {
}); click: (x, y) => window.__contractPointer('click', x, y),
const pointer = { move: (x, y) => window.__contractPointer('move', x, y),
click: (x, y) => window.__contractPointer('click', x, y), };
move: (x, y) => window.__contractPointer('move', x, y), const { createContractSuite } =
}; await import('/src/globe/testing/contractSuite.mjs');
const { createContractSuite } = const outcome = [];
await import('/src/globe/testing/contractSuite.mjs'); // A real 2D canvas: a sprite atlas the engines can draw and pick.
const outcome = []; const makeCanvas = (width, height) =>
for (const { name, run } of createContractSuite(engine, { Object.assign(document.createElement('canvas'), { width, height });
waitFrames, for (const { name, run } of createContractSuite(engine, {
pointer, waitFrames,
})) { pointer,
const owner = appOwned.find(([prefix]) => name.startsWith(prefix)); makeCanvas,
if (owner) { })) {
outcome.push({ name, skipped: owner[1] }); if (only && !only.some((prefix) => name.startsWith(prefix))) continue;
continue; const owner = appOwned.find(([prefix]) => name.startsWith(prefix));
if (owner) {
outcome.push({ name, skipped: owner[1] });
continue;
}
try {
await run();
outcome.push({ name, ok: true });
} catch (error) {
outcome.push({
name,
ok: false,
error: String(error?.message ?? error),
});
}
} }
try { return outcome;
await run(); },
outcome.push({ name, ok: true }); APP_OWNED,
} catch (error) { only,
outcome.push({ );
name,
ok: false,
error: String(error?.message ?? error),
});
}
}
return outcome;
}, APP_OWNED);
await page.close(); await page.close();
return { engine: kind, results, pageErrors }; return { engine: kind, results, pageErrors };
} }