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