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
every engine must pass. `npm test` runs them on the fake engine;
`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
Cesium passed 57 of 57 and Navara 64 of 67: a resting move raised no
`moveEnd`, a click payload never came, and contact sonar lost an angle-only
change inside the app. `movers` and `lines` are required of every engine:
Cesium draws them with billboards, points, models and polyline primitives
(`src/globe/cesium/movers.js`, `lines.js`). The 57 Cesium checks above ran
without them; in Node their checks pass on Cesium without pixels or a
pointer, and the browser run with them is still to come.
on the real Cesium and Navara of a keyless dev server, with a real pointer
and real canvases; checks of state the app drives itself (contact sonar)
are skipped and named. On November 7, 2026 Cesium passed 63 of 63 and
Navara 66 of 66. `movers` and `lines` are required of every engine: Cesium
draws them with billboards, points, models and polyline primitives
(`src/globe/cesium/movers.js`, `lines.js`).
- **Engine coupling.** `node scripts/report-engine-coupling.mjs` counts the
engine-specific code outside the engines: renderer lines per engine, Cesium
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
* 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.
* pointer (the pick checks), real 2D canvases for sprite atlases, 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';
@@ -26,11 +26,13 @@ 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]',
'Usage: node scripts/qa-contract-suite.mjs <origin> [--engine=cesium,navara] [--only=prefix,...] [--json=FILE]',
);
process.exit(2);
}
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
// 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(),
{ 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({
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.
await page.exposeFunction('__contractPointer', async (type, x, y) => {
@@ -86,47 +96,56 @@ async function runOn(browser, kind) {
await page.mouse.move(...at);
}
});
const results = await page.evaluate(async (appOwned) => {
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();
const results = await page.evaluate(
async (appOwned, only) => {
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');
});
engine.render.request('contract-suite');
});
const pointer = {
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 outcome = [];
for (const { name, run } of createContractSuite(engine, {
waitFrames,
pointer,
})) {
const owner = appOwned.find(([prefix]) => name.startsWith(prefix));
if (owner) {
outcome.push({ name, skipped: owner[1] });
continue;
const pointer = {
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 outcome = [];
// A real 2D canvas: a sprite atlas the engines can draw and pick.
const makeCanvas = (width, height) =>
Object.assign(document.createElement('canvas'), { width, height });
for (const { name, run } of createContractSuite(engine, {
waitFrames,
pointer,
makeCanvas,
})) {
if (only && !only.some((prefix) => name.startsWith(prefix))) 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 {
await run();
outcome.push({ name, ok: true });
} catch (error) {
outcome.push({
name,
ok: false,
error: String(error?.message ?? error),
});
}
}
return outcome;
}, APP_OWNED);
return outcome;
},
APP_OWNED,
only,
);
await page.close();
return { engine: kind, results, pageErrors };
}