From 3e261d1bafa9a4f99e3b749bac2f3c4a190dadb3 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 7 Nov 2026 02:39:02 +0400 Subject: [PATCH] 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. --- docs/CURRENT-STATE.md | 14 ++--- scripts/qa-contract-suite.mjs | 105 ++++++++++++++++++++-------------- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 4366d4904..7bbbfc41a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -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 ` 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 diff --git a/scripts/qa-contract-suite.mjs b/scripts/qa-contract-suite.mjs index 6721a30db..6677dea0d 100644 --- a/scripts/qa-contract-suite.mjs +++ b/scripts/qa-contract-suite.mjs @@ -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 [--engine=cesium,navara] [--json=FILE]', + 'Usage: node scripts/qa-contract-suite.mjs [--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 }; }