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