Move QA scripts to the shared harness, batch 2 of 4

Each script launches and closes Chrome through scripts/qa/browser.mjs,
waits on the application through scripts/qa/ready.mjs instead of the
loading screen and fixed pauses, and reads the contract instead of
Cesium where the check is not about Cesium itself.
This commit is contained in:
German Khokhlov committed 2026-11-10 16:43:58 +04:00
1 parent ee697422c9
commit 373edf2989
26 files changed
+526 -803

No files matched your search

+25 -37
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
// Cesium-only probes: native primitive colors independently verify the Cesium aircraft tint.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled, whenFramesDrawn, whenTilesLoaded } from './qa/ready.mjs';
/**
* qa-emergency-squawk.mjs — headless proof for transponder emergency highlighting.
*
@@ -26,7 +29,7 @@
* Exits non-zero on any FAIL. Does not commit anything.
*/
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import { stubPlanespotters } from './qa-photo-stub.mjs';
import fs from 'node:fs';
import path from 'node:path';
@@ -52,7 +55,6 @@ function record(name, ok, detail) {
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** OpenSky state vector: squawk sits at index 14. */
const civilRow = (icao24, callsign, lon, lat, squawk) => [
@@ -175,13 +177,9 @@ const isAmber = (c) =>
Boolean(c) && c.r > 180 && c.g > 110 && c.b < 140 && !isRed(c);
async function shoot(page, name) {
await page
.waitForFunction(
() => window.__godsEyeView.viewer.scene.globe.tilesLoaded,
{ timeout: 30000 },
)
.catch(() => {});
await sleep(600);
await whenTilesLoaded(page, { timeoutMs: 30000 }).catch(() => {});
await whenSettled(page);
await whenFramesDrawn(page);
await page.screenshot({
path: path.join(SHOTS_DIR, `emergency-${name}.png`),
});
@@ -200,19 +198,10 @@ async function main() {
process.exit(2);
}
fs.mkdirSync(SHOTS_DIR, { recursive: true });
const executablePath =
process.env.PUPPETEER_EXECUTABLE_PATH ||
(await puppeteer.executablePath().catch(() => null)) ||
undefined;
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
const browser = await launchBrowser({
headless: true,
protocolTimeout: 180000,
...(executablePath && fs.existsSync(executablePath)
? { executablePath }
: {}),
args: [
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
@@ -252,31 +241,30 @@ async function main() {
timeout: TIMEOUT_MS,
});
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
{ timeout: TIMEOUT_MS },
);
await sleep(5000);
await whenSettled(page);
await whenFramesDrawn(page);
await page.keyboard.press('Escape');
await page.evaluate(
(lo, la) => {
const gev = window.__godsEyeView;
await evaluateWithGeo(
page,
async (qaGeo, lo, la) => {
const d2r = Math.PI / 180;
try {
gev.viewer.camera.cancelFlight();
window.__godsEyeView.engine.camera.cancelFlight();
} catch {
/* no flight active */
}
gev.viewer.camera.setView({
destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian(
{
longitude: lo * d2r,
latitude: la * d2r,
height: 260000,
},
),
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: lo * d2r,
latitude: la * d2r,
height: 260000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
gev.viewer.scene.requestRender?.();
window.__godsEyeView.engine.render.request?.();
},
lon,
lat,
@@ -297,7 +285,7 @@ async function main() {
polling: 500,
})
.catch(() => {});
await sleep(1500);
await whenFramesDrawn(page);
const colors = await page.evaluate(probe);
const show = (id) => JSON.stringify(colors[id] ?? null);
record(
@@ -538,7 +526,7 @@ async function main() {
console.error('\x1b[31mHarness error:\x1b[0m', err);
exitCode = 3;
} finally {
await browser.close();
await closeBrowser(browser);
}
const pass = results.filter((r) => r.ok).length;
+17 -49
View File
@@ -50,9 +50,10 @@
* Exits non-zero if any assertion fails. Never commits anything.
*/
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { classifyAircraft, CLASS_SCALE_2D } from '../src/data/aircraftClass.js';
import { aircraftIcon } from '../src/data/aircraftIcons.js';
@@ -91,26 +92,7 @@ const SESSION_CAP = 300; // ENRICH_AMBIENT_BUDGET_CEIL (rolling-bucket cei
const BUDGET_QA = { ceil: SESSION_CAP, refillTokens: 6, windowMs: 3600000 };
const RESUME_WINDOW_MS = 1000; // E11 swaps windowMs to this to unlock refills
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
await puppeteer.executablePath().catch(() => null),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* skip */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
@@ -186,21 +168,9 @@ async function main() {
fs.mkdirSync(SHOT_DIR, { recursive: true });
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
`--use-angle=${process.env.QA_ANGLE || 'swiftshader'}`,
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
const browser = await launchBrowser({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
});
const consoleErrors = [];
@@ -303,8 +273,9 @@ async function main() {
// A cold dev server compiles the Navara chunks on first load.
{ timeout: 180000, polling: 200 }
);
await whenSettled(page);
await page.evaluate(() => window.__godsEyeView.styleManager.initialRestorePromise);
await page.waitForFunction(() => document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 180000 });
await whenSettled(page);
await page.evaluate(() => window.__godsEyeView.styleManager.orbitController.stop());
console.log(' App globals ready.');
@@ -339,17 +310,16 @@ async function main() {
console.log('Priming straight-flight history through the render delay (30 s)...');
const primed = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer ?? null;
window.__ENR.timeOffsetSec = -32;
dm.setLayerParams('flights', { models3d: false }); // ambient budget excludes model-priority enrichment
await dm.setEnabled('flights', true);
const fl = dm.layers.get('flights').module;
for (const off of [-24, -16, -8, 0]) {
window.__ENR.timeOffsetSec = off;
await fl.update(v);
await fl.update();
}
window.__ENR.timeOffsetSec = 0;
window.__ENR_DRIVER = setInterval(() => { fl.update(v); }, 15000);
window.__ENR_DRIVER = setInterval(() => { fl.update(); }, 15000);
return { count: fl.getStats().count, starts: window.__ENRICH_LOG.starts.length, models3d: fl.getParams().models3d };
});
console.log(` flights count=${primed.count} | adsbdb requests started (held): ${primed.starts}`);
@@ -373,7 +343,7 @@ async function main() {
record('E2 baseline: all 12 category-0 planes default to the airliner glyph',
baselineBad.length === 0,
baselineBad.length ? baselineBad.join(' ') : 'all 12 identical airliner silhouettes (the field-data problem)');
await sleep(1200); // let a fleet tick land rotations before the "before" shot
await whenSettled(page); // let a fleet tick land rotations before the "before" shot
await page.screenshot({ path: path.join(SHOT_DIR, 'enrich-before-monoculture.png') });
console.log(' saved enrich-before-monoculture.png (12 identical airliner glyphs)');
@@ -461,10 +431,9 @@ async function main() {
console.log('\nE8 — next poll after enrichment');
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer ?? null;
await dm.layers.get('flights').module.update(v);
await dm.layers.get('flights').module.update();
});
await sleep(500);
await whenSettled(page);
const afterPoll = await page.evaluate(() => ({
billboards: window.__collectBillboards(),
starts: window.__ENRICH_LOG.starts.length,
@@ -497,7 +466,6 @@ async function main() {
const startsBeforeBatch = await page.evaluate(() => window.__ENRICH_LOG.starts.length);
await page.evaluate(async ({ hexes, rowLat, rowLon0, stepDeg }) => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer ?? null;
// Two fresh rows just north of the original one — still inside the
// top-down 20 km frame, so the sweep's frustum test keeps them.
// STATIONARY (speed 0): moving planes drift north out of the frustum at
@@ -525,8 +493,8 @@ async function main() {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const fl = dm.layers.get('flights').module;
await fl.update(v); // sweep enqueues up to 150 ambient jobs
await fl.update(v); // spend the remaining initial tokens; later polls must stall
await fl.update(); // sweep enqueues up to 150 ambient jobs
await fl.update(); // spend the remaining initial tokens; later polls must stall
}, { hexes: batchHexes, rowLat: ROW_LAT, rowLon0: ROW_LON0, stepDeg: ROW_STEP_DEG });
// Wait for the drip to drain whatever was enqueued, then settle.
await page.waitForFunction(
@@ -537,7 +505,7 @@ async function main() {
// One more poll while exhausted — still nothing new.
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer ?? null);
await dm.layers.get('flights').module.update();
});
await sleep(1000);
const exhausted = await page.evaluate(() => ({
@@ -562,7 +530,7 @@ async function main() {
await sleep(RESUME_WINDOW_MS + 200); // a full (shortened) refill window elapses
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer ?? null);
await dm.layers.get('flights').module.update();
});
const expectedTotal = startsBeforeBatch + BATCH_COUNT; // every batch plane eventually requested
const resumed = await page.waitForFunction(
@@ -581,7 +549,7 @@ async function main() {
finish();
} finally {
await browser.close();
await closeBrowser(browser);
}
function finish() {
+11 -42
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env node
// Cesium-only probes: render-loop shutdown and manual rendering inject a Cesium failure.
import { whenUiSettled, whenSettled } from './qa/ready.mjs';
/**
* qa-failstate-b10.mjs
*
@@ -31,7 +33,7 @@
* Exits non-zero if any assertion fails. DOES NOT COMMIT anything.
*/
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
@@ -58,28 +60,7 @@ function withDetectDebug(url) {
const HEADFUL = getFlag('--headful');
const ARTIFACT_DIR = getOpt('--artifact-dir', null);
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
await puppeteer.executablePath().catch(() => null),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try {
if (fs.existsSync(candidate)) return candidate;
} catch { /* ignore */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
@@ -88,7 +69,6 @@ function record(name, ok, detail) {
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function readLayerControl(page, layerId, expectedLabel) {
await page.waitForFunction(
@@ -127,7 +107,7 @@ async function captureLayerControl(page, layerId, filename) {
const rect = element?.getBoundingClientRect();
return !!rect && rect.width > 0 && rect.height > 0;
}, { timeout: 5_000 }, layerId);
await sleep(100);
await whenUiSettled(page);
await row.screenshot({ path: path.join(ARTIFACT_DIR, filename) });
}
@@ -156,21 +136,9 @@ async function main() {
process.exit(2);
}
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
const browser = await launchBrowser({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
});
let exitCode = 0;
@@ -221,7 +189,8 @@ async function main() {
&& window.__godsEyeView.styleManager,
{ timeout: 60000 },
);
await sleep(1500);
await whenSettled(page);
await whenUiSettled(page);
// ── (0) Manager-owned periodic refresh → shared work/failure/recovery ──
console.log('\n(0) Universal periodic refresh feedback...');
@@ -524,7 +493,7 @@ async function main() {
} finally {
if (tileset) tileset.show = priorTilesetShow;
viewer.useDefaultRenderLoop = priorDefaultLoop;
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
}
const detState = gev.styleManager.getDetectionState?.() || null;
return { present: true, nonEmpty, solid, sampled, mode: detState?.detectionMode ?? null };
@@ -550,7 +519,7 @@ async function main() {
console.error('\x1b[31mHarness error:\x1b[0m', e);
exitCode = 3;
} finally {
await browser.close();
await closeBrowser(browser);
}
const pass = results.filter((r) => r.ok === true).length;
+12 -13
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env node
/** Persistent inline attribution through actual OSM layer display lifetimes. */
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
import { moveCamera } from './qa-journey-recorder.mjs';
const url = process.argv.find((a) => /^https?:/.test(a));
if (!url) throw new Error('Supply the running server URL');
@@ -10,9 +11,9 @@ const dir = process.env.QA_POLISH_DIR || 'qa-shots/final-polish';
await fs.mkdir(dir, { recursive: true });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const result = { states: [], errors: [] };
const browser = await puppeteer.launch({
const browser = await launchBrowser({
headless: true,
args: ['--use-angle=metal'],
args: [],
});
try {
const page = await browser.newPage();
@@ -25,12 +26,10 @@ try {
sessionStorage.setItem('gev:first-run-mission-session:v1', 'dismissed'),
);
await page.goto(`${url}/?welcome=0`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120000 },
);
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
timeout: 120000,
});
await whenSettled(page);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
@@ -53,7 +52,7 @@ try {
value,
);
const shot = async (name, osm, tiles) => {
await sleep(300);
await whenSettled(page);
const state = await page.$eval('#cesium-credits', (e) => {
const nodes = [
...e.querySelectorAll('a,img,.cesium-credit-expand-link'),
@@ -111,7 +110,7 @@ try {
});
result.states.push({ name, ...state });
};
await sleep(1600);
await whenSettled(page);
await shot('nothing', false, false);
await toggle('alpr-cameras', true);
await page.waitForFunction(
@@ -137,7 +136,7 @@ try {
await shot('traffic', true, true);
for (const width of [1280, 1920]) {
await page.setViewport({ width, height: 900 });
await sleep(500);
await whenSettled(page);
await shot(`traffic-${width}`, true, true);
}
await page.setViewport({ width: 1440, height: 900 });
@@ -154,5 +153,5 @@ try {
console.log(JSON.stringify(result, null, 2));
} finally {
await fs.writeFile(`${dir}/credits.json`, JSON.stringify(result, null, 2));
await browser.close();
await closeBrowser(browser);
}
+19 -41
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
// Cesium-only probes: entity sentinels and native flight interception verify the Cesium selection path.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
/**
* qa-firms.mjs — headless proof for the LIVE NASA FIRMS fires layer.
*
@@ -29,7 +32,7 @@
* Exits non-zero on any FAIL. Does not commit anything.
*/
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import {
isExpectedPreviewSetupError,
redact,
@@ -54,33 +57,14 @@ const APP_URL = appUrl.href;
const HEADFUL = argv.includes('--headful');
const FIXTURES = argv.includes('--fixtures');
const PREVIEW = argv.includes('--preview');
const ANGLE = getOpt('--angle', 'swiftshader');
const ANGLE = process.env.QA_ANGLE || 'metal';
const OUT = getOpt('--out', null);
const SHOTS_DIR = OUT ? path.resolve(OUT).replace(/\.json$/, '') + '-shots' : path.join(REPO_ROOT, 'qa-shots');
if (!['metal', 'swiftshader'].includes(ANGLE)) throw new Error('Unknown ANGLE backend');
const runtimeErrors = [];
const expectedSourceErrors = [];
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
await puppeteer.executablePath().catch(() => null),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* ignore */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
@@ -126,7 +110,7 @@ async function waitForCardCanvasInk(page, { timeoutMs = 12000 } = {}) {
let consecutive = 0;
let sample = null;
while (Date.now() < deadline) {
await page.evaluate(() => window.__godsEyeView?.viewer?.scene?.requestRender?.());
await page.evaluate(() => window.__godsEyeView.engine.render.request?.());
await sleep(150);
sample = await cardCanvasInk(page);
if (sample.present && sample.entries > 0 && sample.painted > 0 && sample.ink > 500) {
@@ -160,7 +144,7 @@ async function waitForFirmsActionCount(page, expected, { timeoutMs = 12000 } = {
const deadline = Date.now() + timeoutMs;
let snapshot = await firmsActionSnapshot(page);
while (Date.now() < deadline && snapshot.count !== expected) {
await page.evaluate(() => window.__godsEyeView?.viewer?.scene?.requestRender?.());
await page.evaluate(() => window.__godsEyeView.engine.render.request?.());
await sleep(175);
snapshot = await firmsActionSnapshot(page);
}
@@ -174,9 +158,10 @@ async function bootAndEnable(page, { timeoutS = 45 } = {}) {
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
{ timeout: 60000 },
);
await whenSettled(page);
const actualEngine = await page.evaluate(() => window.__godsEyeView.engine.kind);
if (actualEngine !== ENGINE) throw new Error('Requested FIRMS engine did not load');
await sleep(5000);
await whenSettled(page);
await page.keyboard.press('Escape');
return page.evaluate(async (tS) => {
const dm = window.__godsEyeView.dataManager;
@@ -194,7 +179,7 @@ async function bootAndEnable(page, { timeoutS = 45 } = {}) {
/** Teleport the camera (duck-typed cartographic — no Cesium global). */
async function setView(page, lon, lat, height) {
await page.evaluate(async (lo, la, h) => {
await evaluateWithGeo(page, async (qaGeo, lo, la, h) => {
const gev = window.__godsEyeView;
if (!gev.viewer) {
const geo = await import('/src/geo/index.js');
@@ -203,15 +188,14 @@ async function setView(page, lon, lat, height) {
gev.engine.render.request('firms-legacy-view');
return;
}
const ell = gev.viewer.scene.globe.ellipsoid;
const d2r = Math.PI / 180;
// The app's intro flyTo animation clobbers a setView issued mid-flight.
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight active */ }
gev.viewer.camera.setView({
destination: ell.cartographicToCartesian({ longitude: lo * d2r, latitude: la * d2r, height: h }),
try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* no flight active */ }
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({ longitude: lo * d2r, latitude: la * d2r, height: h }),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
gev.viewer.scene.requestRender?.();
window.__godsEyeView.engine.render.request?.();
}, lon, lat, height);
}
@@ -270,15 +254,9 @@ async function main() {
}
fs.mkdirSync(SHOTS_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', `--use-angle=${ANGLE}`,
'--disable-dev-shm-usage', '--disable-web-security',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1440,900',
],
const browser = await launchBrowser({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1440,900'],
});
let exitCode = 0;
@@ -484,7 +462,7 @@ async function main() {
if (settled) break;
await sleep(100);
}
await sleep(250);
await whenFramesDrawn(page);
const proof = await page.evaluate(() => {
const gev = window.__godsEyeView;
@@ -558,7 +536,7 @@ async function main() {
console.error('\x1b[31mHarness error:\x1b[0m', e);
exitCode = 3;
} finally {
await browser.close();
await closeBrowser(browser);
}
record('RUNTIME: no unexpected browser errors', runtimeErrors.length === 0,
+22 -39
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node
import { whenUiSettled } from './qa/ready.mjs';
/**
* qa-firstrun — the mission launcher's contract, proved in the real app.
*
@@ -18,10 +19,10 @@
* every rendered/behavioral assertion below must go RED. A green --teeth run
* means the assertions are not measuring what they claim to.
*/
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
const args = process.argv.slice(2);
const getOpt = (flag, fallback) => {
@@ -35,12 +36,6 @@ const HEADFUL = args.includes('--headful');
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const SHOT_DIR = path.join(ROOT, 'qa-shots', 'firstrun');
const CHROME_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Version-pinned Chrome-for-Testing over the auto-updating system Chrome.
await puppeteer.executablePath().catch(() => null),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
].filter(Boolean);
const results = [];
let currentSection = 'setup';
@@ -96,7 +91,7 @@ async function open(page, { hash = '', query = '', clearAll = true, clearSession
{ timeout: 45000 },
LAUNCHER,
).catch(() => {});
await sleep(400);
await whenUiSettled(page);
if (errorSink) errorSink.length = 0;
}
@@ -184,7 +179,7 @@ async function pick(page, choice, { timeout = 40000, settle = [] } = {}) {
}, settle).catch(() => {});
}
// Plus a render beat: settled data still has to reach the framebuffer.
await sleep(settle.length ? 3500 : 1500);
await whenUiSettled(page);
}
/** Every section the run is expected to reach; the teeth verdict requires all. */
@@ -236,7 +231,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
record('the launcher is up before the scene starts', beforeScene.onScreen);
await page.evaluate(() => document.body.classList.add('scene-playback-mode'));
await sleep(300);
await whenUiSettled(page);
const duringScene = await launcherState();
record(
'a scene taking the screen makes the launcher YIELD, not just hide',
@@ -249,7 +244,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
// The key that used to dismiss an invisible launcher must now do nothing
// to it — proven by the scene class still being set afterwards.
await page.keyboard.press('Escape');
await sleep(300);
await whenUiSettled(page);
const afterEsc = await page.evaluate(() => ({
sceneStillOn: document.body.classList.contains('scene-playback-mode'),
}));
@@ -259,7 +254,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
afterEsc.sceneStillOn ? 'scene class untouched by the launcher' : 'something else consumed the key',
);
await page.evaluate(() => document.body.classList.remove('scene-playback-mode'));
await sleep(200);
await whenUiSettled(page);
const afterScene = await launcherState();
record('a yielded launcher does not pop back when the scene ends',
!afterScene.classVisible, `classVisible=${afterScene.classVisible}`);
@@ -269,7 +264,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
const beforeCockpit = await launcherState();
record('the launcher is up before cockpit engages', beforeCockpit.onScreen);
await page.evaluate(() => document.body.classList.add('cockpit-mode'));
await sleep(300);
await whenUiSettled(page);
const duringCockpit = await launcherState();
record(
'cockpit engaging makes the launcher YIELD before it can contest ESC',
@@ -277,7 +272,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
`classVisible=${duringCockpit.classVisible} — cockpit registers its capture listener first, so stacking must never arise`,
);
await page.evaluate(() => document.body.classList.remove('cockpit-mode'));
await sleep(200);
await whenUiSettled(page);
// ── Blocker repro: a surface that takes the screen with NO class ────────
// The attribution lightbox is full-screen at z-index 200 against the card's
@@ -303,7 +298,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
link?.click();
return !!link;
});
await sleep(300);
await whenUiSettled(page);
const lightboxUp = await lightboxState();
record(
'the real "Data attribution" lightbox opens above the card',
@@ -318,7 +313,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
);
await page.keyboard.press('Escape');
await sleep(400);
await whenUiSettled(page);
const escUnderLightbox = await launcherState();
const lightboxAfterEsc = await lightboxState();
record(
@@ -343,12 +338,12 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
document.activeElement === document.querySelector('#cesium-credits .cesium-credit-expand-link')
));
record('closing attribution restores its disclosure focus', attributionFocusRestored);
await sleep(300);
await whenUiSettled(page);
const uncovered = await launcherState();
record('closing the lightbox hands the card back the key',
uncovered.topmost, `topmost=${uncovered.topmost}`);
await page.keyboard.press('Escape');
await sleep(600);
await whenUiSettled(page);
const afterUncoveredEsc = await launcherState();
record(
'ESC dismisses normally again once nothing is on top',
@@ -407,7 +402,7 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
// ...and appears once that surface clears.
await page.evaluate(() => document.body.classList.remove('cockpit-mode'));
await sleep(800);
await whenUiSettled(page);
const afterCockpitCleared = await launcherState();
record('the launcher appears once the surface clears',
afterCockpitCleared.onScreen, `onScreen=${afterCockpitCleared.onScreen}`);
@@ -439,21 +434,9 @@ async function main() {
}
fs.mkdirSync(SHOT_DIR, { recursive: true });
const executablePath = CHROME_CANDIDATES.find((candidate) => {
try { return fs.existsSync(candidate); } catch { return false; }
});
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(executablePath ? { executablePath } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox',
// Metal is a macOS-only ANGLE backend; anywhere else it fails WebGL init.
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu', '--ignore-gpu-blocklist']
: ['--use-gl=angle', '--use-angle=swiftshader']),
'--disable-dev-shm-usage', '--disable-background-timer-throttling',
'--disable-renderer-backgrounding', '--window-size=1440,900',
],
const browser = await launchBrowser({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(process.platform === 'darwin' ? ['--enable-gpu', '--ignore-gpu-blocklist'] : []), '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1440,900'],
});
const shots = [];
@@ -510,7 +493,7 @@ async function main() {
// ESC dismisses. Stated as a TRANSITION (was up, now gone) — "not visible"
// alone passes vacuously whenever the launcher never appeared at all.
await page.keyboard.press('Escape');
await sleep(600);
await whenUiSettled(page);
const state = await appState(page);
record('ESC dismisses the launcher', freshVisible && !(await launcherVisible(page)),
freshVisible ? 'visible → dismissed' : 'never appeared, so nothing was dismissed');
@@ -530,11 +513,11 @@ async function main() {
// Checkbox → durable suppression.
await page.click('[data-first-run-suppress]');
await sleep(200);
await whenUiSettled(page);
const ticked = await appState(page);
record('the checkbox writes durable suppression immediately', ticked.durable === 'suppressed', `durable=${ticked.durable}`);
await page.keyboard.press('Escape');
await sleep(400);
await whenUiSettled(page);
await open(page, { clearAll: false, clearSession: true });
record('a suppressed profile stays quiet in later sessions',
ticked.durable === 'suppressed' && !(await launcherVisible(page)),
@@ -789,7 +772,7 @@ async function main() {
if (list) list.scrollTop = 0;
document.activeElement?.blur?.();
}, LAUNCHER);
await sleep(250);
await whenUiSettled(page);
shots.push(await shoot(page, `launcher-${vp.name}`));
}
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
@@ -800,7 +783,7 @@ async function main() {
consoleErrors.slice(0, 3).join(' | ') || 'clean');
});
} finally {
await browser.close();
await closeBrowser(browser);
}
const failed = results.filter((entry) => !entry.ok);
+22 -23
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
// Cesium-only probes: native primitive positions provide the Cesium floor oracle.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled } from './qa/ready.mjs';
/**
* scripts/qa-floor-hold.mjs — a grounded contact holds its floor through a
* terrain-proxy outage (owner incident, 2026-08-21).
@@ -25,13 +28,13 @@
* under Metal — a backend comparison at one point, not to be confused with the
* same-backend run-to-run spread (122.1 m vs 20.6 m on a cell centre) that
* retired the hold chain's unvalidated tier. The
* default is Metal on macOS and SwiftShader elsewhere; `--angle` overrides it.
* default is Metal; QA_ANGLE overrides it.
* Mesh samples describe the selected backend, not another GPU's rendered LOD.
* An unavailable rendered-mesh oracle remains a failed check, even if the
* separate bare-earth DEM check passes. Software evidence does not establish
* real-GPU visual correctness.
*/
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
@@ -39,7 +42,7 @@ const APP_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
const argv = Object.fromEntries(process.argv.slice(2)
.map((a) => a.replace(/^--/, '').split('=')).map(([k, v]) => [k, v ?? true]));
// Metal is a macOS-only ANGLE backend; defaulting to it off-Mac fails WebGL init.
const ANGLE = String(argv.angle || (process.platform === 'darwin' ? 'metal' : 'swiftshader'));
const ANGLE = process.env.QA_ANGLE || 'metal';
const HEADFUL = !!argv.headful;
// Austin airport apron by default — the site qa-floor-verify pins, where the
// mesh sits ~150 m above the geoid and burial is unmistakable.
@@ -58,12 +61,9 @@ function record(name, ok, detail) {
}
fs.mkdirSync(OUT_DIR, { recursive: true });
const chrome = await puppeteer.executablePath().catch(() => undefined);
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
executablePath: chrome,
args: ['--use-gl=angle', `--use-angle=${ANGLE}`, '--enable-webgl',
'--ignore-gpu-blocklist', '--no-sandbox'],
const browser = await launchBrowser({
headless: true,
args: ['--enable-webgl', '--ignore-gpu-blocklist', '--no-sandbox'],
});
const page = await browser.newPage();
await page.setViewport({ width: 1400, height: 900 });
@@ -109,14 +109,14 @@ console.log(`\nFloor hold through a terrain outage\n App : ${APP_URL}\n GPU
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
await page.waitForFunction(() => window.__godsEyeView?.layers && window.__godsEyeView?.dataManager,
{ timeout: 150000 });
await sleep(12000); // let the boot fly-to settle before pinning
await whenSettled(page); // let the boot fly-to settle before pinning
await page.evaluate(() => {
document.querySelector('#first-run-launcher:not([hidden]) [data-first-run-choice="explore"]')?.click();
});
await sleep(600); // let the launcher dismissal finish before taking evidence
await whenSettled(page); // let the launcher dismissal finish before taking evidence
await page.evaluate((site) => { window.__SITE = site; }, SITE);
const pin = () => page.evaluate(async () => {
const pin = () => evaluateWithGeo(page, async (qaGeo) => {
const app = window.__godsEyeView;
if (!app.viewer) {
const geo = await import('/src/geo/index.js');
@@ -128,16 +128,15 @@ const pin = () => page.evaluate(async () => {
app.engine.camera.moveEnd.raiseEvent();
return;
}
const v = app.viewer;
const C = v.camera.positionCartographic.constructor;
v.camera.cancelFlight();
v.camera.setView({
destination: v.scene.globe.ellipsoid.cartographicToCartesian(
const C = qaGeo.cartographic;
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian(
C.fromDegrees(window.__SITE.lon, window.__SITE.lat - 0.008, 900),
),
orientation: { heading: 0, pitch: -0.45, roll: 0 },
});
v.camera.moveEnd.raiseEvent();
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
});
await pin();
const billboardMode = await page.evaluate(async () => {
@@ -152,7 +151,7 @@ const billboardMode = await page.evaluate(async () => {
record('the billboard floor test is explicitly in 2D aircraft mode', billboardMode);
/** Reads the contact's rendered height and the rendered mesh beneath it. */
const measure = () => page.evaluate(async (icao) => {
const measure = () => evaluateWithGeo(page, async (qaGeo, icao) => {
const app = window.__godsEyeView;
if (!app.viewer) {
const geo = await import('/src/geo/index.js');
@@ -171,7 +170,7 @@ const measure = () => page.evaluate(async (icao) => {
lat: +(carto.latitude * 180 / Math.PI).toFixed(5), lon: +(carto.longitude * 180 / Math.PI).toFixed(5), terrain: { ...window.__TERRAIN } };
}
const v = app.viewer;
const C = v.camera.positionCartographic.constructor;
const C = qaGeo.cartographic;
// The contact as the layer draws it (the probe's drawn rows: the layer owns
// no Cesium billboard of its own any more; the engine's batched billboards
// keep a hidden spare's old id, so an id match there can be stale).
@@ -201,7 +200,7 @@ const measure = () => page.evaluate(async (icao) => {
// harness does, and accept the coarse cell centre as a fallback point.
const pump = () => new Promise((resolve) => {
try { window.__godsEyeView.requestRender?.(); } catch { /* no governor */ }
try { v.scene.requestRender(); } catch { /* explicit-render off */ }
try { window.__godsEyeView.engine.render.request(); } catch { /* explicit-render off */ }
requestAnimationFrame(() => requestAnimationFrame(resolve));
});
let mesh = null;
@@ -210,7 +209,7 @@ const measure = () => page.evaluate(async (icao) => {
for (const [pLat, pLon] of [[lat, lon], [+lat.toFixed(3), +lon.toFixed(3)]]) {
for (const exclude of [[...sprites, ...models], []]) {
try {
const h = v.scene.sampleHeight(C.fromDegrees(pLon, pLat, 0), exclude);
const h = window.__godsEyeView.engine.surface.sampleHeight(C.fromDegrees(pLon, pLat, 0), exclude);
if (Number.isFinite(h)) { mesh = h; break; }
} catch { /* mid-stream */ }
}
@@ -278,7 +277,7 @@ for (let i = 1; i <= 4; i++) {
await sample('B', `poll+${i}`);
}
fs.writeFileSync(path.join(OUT_DIR, `hold-B-outage-${ANGLE}.png`), await page.screenshot({ type: 'png' }));
await browser.close();
await closeBrowser(browser);
console.log('');
const outage = samples.filter((s) => s.phase === 'B');
+21 -25
View File
@@ -1,3 +1,6 @@
// Cesium-only probes: native primitive and streamed-tile positions provide the Cesium floor oracle.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled } from './qa/ready.mjs';
// scripts/qa-floor-verify.mjs — live floor verification at AUS (round 5).
// Pins the camera at Austin airport, enables flights, waits ~3 polls, then
// measures every nearby contact's render height against the ACTUAL rendered
@@ -5,7 +8,7 @@
// coarse-LOD poison and the taxiing cold-cell regression on 2026-07-06.
// Run: node scripts/qa-floor-verify.mjs (dev server on :4173, real GPU best)
// with the poison fix + simplified chain live.
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
// QA_BASE_URL matches the sibling harnesses (qa-height-datum / qa-cctv-v2) so a
@@ -29,29 +32,23 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// (2026-07-30 lesson, already applied to the other harnesses): /Applications
// auto-updates underneath the harnesses and its software-GL behavior shifts
// across majors.
const chrome = [
process.env.PUPPETEER_EXECUTABLE_PATH,
await puppeteer.executablePath().catch(() => null),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
].filter(Boolean).find((p) => fs.existsSync(p));
const browser = await puppeteer.launch({
headless: 'new', executablePath: chrome,
args: ['--use-gl=angle', `--use-angle=${process.env.QA_ANGLE || 'swiftshader'}`, '--enable-webgl', '--ignore-gpu-blocklist', '--no-sandbox'],
const browser = await launchBrowser({
headless: true, args: ['--enable-webgl', '--ignore-gpu-blocklist', '--no-sandbox'],
});
const page = await browser.newPage();
await page.setViewport({ width: 1400, height: 900 });
page.on('pageerror', (e) => console.log('[pageerror]', e.message));
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
await page.waitForFunction(() => window.__godsEyeView?.dataManager && window.__godsEyeView?.engine, { timeout: 150000 });
await sleep(2000);
await whenSettled(page);
// Let the app's boot fly-to fully settle FIRST (it was overriding an early
// setView, leaving the camera away from Austin — every probe then read
// coarse global tiles at ~25 m and the oracle was garbage).
await sleep(12000);
await whenSettled(page);
await page.evaluate((site) => { window.__QA_SITE = site; }, SITE);
const camPin = async () => page.evaluate(async () => {
const camPin = async () => evaluateWithGeo(page, async (qaGeo) => {
const v = window.__godsEyeView.viewer;
if (!v) {
const geo = await import('/src/geo/index.js');
@@ -61,11 +58,11 @@ const camPin = async () => page.evaluate(async () => {
const c = camera.pose().cartographic;
return { lat: +(c.latitude * 180 / Math.PI).toFixed(3), lon: +(c.longitude * 180 / Math.PI).toFixed(3), h: Math.round(c.height) };
}
const C = v.camera.positionCartographic.constructor;
v.camera.cancelFlight();
v.camera.setView({ destination: v.scene.globe.ellipsoid.cartographicToCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 3000)) });
v.camera.moveEnd.raiseEvent();
const c = v.camera.positionCartographic;
const C = qaGeo.cartographic;
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({ destination: qaGeo.cartographic.toCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 3000)) });
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
const c = window.__godsEyeView.engine.camera.pose().cartographic;
return { lat: +(c.latitude * 180 / Math.PI).toFixed(3), lon: +(c.longitude * 180 / Math.PI).toFixed(3), h: Math.round(c.height) };
});
console.log('camera pinned:', JSON.stringify(await camPin()));
@@ -87,7 +84,7 @@ const ready = await page.evaluate(() => {
console.log('tileset ready:', JSON.stringify(ready));
await sleep(8000);
const report = await page.evaluate(async () => {
const report = await evaluateWithGeo(page, async (qaGeo) => {
const gev = window.__godsEyeView;
const v = gev.viewer;
const layer = gev.dataManager.layers.get('flights')?.module;
@@ -141,9 +138,8 @@ const report = await page.evaluate(async () => {
}
return { ausContacts: out.length, contacts: out.slice(0, 16), spritesShown: shown, spritesHidden: hidden };
}
const ell = v.scene.globe.ellipsoid;
const C = v.camera.positionCartographic.constructor;
const center = ell.cartographicToCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 200));
const C = qaGeo.cartographic;
const center = qaGeo.cartographic.toCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 200));
const nearby = layer.getNearby(center, 15000, 60);
// Exclude EVERY billboard from the probes — sprites are pickable, so an
// unexcluded probe can return another aircraft's height as "the mesh".
@@ -172,11 +168,11 @@ const report = await page.evaluate(async () => {
const out = [];
for (const p of nearby) {
const modelAt = drawnAt(p);
const c = ell.cartesianToCartographic(modelAt ?? p.position);
const c = qaGeo.cartographic.fromCartesian(modelAt ?? p.position);
const latDeg = c.latitude * 180 / Math.PI, lonDeg = c.longitude * 180 / Math.PI;
let meshH = null;
try {
const h = v.scene.sampleHeight(C.fromDegrees(lonDeg, latDeg), excludes);
const h = window.__godsEyeView.engine.surface.sampleHeight(C.fromDegrees(lonDeg, latDeg), excludes);
if (Number.isFinite(h)) meshH = h;
} catch { /* ignore */ }
out.push({
@@ -201,7 +197,7 @@ const report = await page.evaluate(async () => {
let shown = 0, hidden = 0;
for (const row of gev.layers?.drawn('flights') ?? []) {
if (!row.position || row.trackedModel) continue;
const cc = ell.cartesianToCartographic(row.position);
const cc = qaGeo.cartographic.fromCartesian(row.position);
const la = cc.latitude * 180 / Math.PI, lo = cc.longitude * 180 / Math.PI;
if (Math.abs(la - window.__QA_SITE.lat) > 0.08 || Math.abs(lo - window.__QA_SITE.lon) > 0.10) continue;
if (row.show) shown++; else hidden++;
@@ -223,7 +219,7 @@ for (const b of buried) {
}
const verdict = lows.length === 0 ? 'INCONCLUSIVE' : (buried.length === 0 ? 'PASS' : 'FAIL');
console.log(`VERDICT: ${verdict}`);
await browser.close();
await closeBrowser(browser);
// Exit code (2026-08-19): this harness printed VERDICT: FAIL and still exited 0,
// so a real regression (taxiing contacts rendering under the mesh) stayed
// invisible to every automated caller. Now it reports like its siblings
+25 -44
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node
import { whenSettled } from './qa/ready.mjs';
/**
* fly_route cinematic evidence — drives the REAL voice runner headlessly and
* measures the REAL camera (Cesium heading/pitch/roll + position) every
@@ -9,10 +10,10 @@
*
* Writes a screenshot sequence plus a JSON trace to qa-shots/flyroute/.
*/
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -25,11 +26,6 @@ const OUT_DIR = getOpt('--out', path.join(ROOT, 'qa-shots', 'flyroute'));
const MIRROR_DIR = getOpt('--mirror', '');
const SHOT_EVERY_MS = Number(getOpt('--shot-ms', '2000'));
const CHROME_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
await puppeteer.executablePath().catch(() => null),
].filter(Boolean);
const CHROME_EXECUTABLE = CHROME_CANDIDATES.find((c) => { try { return fs.existsSync(c); } catch { return false; } });
// A 6-waypoint downtown Austin route: north, right, left, right, left.
const ROUTE_POINTS = [
@@ -57,22 +53,9 @@ const wrapDeg = (deg) => ((deg + 540) % 360) - 180;
fs.mkdirSync(OUT_DIR, { recursive: true });
if (MIRROR_DIR) fs.mkdirSync(MIRROR_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: 'new',
...(CHROME_EXECUTABLE ? { executablePath: CHROME_EXECUTABLE } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
// Real GPU when the host has one: the dolly is frame-rate independent, but
// a higher sample rate makes the roll and the ease ramps far easier to see.
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1500,950',
],
const browser = await launchBrowser({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(process.platform === 'darwin' ? ['--enable-gpu'] : []), '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1500,950'],
protocolTimeout: 240000,
});
const page = await browser.newPage();
@@ -99,21 +82,19 @@ if (TERRAIN_DELAY_MS > 0) {
/** Install a postRender sampler: one row per RENDERED frame. */
async function installSampler() {
await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
window.__gevFlyTrace = { rows: [], marks: [] };
if (window.__gevFlyTraceRemove) window.__gevFlyTraceRemove();
let frame = 0;
const listener = () => {
const cam = viewer.camera;
const carto = cam.positionCartographic;
const carto = window.__godsEyeView.engine.camera.pose().cartographic;
// Every 6th frame, ask the RENDERED WORLD what is under the camera. This
// is the only measurement that can prove "never below terrain": it reads
// the surface the user is actually looking at, not our own floor cache.
let surfaceM = null;
frame += 1;
if (frame % 6 === 0 && typeof viewer.scene.sampleHeight === 'function') {
if (frame % 6 === 0 && typeof window.__godsEyeView.engine.surface.sampleHeight === 'function') {
try {
const probe = viewer.scene.sampleHeight(carto.clone());
const probe = window.__godsEyeView.engine.surface.sampleHeight(carto);
if (Number.isFinite(probe)) surfaceM = probe;
} catch { /* tiles not loaded under the camera */ }
}
@@ -122,14 +103,14 @@ async function installSampler() {
lon: (carto.longitude * 180) / Math.PI,
lat: (carto.latitude * 180) / Math.PI,
height: carto.height,
headingDeg: (cam.heading * 180) / Math.PI,
pitchDeg: (cam.pitch * 180) / Math.PI,
rollDeg: (cam.roll * 180) / Math.PI,
headingDeg: (window.__godsEyeView.engine.camera.pose().heading * 180) / Math.PI,
pitchDeg: (window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
rollDeg: (window.__godsEyeView.engine.camera.pose().roll * 180) / Math.PI,
surfaceM,
});
};
viewer.scene.postRender.addEventListener(listener);
window.__gevFlyTraceRemove = () => viewer.scene.postRender.removeEventListener(listener);
window.__godsEyeView.engine.frames.postRender.addEventListener(listener);
window.__gevFlyTraceRemove = () => window.__godsEyeView.engine.frames.postRender.removeEventListener(listener);
});
}
@@ -153,24 +134,25 @@ try {
console.log(`\nfly_route cinematic evidence — ${APP_URL}`);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__gevVoiceCommands?.runner && window.__gevAnnotations,
() => window.__godsEyeView?.engine && window.__gevVoiceCommands?.runner && window.__gevAnnotations,
{ timeout: 150000, polling: 250 },
);
await whenSettled(page);
const run = (name, args = {}) => page.evaluate(
(n, a) => window.__gevVoiceCommands.runner(n, a), name, args,
);
// ── Setup: get over downtown Austin, draw the route ───────────────────
await run('fly_to_location', { query: 'Texas State Capitol, Austin' });
await sleep(9000);
await whenSettled(page);
await run('clear_annotations', {});
await sleep(500);
await whenSettled(page);
const drawn = await run('annotate_map', {
annotations: [{
type: 'route', mode: 'driving', label: 'cinema evidence route', points: ROUTE_POINTS,
}],
});
await sleep(6000);
await whenSettled(page);
const routeInfo = await page.evaluate(() => {
const route = (window.__gevAnnotations.list?.() || []).filter((a) => a.type === 'route').at(-1);
return route ? { label: route.label, waypoints: route.path?.length ?? 0 } : null;
@@ -421,12 +403,12 @@ try {
// ── Run 2: interrupt the dolly MID-BANK ───────────────────────────────
// Cutting a level camera proves nothing about levelling, so this waits for
// the live camera to actually be rolled before it grabs the controls.
await sleep(1500);
await whenSettled(page);
await installSampler();
const second = await run('fly_route', { label: 'cinema evidence', speed: 'normal' });
report(second?.ok === true, 'second flight starts for the interrupt case');
const liveRollDeg = () => page.evaluate(
() => (window.__godsEyeView.viewer.camera.roll * 180) / Math.PI,
() => (window.__godsEyeView.engine.camera.pose().roll * 180) / Math.PI,
);
let rollBeforeCut = 0;
for (let waited = 0; waited < 90000; waited += 400) {
@@ -439,8 +421,7 @@ try {
await page.screenshot({ path: path.join(OUT_DIR, 'interrupt-0-banked.png') });
const cut = await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
const canvas = viewer.scene.canvas;
const canvas = window.__godsEyeView.engine.host.canvas;
// Read the motion slot BEFORE the cut too: under Vite a dynamic import can
// hand back a second module instance whose slot is always empty, and an
// "empty after" that was already empty before proves nothing.
@@ -452,11 +433,11 @@ try {
read = () => mod.getActiveCameraMotion?.() ?? null;
slotBefore = read();
} catch { /* dev-only module read */ }
const rollBefore = (viewer.camera.roll * 180) / Math.PI;
const rollBefore = (window.__godsEyeView.engine.camera.pose().roll * 180) / Math.PI;
window.__gevFlyTrace.marks.push({ label: 'pointerdown', t: performance.now() });
canvas.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
// Same synchronous turn as the pointerdown — no frame has rendered yet.
const rollAfter = (viewer.camera.roll * 180) / Math.PI;
const rollAfter = (window.__godsEyeView.engine.camera.pose().roll * 180) / Math.PI;
if (read) slotAfter = read();
return { slotBefore, slotAfter, rollBefore, rollAfter };
});
@@ -474,7 +455,7 @@ try {
note('motion-slot read',
'the dev server handed the harness a second module instance (HMR); the camera-side roll and freeze checks carry the proof');
}
await sleep(2500);
await whenSettled(page);
const rollAfterSettle = wrapDeg(await liveRollDeg());
report(Math.abs(rollAfterSettle) < 0.01, 'and the horizon STAYS level after the cut',
`roll ${rollAfterSettle.toFixed(4)}° 2.5 s later`);
@@ -596,7 +577,7 @@ try {
} catch (error) {
report(false, 'harness completed', String(error?.message || error).slice(0, 200));
} finally {
await browser.close();
await closeBrowser(browser);
}
const failed = results.filter((r) => r.ok === false).length;
+14 -24
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
// Cesium-only probes: native clock, tracking and manual rendering provide the Cesium focus oracle.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled } from './qa/ready.mjs';
/**
* Moving visual evidence for focus de-emphasis and aircraft recession.
*
@@ -17,11 +20,11 @@
* --headful drops those flags and is the real-GPU visual sign-off path.
*/
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import { redact } from './qa-browserEvidence.mjs';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
const argv = process.argv.slice(2);
@@ -303,6 +306,7 @@ async function waitForApp(page) {
() => Boolean(window.__godsEyeView?.layers && window.__godsEyeView?.dataManager),
{ timeout: 60_000, polling: 200 },
);
await whenSettled(page);
await page.evaluate(async () => { window.__focusGeo = await import('/src/geo/index.js'); });
}
@@ -336,7 +340,7 @@ async function requireEvidenceSeams(page) {
}
async function setCamera(page, values) {
await page.evaluate(([lon, lat, height, heading, pitch]) => {
await evaluateWithGeo(page, async (qaGeo, [lon, lat, height, heading, pitch]) => {
const app = window.__godsEyeView;
if (!app.viewer) {
const geo = window.__focusGeo;
@@ -349,12 +353,10 @@ async function setCamera(page, values) {
return;
}
const viewer = app.viewer;
const ellipsoid = viewer.scene.globe.ellipsoid;
viewer.trackedEntity = undefined;
viewer.camera.cancelFlight?.();
viewer.scene.tweens?.removeAll?.();
viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
window.__godsEyeView.engine.camera.cancelFlight?.();
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: lon * Math.PI / 180,
latitude: lat * Math.PI / 180,
height,
@@ -654,11 +656,7 @@ async function browserContext(page, browser) {
platform: os.platform(),
arch: os.arch(),
},
caveat: HEADFUL
? 'Headful capture omits SwiftShader flags and is the real-GPU visual sign-off path.'
: process.env.QA_ANGLE === 'metal'
? 'Headless ANGLE/Metal capture; renderer and DPR recorded above.'
: 'SwiftShader/headless output is relative-only.',
caveat: `Headless capture with QA_ANGLE=${process.env.QA_ANGLE || 'metal'}; renderer and DPR recorded above.`,
};
}
@@ -671,18 +669,10 @@ async function main() {
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
fs.mkdirSync(path.dirname(JSON_PATH), { recursive: true });
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
const browser = await launchBrowser({
headless: true,
protocolTimeout: 300000,
args: [
...(HEADFUL ? [] : process.env.QA_ANGLE ? ['--use-gl=angle', `--use-angle=${process.env.QA_ANGLE}`] : ['--enable-unsafe-swiftshader', '--use-gl=swiftshader']),
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
`--window-size=${VIEWPORT.width},${VIEWPORT.height}`,
],
args: [...(HEADFUL ? [] : process.env.QA_ANGLE ? [] : ['--enable-unsafe-swiftshader']), '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', `--window-size=${VIEWPORT.width},${VIEWPORT.height}`],
});
const page = await browser.newPage();
await stubPlanespotters(page);
@@ -740,7 +730,7 @@ async function main() {
}
} finally {
if (!page.isClosed()) await releaseFrameClock(page).catch(() => {});
await browser.close();
await closeBrowser(browser);
}
fs.writeFileSync(JSON_PATH, `${JSON.stringify(report, null, 2)}\n`);
+9 -10
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node
/** Native form-field regression gate. CCTV ports are fixtures, never live writes. */
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
const browser = await launchBrowser({
headless: true,
args: ['--use-gl=angle', '--use-angle=swiftshader'],
args: [],
});
const page = await browser.newPage();
page.setDefaultNavigationTimeout(60000);
@@ -29,11 +30,10 @@ try {
},
);
await page.waitForFunction(
() =>
window.__godsEyeView?.styleManager?._cctvControls &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
() => window.__godsEyeView?.styleManager?._cctvControls,
{ timeout: 60000 },
);
await whenSettled(page);
const inventory = await page.evaluate(() => {
const selects = [
'scene-select',
@@ -400,11 +400,10 @@ try {
localized.searchParams.set('lang', 'ru');
await page.goto(localized.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__godsEyeView?.styleManager?._cctvControls &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
() => window.__godsEyeView?.styleManager?._cctvControls,
{ timeout: 60000 },
);
await whenSettled(page);
await page.evaluate(() => {
for (const panel of ['pp-toggles', 'scene-panel', 'cctv-panel'])
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
@@ -439,6 +438,6 @@ try {
await page.screenshot({ path: 'qa-shots/form-fields/ru-390.png' });
check('no uncaught browser errors', errors.length === 0, errors);
} finally {
await browser.close();
await closeBrowser(browser);
}
process.exitCode = failures ? 1 : 0;
+7 -19
View File
@@ -5,10 +5,11 @@
* --w2 adds raster checks that import source modules: dev server only.
* GBFS documents are synthetic; no operator feed or provider key is required.
*/
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import { fromDegrees } from '../src/geo/cartesian3.js';
import { watchBrowserPage, redact } from './qa-browserEvidence.mjs';
@@ -67,15 +68,9 @@ const status = {
})),
},
};
const browser = await puppeteer.launch({
headless: 'new',
...(process.env.PUPPETEER_EXECUTABLE_PATH
? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH }
: {}),
args:
process.platform === 'darwin'
? ['--use-angle=metal']
: ['--use-gl=angle', '--use-angle=swiftshader'],
const browser = await launchBrowser({
headless: true,
args: process.platform === 'darwin' ? [] : [],
});
let checks = 0;
const check = (name, actual, expected) => {
@@ -151,14 +146,7 @@ async function open(mode = 'normal') {
window.__godsEyeView?.dataManager && window.__godsEyeView?.voiceCommands,
{ timeout: 60000 },
);
await page.waitForFunction(
() =>
!document.getElementById('loading-screen') ||
(document.getElementById('loading-screen').classList.contains('hidden') &&
getComputedStyle(document.getElementById('loading-screen')).opacity ===
'0'),
{ timeout: 60000 },
);
await whenSettled(page);
await page.evaluate(
async (destination) => {
const { dataManager, engine } = window.__godsEyeView;
@@ -619,5 +607,5 @@ try {
} finally {
if (reportPath)
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
await browser.close();
await closeBrowser(browser);
}
+1
View File
@@ -30,6 +30,7 @@ function fixture({ extended, dev }) {
information: {},
status: {},
fromDegrees: () => ({}),
whenSettled: async () => {},
};
return new Function(
...Object.keys(dependencies),
+27 -34
View File
@@ -1,12 +1,14 @@
#!/usr/bin/env node
// Cesium-only probes: KML DataSources, imagery and the Julian clock form the Cesium reference oracle.
/** Large KML acceptance: style equivalence, 27.6 MB import and optional real file.
* QA_BASE_URL defaults to http://localhost:4173. Optional --file /path/file.kml.
* For a USGS fixture with blocked icons, add --expect-local-pins to check paint.
*/
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { installImportProbe } from './qa-geo-import-probe.mjs';
import { hookRenderErrors, readRenderErrors } from './qa-browserEvidence.mjs';
@@ -61,12 +63,9 @@ const small = wrap(
)
.join(''),
);
const browser = await puppeteer.launch({
const browser = await launchBrowser({
headless: true,
args:
process.platform === 'darwin'
? ['--use-gl=angle', '--use-angle=metal']
: ['--no-sandbox', '--use-gl=angle', '--use-angle=swiftshader'],
args: process.platform === 'darwin' ? [] : ['--no-sandbox'],
});
const page = await browser.newPage();
await hookRenderErrors(page);
@@ -88,12 +87,8 @@ const check = (name, ok, detail) => {
try {
await page.setViewport({ width: 1440, height: 900 });
await page.goto(`${base}/?welcome=0`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__gevGeoImport &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60000 },
);
await page.waitForFunction(() => window.__gevGeoImport, { timeout: 60000 });
await whenSettled(page);
await installImportProbe(page);
await page.evaluate(async () => {
const { viewer, dataManager } = window.__godsEyeView;
@@ -103,8 +98,8 @@ try {
viewer.clock.shouldAnimate = false;
viewer.imageryLayers.removeAll();
viewer.terrainProvider = new C.EllipsoidTerrainProvider();
viewer.camera.cancelFlight();
viewer.camera.setView({
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: C.Cartesian3.fromDegrees(-122.5, 37.72, 50000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
@@ -222,7 +217,7 @@ try {
el.remove();
const source = await C.KmlDataSource.load(doc, {
camera: viewer.scene.camera,
canvas: viewer.scene.canvas,
canvas: window.__godsEyeView.engine.host.canvas,
clampToGround: true,
});
for (const e of source.importedEntities || source.entities.values)
@@ -231,7 +226,6 @@ try {
};
});
const equivalent = await page.evaluate(async (text) => {
const { viewer } = window.__godsEyeView;
const control = await window.__qaUnsharedKml(text);
const outcomes = await window.__gevGeoImport.importFiles([
new File([text], 'style-equivalence.kml'),
@@ -269,9 +263,8 @@ try {
page.evaluate(async () => {
const C = window.__CESIUM__,
{ viewer } = window.__godsEyeView;
const scene = viewer.scene;
viewer.camera.cancelFlight();
viewer.camera.setView({
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: C.Cartesian3.fromDegrees(-122.5, 37.72, 50000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
@@ -279,13 +272,14 @@ try {
samples = [];
let last,
frames = 0;
const remove = scene.postRender.addEventListener(() => {
const t = performance.now();
if (t - started < 2000) return;
frames++;
if (last !== undefined) samples.push(t - last);
last = t;
});
const remove =
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
const t = performance.now();
if (t - started < 2000) return;
frames++;
if (last !== undefined) samples.push(t - last);
last = t;
});
await new Promise((resolve) => {
let previous = performance.now();
function tick(t) {
@@ -294,7 +288,7 @@ try {
Math.min(t - previous, 100) * 0.0000002,
);
previous = t;
scene.requestRender();
window.__godsEyeView.engine.render.request();
if (t - started < 7000) requestAnimationFrame(tick);
else resolve();
}
@@ -302,7 +296,7 @@ try {
});
remove();
samples.sort((a, b) => a - b);
const gl = scene.context._gl,
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2'),
debug = gl.getExtension('WEBGL_debug_renderer_info');
return {
fps: frames / 5,
@@ -401,8 +395,7 @@ try {
);
if (suppliedItem && process.argv.includes('--expect-local-pins')) {
const markers = await page.evaluate(() => {
const { viewer } = window.__godsEyeView,
C = window.__CESIUM__;
const C = window.__CESIUM__;
const sets = window.__qaImports
.records()
.filter((record) => record.kind === 'movers');
@@ -412,8 +405,8 @@ try {
.flatMap((record) => record.items);
if (images.length) {
const at = C.Cartographic.fromCartesian(images[0].position);
viewer.camera.cancelFlight();
viewer.camera.setView({
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: C.Cartesian3.fromRadians(
at.longitude,
at.latitude,
@@ -462,7 +455,7 @@ try {
{ id: suppliedItem.id, visible },
);
await setVisible(false);
await new Promise((resolve) => setTimeout(resolve, 1000));
await whenSettled(page);
const hidden = await yellowPixels();
await setVisible(true);
let shown = hidden;
@@ -510,6 +503,6 @@ try {
);
console.log('Fixture retained at', file);
} finally {
await browser.close();
await closeBrowser(browser);
}
process.exitCode = failures ? 1 : 0;
+26 -25
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env node
// Cesium-only probes: primitive failure injection and terrain isolation exercise the Cesium import adapter.
/** Import regressions through the real UI controller and globe primitives.
* Run against dev and preview: QA_BASE_URL=http://localhost:4173 node scripts/qa-geo-import-regressions.mjs
*/
import puppeteer from 'puppeteer';
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import * as Cesium from 'cesium';
import { installImportProbe } from './qa-geo-import-probe.mjs';
import { hookRenderErrors, readRenderErrors } from './qa-browserEvidence.mjs';
const browser = await puppeteer.launch({ headless: true });
const browser = await launchBrowser({ headless: true });
const page = await browser.newPage();
const meshBytes = Buffer.alloc(60);
[
@@ -92,12 +94,8 @@ try {
`${process.argv.find((arg) => /^https?:/.test(arg)) || process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
{ waitUntil: 'domcontentloaded' },
);
await page.waitForFunction(
() =>
window.__gevGeoImport &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 90000 },
);
await page.waitForFunction(() => window.__gevGeoImport, { timeout: 90000 });
await whenSettled(page);
await page.evaluate(async () => {
const { viewer, dataManager } = window.__godsEyeView,
C = window.__CESIUM__;
@@ -106,8 +104,8 @@ try {
viewer.imageryLayers.removeAll();
viewer.terrainProvider = new C.EllipsoidTerrainProvider();
viewer.clock.shouldAnimate = false;
viewer.camera.cancelFlight();
viewer.camera.setView({
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: C.Cartesian3.fromDegrees(-122.5, 37.7, 100000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
@@ -198,7 +196,7 @@ try {
Math.abs(pinHeight()) >= 0.01 &&
performance.now() < groundDeadline
) {
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
await new Promise((resolve) => setTimeout(resolve, 100));
}
const pin = pins(r)[0],
@@ -402,11 +400,13 @@ try {
check(
'symmetric global extents keep a finite camera',
r.outcome.ok &&
Number.isFinite(viewer.camera.positionCartographic.height),
Number.isFinite(
window.__godsEyeView.engine.camera.pose().cartographic.height,
),
);
remove(r);
viewer.camera.cancelFlight();
viewer.camera.setView({
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: C.Cartesian3.fromDegrees(-122.5, 37.7, 100000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
@@ -418,17 +418,18 @@ try {
// Let the new camera/tiles reach a rendered frame before testing sampling.
await new Promise((resolve) => {
let frames = 0;
const remove = viewer.scene.postRender.addEventListener(() => {
if (++frames >= 2) {
remove();
resolve();
} else viewer.scene.requestRender();
});
viewer.scene.requestRender();
const remove =
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
if (++frames >= 2) {
remove();
resolve();
} else window.__godsEyeView.engine.render.request();
});
window.__godsEyeView.engine.render.request();
});
const fixtureDeadline = performance.now() + 5000;
while (!mesh.tilesLoaded && performance.now() < fixtureDeadline) {
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
await new Promise((resolve) => setTimeout(resolve, 50));
}
const globeShown = viewer.scene.globe.show;
@@ -461,8 +462,8 @@ try {
{
screen: meshScreen && [meshScreen.x, meshScreen.y],
camera: {
height: viewer.camera.positionCartographic.height,
pitch: viewer.camera.pitch,
height: window.__godsEyeView.engine.camera.pose().cartographic.height,
pitch: window.__godsEyeView.engine.camera.pose().pitch,
},
meshHeight,
},
@@ -551,6 +552,6 @@ try {
);
if (unexpected.length) failed++;
} finally {
await browser.close();
await closeBrowser(browser);
}
process.exitCode = failed ? 1 : 0;
+21 -24
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node
// Cesium-only probes: imagery, terrain and the native timeline validate Cesium import integration.
/**
* Rendered acceptance for UTILITIES ▸ Import: real files through the real file
* input and a real drop, drawn through the globe contract.
@@ -19,10 +20,11 @@
* Usage: dev server up (`npm run dev`), then `node scripts/qa-geo-import.mjs`.
* QA_BASE_URL (default http://localhost:4173), QA_SHOT_DIR, QA_HEADFUL=1.
*/
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { installImportProbe } from './qa-geo-import-probe.mjs';
import { hookRenderErrors, readRenderErrors } from './qa-browserEvidence.mjs';
@@ -165,15 +167,13 @@ const FILES = [
/* ── harness ───────────────────────────────────────────────────────────── */
const browser = await puppeteer.launch({
headless: !HEADFUL,
const browser = await launchBrowser({
headless: true,
args: [
'--no-sandbox',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
],
});
const page = await browser.newPage();
@@ -211,8 +211,8 @@ const CLIP = { x: 380, y: 240, width: 480, height: 320 };
/** Pixels in the clip close to the first import color (#39d0ff). */
async function cyanPixels() {
await page.evaluate(() => window.__godsEyeView.viewer.scene.requestRender());
await wait(600);
await page.evaluate(() => window.__godsEyeView.engine.render.request());
await whenSettled(page);
const base64 = await page.screenshot({
clip: CLIP,
type: 'png',
@@ -247,12 +247,10 @@ try {
await page.setViewport({ width: 1280, height: 860 });
await page.goto(`${BASE_URL}/?welcome=0`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__gevGeoImport &&
window.__godsEyeView?.viewer &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
() => window.__gevGeoImport && window.__godsEyeView?.engine,
{ timeout: 90_000 },
);
await whenSettled(page);
await page.evaluate(async (view) => {
const viewer = window.__godsEyeView.viewer;
for (const id of window.__godsEyeView.dataManager.layers.keys())
@@ -261,8 +259,8 @@ try {
viewer.terrainProvider = new window.__CESIUM__.EllipsoidTerrainProvider();
viewer.scene.globe.baseColor = window.__CESIUM__.Color.BLACK;
viewer.scene.globe.enableLighting = false;
viewer.camera.cancelFlight?.();
viewer.camera.setView({
window.__godsEyeView.engine.camera.cancelFlight?.();
window.__godsEyeView.engine.camera.setView({
destination: window.__CESIUM__.Cartesian3.fromDegrees(
view.lon,
view.lat,
@@ -270,9 +268,9 @@ try {
),
orientation: { heading: 0, pitch: (view.pitch * Math.PI) / 180, roll: 0 },
});
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
}, VIEW);
await wait(4000);
await whenSettled(page);
await page.evaluate(() => {
const panel = document.getElementById('utilities-panel');
if (panel?.classList.contains('collapsed'))
@@ -367,9 +365,8 @@ try {
// Over the files: their pins on the ground show unless beyond the
// horizon, so all of them show here.
await page.evaluate((view) => {
const viewer = window.__godsEyeView.viewer;
viewer.camera.cancelFlight?.();
viewer.camera.setView({
window.__godsEyeView.engine.camera.cancelFlight?.();
window.__godsEyeView.engine.camera.setView({
destination: window.__CESIUM__.Cartesian3.fromDegrees(
view.lon,
view.lat,
@@ -378,7 +375,7 @@ try {
orientation: { heading: 0, pitch: (view.pitch * Math.PI) / 180, roll: 0 },
});
}, VIEW);
await wait(2500);
await whenSettled(page);
const inside = await page.evaluate(() => {
const groups = window.__qaImports.groups();
const pins = (group) =>
@@ -506,7 +503,7 @@ try {
// Dropped onto the globe, as a person drags from their file manager.
await page.evaluate((text) => {
const container = window.__godsEyeView.viewer.container;
const container = window.__godsEyeView.engine.host.container;
const transfer = new DataTransfer();
transfer.items.add(
new File([text], 'dropped.geojson', { type: 'application/geo+json' }),
@@ -536,7 +533,7 @@ try {
transfer.items.add(
new File(['just some notes'], 'notes.txt', { type: 'text/plain' }),
);
window.__godsEyeView.viewer.container.dispatchEvent(
window.__godsEyeView.engine.host.container.dispatchEvent(
new DragEvent('drop', {
dataTransfer: transfer,
bubbles: true,
@@ -544,7 +541,7 @@ try {
}),
);
});
await wait(800);
await whenSettled(page);
const refused = await state();
check(
'a file that is not geodata is refused with a reason',
@@ -576,7 +573,7 @@ try {
} catch (error) {
check('harness ran to completion', false, error?.stack || String(error));
} finally {
await browser.close();
await closeBrowser(browser);
fs.rmSync(dir, { recursive: true, force: true });
}
console.log(failures ? `\n${failures} check(s) failed` : '\nall checks passed');
+60 -59
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
// Cesium-only probes: native primitive failure injection tests the Cesium paint path.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled, whenFramesDrawn, whenTilesLoaded } from './qa/ready.mjs';
/**
* qa-ghostmaps.mjs — headless proof for the S2 Underground GhostMaps layer
* (id `s2-ghostmaps`): S2's public KMZ intel products, kept current from
@@ -39,7 +42,7 @@
* Exits non-zero on any FAIL. Does not commit anything.
*/
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -64,7 +67,6 @@ function record(name, ok, detail) {
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function boot(page) {
await page.goto(`${APP_URL}/?welcome=0`, {
@@ -72,10 +74,11 @@ async function boot(page) {
timeout: TIMEOUT_MS,
});
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
{ timeout: TIMEOUT_MS },
);
await sleep(5000);
await whenSettled(page);
await whenFramesDrawn(page);
await page.keyboard.press('Escape');
}
@@ -186,14 +189,14 @@ async function showRow(page) {
.querySelector(`[data-layer-id="${id}"]`)
?.scrollIntoView({ block: 'center' });
}, LAYER_ID);
await sleep(400);
await whenFramesDrawn(page);
}
async function main() {
fs.mkdirSync(SHOTS_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: !HEADFUL,
args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'],
const browser = await launchBrowser({
headless: true,
args: ['--enable-unsafe-swiftshader'],
defaultViewport: { width: 1440, height: 900 },
});
try {
@@ -330,6 +333,7 @@ async function main() {
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
timeout: TIMEOUT_MS,
});
await whenSettled(page);
await enableAndSettle(page);
const persistedAge = await stats(page);
record(
@@ -351,41 +355,38 @@ async function main() {
// ---- (ii) PICK ----------------------------------------------------------
await installPins(page);
const target = await page.evaluate((id) => {
const gev = window.__godsEyeView;
const pin = window.__qaGhostPins(`${id}:cip:`)[0];
const carto = gev.viewer.scene.globe.ellipsoid.cartesianToCartographic(
pin.position,
);
try {
gev.viewer.camera.cancelFlight();
} catch {
/* no flight */
}
gev.viewer.camera.setView({
destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian({
longitude: carto.longitude,
latitude: carto.latitude,
height: 40000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
gev.viewer.scene.requestRender?.();
return pin.id;
}, LAYER_ID);
await page
.waitForFunction(
() => window.__godsEyeView.viewer.scene.globe.tilesLoaded,
{ timeout: 30000 },
)
.catch(() => {});
const target = await evaluateWithGeo(
page,
async (qaGeo, id) => {
const pin = window.__qaGhostPins(`${id}:cip:`)[0];
const carto = qaGeo.cartographic.fromCartesian(pin.position);
try {
window.__godsEyeView.engine.camera.cancelFlight();
} catch {
/* no flight */
}
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: carto.longitude,
latitude: carto.latitude,
height: 40000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
window.__godsEyeView.engine.render.request?.();
return pin.id;
},
LAYER_ID,
);
await whenTilesLoaded(page, { timeoutMs: 30000 }).catch(() => {});
await whenSettled(page);
// The horizon cull reveals the pin on the frame after the camera moves.
await page
.waitForFunction(
() => {
const { viewer: v, engine } = window.__godsEyeView;
v.scene.requestRender?.();
const c = v.scene.canvas;
const { engine } = window.__godsEyeView;
window.__godsEyeView.engine.render.request?.();
const c = window.__godsEyeView.engine.host.canvas;
return engine.picker
.drillPick({ x: c.clientWidth / 2, y: c.clientHeight / 2 }, 8)
.some((p) => String(p?.id ?? '').startsWith('ghostmaps:cip:'));
@@ -394,8 +395,8 @@ async function main() {
)
.catch(() => {});
const probe = await page.evaluate(() => {
const { viewer: v, engine } = window.__godsEyeView;
const c = v.scene.canvas;
const { engine } = window.__godsEyeView;
const c = window.__godsEyeView.engine.host.canvas;
const box = c.getBoundingClientRect();
const centre = { x: c.clientWidth / 2, y: c.clientHeight / 2 };
const picked = engine.picker.drillPick(centre, 8);
@@ -486,27 +487,26 @@ async function main() {
// far side. A pin clearly behind the planet (sphere test with margin) must
// be hidden, and pins facing the camera must still show.
const horizonCounts = (lon, lat) =>
page.evaluate(
async (id, lo, la) => {
const gev = window.__godsEyeView;
evaluateWithGeo(
page,
async (qaGeo, id, lo, la) => {
const d2r = Math.PI / 180;
try {
gev.viewer.camera.cancelFlight();
window.__godsEyeView.engine.camera.cancelFlight();
} catch {
/* no flight */
}
gev.viewer.camera.setView({
destination:
gev.viewer.scene.globe.ellipsoid.cartographicToCartesian({
longitude: lo * d2r,
latitude: la * d2r,
height: 18_000_000,
}),
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: lo * d2r,
latitude: la * d2r,
height: 18_000_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
gev.viewer.scene.requestRender?.();
window.__godsEyeView.engine.render.request?.();
await new Promise((r) => setTimeout(r, 1200));
const c = gev.viewer.camera.positionWC;
const c = window.__godsEyeView.engine.camera.pose().position;
const norm = (v) => Math.hypot(v.x, v.y, v.z);
let far = 0;
let nearShown = 0;
@@ -583,7 +583,8 @@ async function main() {
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
timeout: TIMEOUT_MS,
});
await sleep(3000);
await whenSettled(page);
await whenFramesDrawn(page);
await enableAndSettle(page);
const kept = await stats(page);
record(
@@ -612,7 +613,7 @@ async function main() {
(id) => window.__godsEyeView.dataManager.setEnabled(id, false),
LAYER_ID,
);
await sleep(500);
await whenFramesDrawn(page);
record(
'DISABLE: every GhostMaps data source leaves the viewer',
(await sources(page)).length === 0,
@@ -679,7 +680,7 @@ async function main() {
{ timeout: 20000, polling: 100 },
LAYER_ID,
);
await sleep(500);
await whenFramesDrawn(page);
const second = await identity();
record(
'PUSH: a new edition reloads only that product',
@@ -698,7 +699,7 @@ async function main() {
.filter((s) => !s.closed)
.forEach((s) => s.emit(catalog));
});
await sleep(1000);
await whenFramesDrawn(page);
const third = await identity();
record(
'PUSH: a product leaving the catalog leaves the globe',
@@ -738,7 +739,7 @@ async function main() {
});
await phone.close();
} finally {
await browser.close();
await closeBrowser(browser);
}
const failed = results.filter((r) => !r.ok);
console.log(
+64 -74
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
// Cesium-only probes: native ground-primitive failure injection tests the Cesium paint path.
import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
/**
* qa-gnss.mjs — headless proof for the GNSS Integrity layer
* (id `gnss-integrity`): low navigation accuracy reported by ADS-B.
@@ -45,7 +48,7 @@
* Exits non-zero on any FAIL. Does not commit anything.
*/
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -74,7 +77,6 @@ function record(name, ok, detail) {
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** Boot the app, dismiss the intro and place the camera straight down. */
async function boot(page, { lon, lat, height }) {
@@ -83,30 +85,30 @@ async function boot(page, { lon, lat, height }) {
timeout: TIMEOUT_MS,
});
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
{ timeout: TIMEOUT_MS },
);
await sleep(5000);
await whenSettled(page);
await page.keyboard.press('Escape');
await page.evaluate(
(lo, la, h) => {
const gev = window.__godsEyeView;
await evaluateWithGeo(
page,
async (qaGeo, lo, la, h) => {
const d2r = Math.PI / 180;
// The app's intro flyTo animation clobbers a setView issued mid-flight.
try {
gev.viewer.camera.cancelFlight();
window.__godsEyeView.engine.camera.cancelFlight();
} catch {
/* no flight active */
}
gev.viewer.camera.setView({
destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian({
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: lo * d2r,
latitude: la * d2r,
height: h,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
gev.viewer.scene.requestRender?.();
window.__godsEyeView.engine.render.request?.();
},
lon,
lat,
@@ -168,7 +170,7 @@ async function showLayerRow(page) {
.querySelector(`[data-layer-id="${id}"]`)
?.scrollIntoView({ block: 'center' });
}, LAYER_ID);
await sleep(800);
await whenSettled(page);
}
/** Actual card pixels on the shared canvas, available in dev and production. */
@@ -184,12 +186,8 @@ function paintedOverlayPixels() {
}
async function shoot(page, name) {
await page
.waitForFunction(
() => window.__godsEyeView.viewer.scene.globe.tilesLoaded,
{ timeout: TIMEOUT_MS },
)
.catch(() => {});
await whenTilesLoaded(page, { timeoutMs: TIMEOUT_MS }).catch(() => {});
await whenSettled(page);
await showLayerRow(page);
await page.screenshot({ path: path.join(SHOTS_DIR, `gnss-${name}.png`) });
}
@@ -367,18 +365,9 @@ async function main() {
process.exit(2);
}
fs.mkdirSync(SHOTS_DIR, { recursive: true });
const executablePath =
process.env.PUPPETEER_EXECUTABLE_PATH ||
(await puppeteer.executablePath().catch(() => null)) ||
undefined;
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(executablePath && fs.existsSync(executablePath)
? { executablePath }
: {}),
const browser = await launchBrowser({
headless: true,
args: [
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
@@ -530,7 +519,7 @@ async function main() {
await shoot(page, 'synthetic-bands');
// GNSS fills must not own map input, including over a real perimeter.
await page.evaluate(async () => {
await evaluateWithGeo(page, async (qaGeo) => {
const original = window.fetch.bind(window);
window.fetch = (input, init) => {
const url = typeof input === 'string' ? input : input.url;
@@ -562,10 +551,10 @@ async function main() {
);
return original(input, init);
};
const { viewer, dataManager } = window.__godsEyeView;
viewer.camera.cancelFlight();
viewer.camera.setView({
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
const { dataManager } = window.__godsEyeView;
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: (18.25 * Math.PI) / 180,
latitude: (54.25 * Math.PI) / 180,
height: 2500,
@@ -573,15 +562,15 @@ async function main() {
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await dataManager.setEnabled('fire-perimeters', true);
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
});
await page.waitForFunction(
() => {
// The engine's pick names the perimeter ground item's pickId.
const { viewer: v, engine } = window.__godsEyeView,
const { engine } = window.__godsEyeView,
p = engine.picker.pick({
x: v.scene.canvas.clientWidth / 2,
y: v.scene.canvas.clientHeight / 2,
x: window.__godsEyeView.engine.host.canvas.clientWidth / 2,
y: window.__godsEyeView.engine.host.canvas.clientHeight / 2,
});
return String(p?.id?.id ?? p?.id ?? '').startsWith(
'fire-perimeter:gnss-pick-qa:',
@@ -590,8 +579,8 @@ async function main() {
{ timeout: TIMEOUT_MS, polling: 250 },
);
const pickProbe = await page.evaluate(() => {
const { viewer: v, engine } = window.__godsEyeView,
c = v.scene.canvas;
const { engine } = window.__godsEyeView,
c = window.__godsEyeView.engine.host.canvas;
const centre = { x: c.clientWidth / 2, y: c.clientHeight / 2 };
const empty = { x: centre.x + 300, y: centre.y };
const picked = engine.picker.pick(empty),
@@ -600,7 +589,7 @@ async function main() {
centre: { x: centre.x + box.left, y: centre.y + box.top },
empty: { x: empty.x + box.left, y: empty.y + box.top },
surface: !picked || picked.id === undefined,
height: v.camera.positionCartographic.height,
height: window.__godsEyeView.engine.camera.pose().cartographic.height,
};
});
record('PICK: a perimeter remains selectable under GNSS', true);
@@ -637,13 +626,13 @@ async function main() {
await page.mouse.click(pickProbe.empty.x, pickProbe.empty.y, {
clickCount: 2,
});
await sleep(1000);
await whenSettled(page);
record(
'PICK: empty double-click does not zoom to a GNSS cell',
await page.evaluate(
(height) =>
Math.abs(
window.__godsEyeView.viewer.camera.positionCartographic.height -
window.__godsEyeView.engine.camera.pose().cartographic.height -
height,
) < 200,
pickProbe.height,
@@ -659,17 +648,16 @@ async function main() {
const beforeMove = await page.evaluate(
() => window.__gnssQaRequests.length,
);
await page.evaluate(() => {
const { viewer } = window.__godsEyeView;
viewer.camera.setView({
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
await evaluateWithGeo(page, async (qaGeo) => {
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: (139.7 * Math.PI) / 180,
latitude: (35.7 * Math.PI) / 180,
height: 1_100_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
});
await page.waitForFunction(
() => {
@@ -684,7 +672,7 @@ async function main() {
},
{ timeout: 15000 },
);
await sleep(3500);
await whenSettled(page);
const moved = await page.evaluate(
(id) => ({
requests: window.__gnssQaRequests.length,
@@ -708,17 +696,16 @@ async function main() {
JSON.stringify(moved),
);
await shoot(page, 'camera-refresh');
await page.evaluate(() => {
const { viewer } = window.__godsEyeView;
viewer.camera.setView({
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
await evaluateWithGeo(page, async (qaGeo) => {
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: (12.4 * Math.PI) / 180,
latitude: (41.9 * Math.PI) / 180,
height: 16_000_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
});
await page.waitForFunction(
() => {
@@ -732,19 +719,23 @@ async function main() {
},
{ timeout: 15000 },
);
const world = await page.evaluate((id) => {
const { viewer, dataManager } = window.__godsEyeView;
const bounds = viewer.camera.computeViewRectangle(
viewer.scene.globe.ellipsoid,
);
return {
enabled: dataManager.isEnabled(id),
height: viewer.camera.positionCartographic.height,
boundsMidLongitude:
bounds && ((bounds.west + bounds.east) * 90) / Math.PI,
coverage: dataManager.layers.get(id).module.getStats().coverage,
};
}, LAYER_ID);
const world = await evaluateWithGeo(
page,
async (qaGeo, id) => {
const { dataManager } = window.__godsEyeView;
const bounds = window.__godsEyeView.engine.camera.viewRectangle(
qaGeo.ellipsoid.WGS84,
);
return {
enabled: dataManager.isEnabled(id),
height: window.__godsEyeView.engine.camera.pose().cartographic.height,
boundsMidLongitude:
bounds && ((bounds.west + bounds.east) * 90) / Math.PI,
coverage: dataManager.layers.get(id).module.getStats().coverage,
};
},
LAYER_ID,
);
record(
'GLOBE: zoomed-out sampling stays centred on Rome and exposes its regional radius',
world.enabled &&
@@ -755,17 +746,16 @@ async function main() {
JSON.stringify(world),
);
await shoot(page, 'globe-regional');
await page.evaluate(() => {
const { viewer } = window.__godsEyeView;
viewer.camera.setView({
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
await evaluateWithGeo(page, async (qaGeo) => {
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: (18.6 * Math.PI) / 180,
latitude: (54.6 * Math.PI) / 180,
height: 450_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
});
await page.waitForFunction(
() => {
@@ -780,7 +770,7 @@ async function main() {
},
{ timeout: 15000 },
);
await sleep(3500);
await whenSettled(page);
// ── (iii) FAILURE ─────────────────────────────────────────────────────
console.log('\n(iii) FAILURE — /api/gnss-integrity stubbed to 502...');
@@ -990,7 +980,7 @@ async function main() {
console.error('\x1b[31mHarness error:\x1b[0m', err);
exitCode = 3;
} finally {
await browser.close();
await closeBrowser(browser);
}
const pass = results.filter((r) => r.ok).length;
+40 -53
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env node
// Cesium-only probes: native camera rotation preserves the Cesium orbit performance measurement.
import { evaluateWithGeo } from './qa/geometry.mjs';
/**
* qa-infra-lod — browser checks for the local-infrastructure LOD declutter.
*
@@ -24,7 +26,8 @@
* rAF throttling disabled so the frame clock is honest. Exits non-zero on any
* FAIL. Commits nothing.
*/
import puppeteer from 'puppeteer';
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -59,18 +62,10 @@ function report(name, detail) {
console.log(` [MEAS] ${name} — ${JSON.stringify(detail)}`);
}
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
const browser = await launchBrowser({
headless: true,
protocolTimeout: 300_000,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--window-size=1440,900',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-background-timer-throttling',
],
args: ['--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-timer-throttling'],
});
try {
@@ -81,8 +76,8 @@ try {
const testUrl = new URL(APP_URL);
testUrl.searchParams.set('welcome', '0');
await page.goto(testUrl.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__godsEyeView?.styleManager
&& document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 90_000 });
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 90_000 });
await whenSettled(page);
// The startup view restore and any boot flight land after the loading
// screen; park only once they are over, or they move the camera away from
// the full-earth view before the first LOD walk.
@@ -101,20 +96,18 @@ try {
!document.querySelector('#first-run-launcher:not([hidden])')
)));
report('renderer', await page.evaluate(() => {
const gl = window.__godsEyeView.viewer.scene.context._gl;
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
const debug = gl.getExtension('WEBGL_debug_renderer_info');
return debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
}));
// Park at a full-earth view and disable every layer so infra is measured
// in isolation.
await page.evaluate(async () => {
await evaluateWithGeo(page, async (qaGeo) => {
const gev = window.__godsEyeView;
const v = gev.viewer;
v.camera.cancelFlight();
const ell = v.scene.globe.ellipsoid;
v.camera.setView({
destination: ell.cartographicToCartesian({
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: 0, latitude: 15 * Math.PI / 180, height: 24_000_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
@@ -125,7 +118,7 @@ try {
}
}
});
await new Promise((r) => setTimeout(r, 4_000));
await whenSettled(page);
// Enable the infrastructure layers (skipped in --control so the orbit cost
// measures the empty scene for attribution).
@@ -149,7 +142,7 @@ try {
return (s.count || 0) > 0 || s.error;
});
if (done) break;
gev.viewer.scene.requestRender?.();
window.__godsEyeView.engine.render.request?.();
await new Promise((r) => setTimeout(r, 200));
}
const out = {};
@@ -161,8 +154,8 @@ try {
check(`${id} loaded its bundled dataset`, (loaded[id]?.count || 0) > 0, loaded[id]);
}
// Nudge a render pass so the first post-enable LOD walk runs.
await page.evaluate(() => window.__godsEyeView?.viewer?.scene?.requestRender?.());
await new Promise((r) => setTimeout(r, 2_000));
await page.evaluate(() => window.__godsEyeView.engine.render.request?.());
await whenSettled(page);
}
// ── GATE a: full-earth budget engaged ─────────────────────────────────
@@ -204,21 +197,19 @@ try {
}
// ── GATE b: zoom in widens the budget ───────────────────────────────
await page.evaluate(() => {
const v = window.__godsEyeView.viewer;
const ell = v.scene.globe.ellipsoid;
v.camera.cancelFlight();
v.camera.setView({
destination: ell.cartographicToCartesian({
await evaluateWithGeo(page, async (qaGeo) => {
window.__godsEyeView.engine.camera.cancelFlight();
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: -97.74 * Math.PI / 180, latitude: 30.27 * Math.PI / 180, height: 55_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 3, roll: 0 },
});
// setView does not fire moveEnd; raise the LOD recompute trigger.
v.camera.moveEnd.raiseEvent?.();
v.scene.requestRender?.();
window.__godsEyeView.engine.camera.moveEnd.raiseEvent?.();
window.__godsEyeView.engine.render.request?.();
});
await new Promise((r) => setTimeout(r, 3_000));
await whenSettled(page);
const regionalLod = await readLod('getLodDiagnostics @ city');
for (const id of INFRA_LAYER_IDS) {
const g = globalLod[id];
@@ -240,7 +231,7 @@ try {
.slice().sort());
const before = snap();
for (let i = 0; i < 5; i++) {
gev.viewer.scene.requestRender?.();
window.__godsEyeView.engine.render.request?.();
await new Promise((r) => setTimeout(r, 120));
}
return { before, after: snap() };
@@ -251,29 +242,26 @@ try {
{ beforeCounts: churn.before.map((ids) => ids.length), afterCounts: churn.after.map((ids) => ids.length) });
// Back to full-earth for the frame-cost measurement.
await page.evaluate(() => {
const v = window.__godsEyeView.viewer;
const ell = v.scene.globe.ellipsoid;
v.camera.setView({
destination: ell.cartographicToCartesian({
await evaluateWithGeo(page, async (qaGeo) => {
window.__godsEyeView.engine.camera.setView({
destination: qaGeo.cartographic.toCartesian({
longitude: 0, latitude: 15 * Math.PI / 180, height: 24_000_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
v.camera.moveEnd.raiseEvent?.();
v.scene.requestRender?.();
window.__godsEyeView.engine.camera.moveEnd.raiseEvent?.();
window.__godsEyeView.engine.render.request?.();
});
await new Promise((r) => setTimeout(r, 3_000));
await whenSettled(page);
}
// Count actual rendered frames: scene.render() can return without drawing.
const frameCost = await page.evaluate(() => new Promise((resolve) => {
const v = window.__godsEyeView.viewer;
const scene = v.scene;
const durations = [];
let frames = 0;
let previous;
const remove = scene.postRender.addEventListener(() => {
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
const now = performance.now();
if (previous !== undefined) durations.push(now - previous);
previous = now;
@@ -282,7 +270,7 @@ try {
const t0 = performance.now();
const tick = () => {
v.camera.rotateRight(0.0004);
scene.requestRender();
window.__godsEyeView.engine.render.request();
if (performance.now() - t0 < 10_000) requestAnimationFrame(tick);
else {
remove();
@@ -308,24 +296,23 @@ try {
: 'rendered-frame intervals, infra ON, 10s orbit', frameCost);
// ── parked-idle honesty: the LOD walk must not force continuous render ─
await new Promise((r) => setTimeout(r, 4_000));
await whenSettled(page);
const idle = await page.evaluate(async () => {
// The debug handle's own governor read (no /src import: runs on a build).
const { getRenderGovernorDiagnostics } = window.__godsEyeView;
return new Promise((resolve) => {
const scene = window.__godsEyeView.viewer.scene;
const startedAt = Date.now();
const tilesLoadedBefore = scene.globe.tilesLoaded;
const tilesLoadedBefore = window.__godsEyeView.engine.surface.tilesLoaded();
let renders = 0;
let framesWithPendingTiles = 0;
const remove = scene.postRender.addEventListener(() => {
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
renders += 1;
if (!scene.globe.tilesLoaded) framesWithPendingTiles += 1;
if (!window.__godsEyeView.engine.surface.tilesLoaded()) framesWithPendingTiles += 1;
});
setTimeout(() => {
remove();
const governor = getRenderGovernorDiagnostics();
resolve({ renders, tilesLoadedBefore, tilesLoadedAfter: scene.globe.tilesLoaded,
resolve({ renders, tilesLoadedBefore, tilesLoadedAfter: window.__godsEyeView.engine.surface.tilesLoaded(),
framesWithPendingTiles, mode: governor.mode, holds: governor.holds,
requests: governor.recentRequests.filter(request => request.at >= startedAt) });
}, 5_000);
@@ -348,7 +335,7 @@ try {
console.log(` [SHOT] skipped — ${e?.message || e}`);
}
} finally {
await browser.close();
await closeBrowser(browser);
}
const passed = results.filter((r) => r.pass).length;
+16 -26
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env node
/** Mapped installations: isolated camera navigation, cached revisits and Contacts release. */
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import puppeteer from 'puppeteer';
import { moveCamera } from './qa-journey-recorder.mjs';
const args = process.argv.slice(2);
@@ -13,10 +14,9 @@ if (args.includes('--help')) {
}
const baseline = args.includes('--baseline');
const result = { views: [], errors: [], forbidden: [], failures: [] };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const browser = await puppeteer.launch({
const browser = await launchBrowser({
headless: true,
args: ['--use-angle=metal', '--disable-background-timer-throttling'],
args: ['--disable-background-timer-throttling'],
protocolTimeout: 120000,
});
try {
@@ -58,12 +58,10 @@ try {
args.find((a) => /^https?:/.test(a)),
{ waitUntil: 'domcontentloaded' },
);
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 120000 },
);
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
timeout: 120000,
});
await whenSettled(page);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
@@ -72,13 +70,12 @@ try {
const g = window.__godsEyeView;
for (const [id] of g.dataManager.layers)
await g.dataManager.setEnabled(id, false);
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
window.installationEvents = { changed: 0, end: 0 };
window.installationEntityRefs = new Map();
g.viewer.camera.changed.addEventListener(
window.__godsEyeView.engine.camera.changed.addEventListener(
() => window.installationEvents.changed++,
);
g.viewer.camera.moveEnd.addEventListener(
window.__godsEyeView.engine.camera.moveEnd.addEventListener(
() => window.installationEvents.end++,
);
});
@@ -90,7 +87,7 @@ try {
const fits = await page.evaluate(async () => {
const { installationTileZoom } =
await import('/src/layers/installations/source.js');
const r = window.__godsEyeView.viewer.camera.computeViewRectangle();
const r = window.__godsEyeView.engine.camera.viewRectangle();
const d = 180 / Math.PI;
return (
r &&
@@ -248,7 +245,7 @@ try {
});
await page.type('#location-search', '33.33, -117.40');
await page.keyboard.press('Enter');
await sleep(4500);
await whenSettled(page);
} else {
await moveCamera(
page,
@@ -274,7 +271,7 @@ try {
},
{ timeout: 15000 },
);
await sleep(1000);
await whenSettled(page);
await page.evaluate(async () => {
const g = window.__godsEyeView;
const id = window.__qaInstallationSites()[0].id;
@@ -298,10 +295,7 @@ try {
},
{ timeout: 15000 },
);
await page.waitForFunction(
() => !window.__godsEyeView.viewer.camera._currentFlight,
{ timeout: 10000 },
);
await whenSettled(page);
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
@@ -361,11 +355,7 @@ try {
{ timeout: 30000 },
);
}
await page.waitForFunction(
() => !window.__godsEyeView.viewer.camera._currentFlight,
{ timeout: 10000 },
);
await sleep(500);
await whenSettled(page);
previous = await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers
@@ -411,7 +401,7 @@ try {
`qa-shots/installations-navigation-${baseline ? 'before' : 'after'}.json`,
JSON.stringify(result, null, 2),
);
await browser.close();
await closeBrowser(browser);
}
if (
!baseline &&
+13 -16
View File
@@ -1,14 +1,15 @@
// Cesium-only probes: native ground-primitive inspection verifies the Cesium outline paint.
import { whenUiSettled } from './qa/ready.mjs';
/** Shared browser assertions for installation label persistence and selection. */
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import { moveCamera, createScreencast } from './qa-journey-recorder.mjs';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function checkInstallationPan(page, dir) {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('local-datacenters', true),
);
await sleep(3000);
await whenUiSettled(page);
await page.evaluate(async () => {
const { getPaintRect } = (
await import('/src/overlays/worldOverlay.js')
@@ -25,7 +26,7 @@ export async function checkInstallationPan(page, dir) {
if (record.layerId === 'local-datacenters' && record.entity?.id != null)
centerIds.push(record.entity.id);
window.__polishPanFrames = [];
window.__stopPolishPan = g.viewer.scene.postRender.addEventListener(() => {
window.__stopPolishPan = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
const sites = [],
centers = [];
installations.visitNamedMarkers((id) => {
@@ -153,7 +154,7 @@ export async function checkInstallationSelection(
},
{ timeout: 60000 },
);
await sleep(1500);
await whenUiSettled(page);
const picked = await page.evaluate(async (site) => {
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
const g = window.__godsEyeView,
@@ -166,10 +167,7 @@ export async function checkInstallationSelection(
layer.visitNamedMarkers((id, p) => {
if (id === record.id) point = p.position;
});
const screen = C.SceneTransforms.worldToWindowCoordinates(
g.viewer.scene,
point,
);
const screen = window.__godsEyeView.engine.projector.worldToWindow(point);
window.__polishSelectedId = record.id;
window.__polishTitlePaints = [];
const canvas = document.getElementById('world-overlay-canvas'),
@@ -224,7 +222,7 @@ export async function checkInstallationSelection(
};
};
window.__stopPolishSelection =
g.viewer.scene.postRender.addEventListener(() => {
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
window.__polishSelectionFrames.push({
paints: window.__polishTitlePaints.splice(0).length,
own: Boolean(getPaintRect('military-installations', record.id)),
@@ -274,7 +272,7 @@ export async function checkInstallationSelection(
);
throw error;
});
await sleep(500);
await whenUiSettled(page);
const selected = await page.evaluate(() => window.__polishFillProbe());
await page.screenshot({ path: `${dir}/after-selection-${site.key}.png` });
// Close, oblique angles expose surface clipping and floating fills.
@@ -283,13 +281,12 @@ export async function checkInstallationSelection(
async (site, heading) => {
const C =
await import('/node_modules/cesium/Build/Cesium/index.js');
const g = window.__godsEyeView;
const target = C.Cartesian3.fromDegrees(
site.lon,
site.lat,
site.key === 'mabry' ? 180 : 10,
);
g.viewer.camera.lookAt(
window.__godsEyeView.engine.camera.lookAt(
target,
new C.HeadingPitchRange(
C.Math.toRadians(heading),
@@ -297,12 +294,12 @@ export async function checkInstallationSelection(
800,
),
);
g.viewer.camera.lookAtTransform(C.Matrix4.IDENTITY);
window.__godsEyeView.engine.camera.lookAtTransform(C.Matrix4.IDENTITY);
},
site,
heading,
);
await sleep(1700);
await whenUiSettled(page);
assert.ok(
(await page.evaluate(() => window.__polishFillProbe())).selected,
'orbit must preserve installation selection',
@@ -312,7 +309,7 @@ export async function checkInstallationSelection(
});
}
await moveCamera(page, { ...site, pitch: -80 }, 1);
await sleep(900);
await whenUiSettled(page);
const empty = await page.evaluate(() => {
const g = window.__godsEyeView;
for (let y = 250; y < 650; y += 75)
@@ -329,7 +326,7 @@ export async function checkInstallationSelection(
window.__polishFillProbe().selected === null &&
window.__polishFillProbe().items === 0,
);
await sleep(600);
await whenUiSettled(page);
const deselected = await page.evaluate(() => {
window.__stopPolishSelection();
window.__restorePolishPaint();
+22 -32
View File
@@ -1,3 +1,4 @@
// Cesium-only probes: native credits, imagery, primitive ownership and local camera coordinates are Cesium-specific claims.
/**
* qa-l9-matrix.mjs — the L9 release-candidate QA matrix, in one command.
*
@@ -36,7 +37,7 @@
*
* Exit codes: 0 = no FAILs · 1 = at least one FAIL · 2 = target unreachable.
*/
import puppeteer from 'puppeteer';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import { spawn } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -1207,7 +1208,7 @@ function selected(c) {
}
// ── browser group runner ──────────────────────────────────────────────────
const BOOT_JS = () => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager;
const BOOT_JS = () => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager;
async function runBrowserGroup(record) {
const ids = BROWSER_CHECKS.map(([id]) => id).filter((id) => selected(CHECKS.find((c) => c.id === id)));
@@ -1215,20 +1216,10 @@ async function runBrowserGroup(record) {
const emit = (id, res, ms) => { if (ids.includes(id)) record(CHECKS.find((c) => c.id === id), res, ms); };
const only = (id) => ids.includes(id);
const exe = await puppeteer.executablePath().catch(() => null);
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(exe ? { executablePath: exe } : {}),
// Heavy layers (CCTV fleet admission, traffic road graphs) can block the
// page's main thread past puppeteer's 180 s default and turn a healthy
// layer into a bogus "probe threw" FAIL.
const browser = await launchBrowser({
headless: true,
protocolTimeout: 420000,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
// C17 drives the keyless voice path, which asks for a microphone before
// it mints a token. A fake device + auto-granted permission keeps the
// failure under test the MISSING KEY, not a missing microphone.
'--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'],
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'],
});
env.browserVersion = await browser.version();
const page = await browser.newPage();
@@ -1309,15 +1300,15 @@ async function runBrowserGroup(record) {
// holds non-blank pixels.
const painted = await new Promise((resolve) => {
let done = false;
const stop = g.viewer.scene.postRender.addEventListener(() => {
const stop = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
if (done) return;
done = true;
setTimeout(() => { try { stop(); } catch { /* removed */ } resolve(true); }, 0);
});
g.viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
setTimeout(() => { if (!done) { done = true; try { stop(); } catch { /* removed */ } resolve(false); } }, 20000);
});
const canvas = g.viewer.scene.canvas;
const canvas = window.__godsEyeView.engine.host.canvas;
return { painted, w: canvas?.width ?? 0, h: canvas?.height ?? 0, frames: g.viewer.scene.frameState?.frameNumber ?? null };
}, null, 45000);
paint = paintR.ok ? paintR.value : { probeError: paintR.reason };
@@ -1339,12 +1330,12 @@ async function runBrowserGroup(record) {
for (const [id] of BROWSER_CHECKS.slice(1)) {
emit(id, skip('app did not boot — see C1', 'ENV'), 0);
}
await browser.close();
await closeBrowser(browser);
return;
}
// Cancel the intro flyTo: it clobbers any setView issued mid-flight.
await evalBounded(() => { try { window.__godsEyeView.viewer.camera.cancelFlight(); } catch { /* none */ } });
await evalBounded(() => { try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* none */ } });
await new Promise((r) => setTimeout(r, 3000));
// Enable a layer, then poll its stats from THIS side. Polling in Node (many
@@ -1608,7 +1599,7 @@ async function runBrowserGroup(record) {
// is also the only altitude at which "live flow" means anything.
await evalBounded(async () => {
const g = window.__godsEyeView;
g.viewer.camera.cancelFlight();
window.__godsEyeView.engine.camera.cancelFlight();
g.styleManager.applyCameraState({ lat: 30.2672, lon: -97.7431, alt: 2500, heading: 0, pitch: -40 }, 1.2);
await new Promise((r) => setTimeout(r, 3500));
});
@@ -1653,7 +1644,7 @@ async function runBrowserGroup(record) {
// Establish the camera this check needs instead of inheriting one.
await evalBounded(async () => {
const g = window.__godsEyeView;
g.viewer.camera.cancelFlight();
window.__godsEyeView.engine.camera.cancelFlight();
g.styleManager.applyCameraState({ lat: 20, lon: 0, alt: 14000000, heading: 0, pitch: -90 }, 1.5);
await new Promise((r) => setTimeout(r, 4000));
}, null, 30000);
@@ -1725,7 +1716,7 @@ async function runBrowserGroup(record) {
// down: nadir at 25 km spans well under a degree.
await evalBounded(async (b) => {
const g = window.__godsEyeView;
g.viewer.camera.cancelFlight();
window.__godsEyeView.engine.camera.cancelFlight();
g.styleManager.applyCameraState({ lat: b.lat, lon: b.lon, alt: 25000, heading: 0, pitch: -90 }, 1.2);
await new Promise((r) => setTimeout(r, 4000));
}, box, 30000);
@@ -1791,11 +1782,11 @@ async function runBrowserGroup(record) {
const credR = await mustEval(async () => {
const viewer = window.__godsEyeView.viewer;
const dm = window.__godsEyeView.dataManager;
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
const html = (viewer.creditDisplay._staticCredits || []).map((c) => c.html);
viewer.creditDisplay.showLightbox();
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
const listCount = document.querySelector('.cesium-credit-lightbox > ul')?.children?.length || 0;
viewer.creditDisplay.hideLightbox();
@@ -1830,12 +1821,11 @@ async function runBrowserGroup(record) {
await step('C13', async () => {
const stR = await mustEval(async () => {
const viewer = window.__godsEyeView.viewer;
window.__godsEyeView.styleManager.setCleanView(true);
// Cesium paints the credit line during a render — force frames before
// measuring, or a healthy credit reads as 0x0.
for (let i = 0; i < 4; i += 1) {
viewer.scene.requestRender();
window.__godsEyeView.engine.render.request();
// eslint-disable-next-line no-await-in-loop
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
}
@@ -1954,10 +1944,10 @@ async function runBrowserGroup(record) {
// control at all.
const setupR = await mustEval(async () => {
const g = window.__godsEyeView;
g.viewer.camera.cancelFlight();
window.__godsEyeView.engine.camera.cancelFlight();
g.styleManager.applyCameraState({ lat: 30.2672, lon: -97.7431, alt: 3000, heading: 0, pitch: -35 }, 1.5);
await new Promise((r) => setTimeout(r, 3000));
return { altKm: g.viewer.camera.positionCartographic.height / 1000 };
return { altKm: window.__godsEyeView.engine.camera.pose().cartographic.height / 1000 };
}, null, 60000);
if (!setupR.ok) return crash(`could not put the camera at city altitude to test the reset: ${setupR.reason}`);
const before = setupR.value?.altKm;
@@ -1978,7 +1968,7 @@ async function runBrowserGroup(record) {
// eslint-disable-next-line no-await-in-loop
await new Promise((r) => setTimeout(r, 1000));
// eslint-disable-next-line no-await-in-loop
altKm = (await evalBounded(() => window.__godsEyeView.viewer.camera.positionCartographic.height / 1000, null, 20000)) ?? altKm;
altKm = (await evalBounded(() => window.__godsEyeView.engine.camera.pose().cartographic.height / 1000, null, 20000)) ?? altKm;
if (altKm > 5000) break;
}
return altKm > 5000
@@ -2028,7 +2018,7 @@ async function runBrowserGroup(record) {
errorDetail: document.getElementById('gev-voice-error-detail')?.textContent?.trim() || null,
dataStatus: document.getElementById('gev-voice-control')?.dataset?.status || null,
recentError: diag?.recentErrors?.[0] ? { source: diag.recentErrors[0].source, message: diag.recentErrors[0].message } : null,
appAlive: !!window.__godsEyeView?.viewer,
appAlive: !!window.__godsEyeView?.engine,
};
}, null, 60000);
if (!surfacedR.ok) return crash(`could not drive the keyless voice path: ${surfacedR.reason}`);
@@ -2045,7 +2035,7 @@ async function runBrowserGroup(record) {
return pass(`keyless voice degrades honestly: status="${surfaced.status}", detail="${surfaced.detail}", reason "${surfaced.recentError?.message}"; globe still alive`);
});
await browser.close();
await closeBrowser(browser);
}
// ── preflight ─────────────────────────────────────────────────────────────
+14 -41
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
/**
* qa-label-readability.mjs — before/after evidence capture for ambient label
* legibility over bright ground.
@@ -30,9 +31,9 @@
* not only an eyeball.
*/
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const argv = process.argv.slice(2);
const getOpt = (name, fallback) => {
@@ -54,7 +55,6 @@ const SCENE_FILTER = getOpt('--scenes', '')
.filter(Boolean);
const SHOT_DIR = path.resolve(getOpt('--out', 'qa-shots/labels'));
const SETTLE_MAX_MS = 40_000;
const SETTLE_MIN_MS = 4_000;
/**
* Scenes are (camera pose × visual style). `field` describes the synthetic
@@ -162,19 +162,7 @@ const SCENES = [
},
];
const CHROME_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Pin puppeteer's Chrome-for-Testing: system Chrome auto-updates underneath
// the harnesses and its software-GL behaviour shifts across majors.
await puppeteer.executablePath().catch(() => null),
].filter(Boolean);
function findChrome() {
for (const candidate of CHROME_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* fall through */ }
}
return null;
}
/** Decode a base64 PNG inside the page and return luminance statistics. */
async function measureShot(page, base64) {
@@ -207,21 +195,13 @@ async function measureShot(page, base64) {
}, base64);
}
/** Wait until consecutive frames stop changing, so tiles are fully resolved. */
/** Resolve map tiles before measuring the label background. */
async function settle(page) {
const started = Date.now();
let previous = null;
while (Date.now() - started < SETTLE_MAX_MS) {
await new Promise((resolve) => setTimeout(resolve, 1500));
const state = await page.evaluate(() => {
const { engine } = window.__godsEyeView;
return { tilesLoaded: engine.surface.tilesLoaded(), pending: 0 };
});
const settled = state.tilesLoaded && state.pending === 0;
if (settled && previous && Date.now() - started > SETTLE_MIN_MS) return true;
previous = settled;
}
return false;
await whenSettled(page);
return whenTilesLoaded(page, { timeoutMs: SETTLE_MAX_MS }).then(
() => true,
() => false,
);
}
async function main() {
@@ -239,17 +219,9 @@ async function main() {
}
fs.mkdirSync(SHOT_DIR, { recursive: true });
const executablePath = findChrome();
const browser = await puppeteer.launch({
headless: 'new',
...(executablePath ? { executablePath } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox',
'--use-gl=angle', `--use-angle=${process.env.QA_ANGLE || 'swiftshader'}`,
'--disable-dev-shm-usage', '--disable-web-security',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1280,800',
],
const browser = await launchBrowser({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
});
const results = [];
@@ -263,9 +235,10 @@ async function main() {
() => window.__godsEyeView?.layers && window.__godsEyeView?.styleManager,
{ timeout: 60_000, polling: 100 },
);
await whenSettled(page);
// flyToAustin arrives ~500 ms after init; let it start and land before any
// scene sets its own pose, or the arrival overwrites the first camera.
await new Promise((resolve) => setTimeout(resolve, 2500));
await whenSettled(page);
const selected = SCENE_FILTER.length
? SCENES.filter((scene) => SCENE_FILTER.some((prefix) => scene.id.startsWith(prefix)))
@@ -421,7 +394,7 @@ async function main() {
);
}
} finally {
await browser.close();
await closeBrowser(browser);
}
const summary = path.join(SHOT_DIR, `${TAG}-summary.json`);
+8 -41
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node
import { whenSettled } from './qa/ready.mjs';
/**
* Deterministic QA for the detection overlay's bounded two-lane label pipeline.
*
@@ -16,9 +17,9 @@
* makes a small orbit.
*/
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const argv = process.argv.slice(2);
const getFlag = (name) => argv.includes(name);
@@ -58,30 +59,7 @@ const NORMAL_COUNTS = Object.freeze({
satellites: 830,
});
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
await puppeteer.executablePath().catch(() => null),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try {
if (fs.existsSync(candidate)) return candidate;
} catch {
// Let Puppeteer use its bundled browser.
}
}
return null;
}
const results = [];
let skippedChecks = 0;
@@ -218,21 +196,9 @@ async function main() {
}
fs.mkdirSync(SHOT_DIR, { recursive: true });
const executablePath = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(executablePath ? { executablePath } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
`--use-angle=${process.env.QA_ANGLE || 'swiftshader'}`,
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
const browser = await launchBrowser({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
});
const consoleErrors = [];
@@ -278,11 +244,12 @@ async function main() {
() => window.__godsEyeView?.layers && window.__godsEyeView?.styleManager,
{ timeout: 60000, polling: 100 },
);
await whenSettled(page);
// flyToAustin schedules its 600 m arrival 500 ms after initialization.
// Let that callback start, then cancel it before establishing the global
// measurement camera so startup motion cannot invalidate the sample.
await new Promise((resolve) => setTimeout(resolve, 600));
await whenSettled(page);
await page.evaluate(() => window.__godsEyeView.engine.camera.cancelFlight());
const stateChecks = await page.evaluate((storageKey) => {
@@ -647,7 +614,7 @@ async function main() {
console.log(` Screenshot: ${path.join(SHOT_DIR, 'normal-global-5200.png')}`);
console.log(` Synthetic solves: ${solveSamples.length}; normal frames: ${normalFieldFrames.length} (${normalFrames.length} non-solve)`);
} finally {
await browser.close();
await closeBrowser(browser);
if (skippedField) summarize();
}
+6 -6
View File
@@ -1,13 +1,12 @@
#!/usr/bin/env node
import { whenSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
/** Browser proof of Layers panel replacement and subscription ownership. */
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
const browser = await launchBrowser({
headless: true,
args: [
'--no-sandbox',
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
],
});
const page = await browser.newPage();
@@ -26,6 +25,7 @@ try {
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
timeout: 60000,
});
await whenSettled(page);
const results = await page.evaluate(async () => {
const manager = window.__godsEyeView.dataManager;
const entry = document.querySelector(
@@ -139,7 +139,7 @@ try {
for (const [name, passed] of results) check(name, passed);
check('no uncaught browser errors', errors.length === 0);
} finally {
await browser.close();
await closeBrowser(browser);
}
console.log(`RESULT: ${failures} failures`);
process.exitCode = failures ? 1 : 0;
+4 -6
View File
@@ -7,9 +7,9 @@
*
* Run against a local Vite server: node scripts/qa-layer-token-twochar.mjs
*/
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import puppeteer from 'puppeteer';
import {
LAYER_STATE_TOKEN_RESERVATIONS,
nextLayerStateToken,
@@ -119,14 +119,12 @@ function injectCatalog(source) {
return result;
}
const browser = await puppeteer.launch({
const browser = await launchBrowser({
headless: true,
args: [
'--no-sandbox',
'--disable-dev-shm-usage',
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
],
});
@@ -333,5 +331,5 @@ try {
console.log('RESULT: 10 passed, 0 failed, 0 skipped');
} finally {
await Promise.all(contexts.map((context) => context.close().catch(() => {})));
await browser.close();
await closeBrowser(browser);
}