mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Merge branch 'german/refactor-e12-qa-harness-2' into german/navara-refactor
# Conflicts: # scripts/qa-harness-baseline.json # src/layers/satellites/cockpitSubject.test.mjs
This commit is contained in:
commit
3c304502a0
133 files changed
+3219
-4148
No files matched your search
@@ -19,6 +19,17 @@ excluded. The formatter validates every entry before writing any file.
|
||||
|
||||
New world text labels must use the world-overlay host; Cesium text labels are forbidden.
|
||||
|
||||
## QA scripts
|
||||
|
||||
New `scripts/qa-*.mjs` scenarios use `scripts/qa/browser.mjs` for headless launch
|
||||
and bounded shutdown, and `scripts/qa/ready.mjs` for application, camera and
|
||||
frame readiness. Select the GPU backend with `QA_ANGLE`. Keep timed pauses only
|
||||
when elapsed time is part of the assertion, and explain that measurement.
|
||||
Read scene state through the globe contract or the QA probe; native reads belong
|
||||
only in explicit engine-adapter checks. The counters in
|
||||
`node scripts/report-qa-harness.mjs` must not grow; lower the baseline with
|
||||
`--write-baseline` after removing duplication.
|
||||
|
||||
## Current component ownership
|
||||
|
||||
Package imports use `gods-eye-view`; `package.json` is the authoritative
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
* native dispatch/disabled/teardown without providers or world calibration writes.
|
||||
* Real Scene authoring/playback remains covered by qa-scene-controls.mjs.
|
||||
*/
|
||||
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();
|
||||
const errors = [];
|
||||
@@ -34,11 +35,10 @@ try {
|
||||
{ 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);
|
||||
const inventory = await page.evaluate(() => {
|
||||
const ids = [
|
||||
'scene-new-btn',
|
||||
@@ -304,11 +304,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);
|
||||
for (const panel of ['scene-panel', 'cctv-panel']) {
|
||||
await page.evaluate((panel) => {
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
||||
@@ -329,7 +328,7 @@ try {
|
||||
}
|
||||
check('no uncaught browser errors', errors.length === 0, errors);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: DataSource and ground-primitive readiness validate the native outline adapter.
|
||||
/**
|
||||
* qa-admin-outlines.mjs — bundled state/province/county outlines, driven like a
|
||||
* user would drive them.
|
||||
@@ -23,11 +24,12 @@
|
||||
* timings, frame-time p50/p95 and still-camera frame-difference statistics.
|
||||
* Exits nonzero on any failed assertion.
|
||||
*/
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -63,12 +65,11 @@ const result = {
|
||||
video: null,
|
||||
};
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !args.includes('--headful'),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--use-gl=angle',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -145,14 +146,10 @@ try {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.annotations,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.annotations,
|
||||
{ timeout: 90_000, polling: 500 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 90_000, polling: 250 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -162,7 +159,7 @@ try {
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
result.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)
|
||||
@@ -177,7 +174,7 @@ try {
|
||||
window.__qaOutlines = {
|
||||
fly(view, duration) {
|
||||
return new Promise((resolve) => {
|
||||
viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
viewer.camera.flyTo({
|
||||
destination: Cesium.Cartesian3.fromDegrees(
|
||||
view.lon,
|
||||
@@ -264,20 +261,7 @@ try {
|
||||
});
|
||||
|
||||
const settle = () =>
|
||||
page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
for (let i = 0; i < scene.primitives.length; i++) {
|
||||
const p = scene.primitives.get(i);
|
||||
if (p?.show && typeof p.tilesLoaded === 'boolean' && !p.tilesLoaded)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 60_000, polling: 500 },
|
||||
)
|
||||
.catch(() => {});
|
||||
whenTilesLoaded(page, { timeoutMs: 60_000 }).catch(() => {});
|
||||
|
||||
async function shot(name) {
|
||||
await page.evaluate(
|
||||
@@ -317,11 +301,14 @@ try {
|
||||
// built the mark's primitives (each tick updates them before render).
|
||||
const { viewer } = window.__godsEyeView;
|
||||
await new Promise((resolve) => {
|
||||
const off = viewer.scene.postRender.addEventListener(() => {
|
||||
if (!viewer.dataSourceDisplay.ready) return;
|
||||
off();
|
||||
resolve();
|
||||
});
|
||||
const off =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
() => {
|
||||
if (!viewer.dataSourceDisplay.ready) return;
|
||||
off();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
const drawnMs = performance.now() - started;
|
||||
const mark = window.__godsEyeView.annotations
|
||||
@@ -343,7 +330,7 @@ try {
|
||||
target,
|
||||
flyTo,
|
||||
);
|
||||
if (flyTo) await new Promise((r) => setTimeout(r, 2200));
|
||||
if (flyTo) await whenSettled(page);
|
||||
await settle();
|
||||
// Hold still: the drawn mark must not rebuild or grow.
|
||||
marker = `still:${name}`;
|
||||
@@ -551,7 +538,7 @@ try {
|
||||
.screenshot({ path: path.join(shots, 'admin-outlines-failure.png') })
|
||||
.catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
if (recordVideo && frames.length > 1) {
|
||||
|
||||
+15
-21
@@ -1,3 +1,6 @@
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
/**
|
||||
* qa-air-quality.mjs — headless proof for the Air quality layer.
|
||||
*
|
||||
@@ -12,7 +15,6 @@
|
||||
* Run: node scripts/qa-air-quality.mjs --url http://localhost:4173
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const getOpt = (name, dflt) => {
|
||||
@@ -27,19 +29,13 @@ 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 main() {
|
||||
let exitCode = 0;
|
||||
const consoleErrors = [];
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--use-gl=swiftshader',
|
||||
'--enable-unsafe-swiftshader',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--enable-unsafe-swiftshader', '--no-sandbox', '--disable-dev-shm-usage'],
|
||||
defaultViewport: { width: 1400, height: 900 },
|
||||
});
|
||||
|
||||
@@ -52,10 +48,10 @@ async function main() {
|
||||
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 90000 },
|
||||
);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
|
||||
const stats = await page.evaluate(async () => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
@@ -106,26 +102,24 @@ async function main() {
|
||||
// The card's whole point is "highest readings in view", so put the camera
|
||||
// over Canada before asserting on it — the default view is over Austin,
|
||||
// where an empty in-view list is correct and proves nothing.
|
||||
await page.evaluate(() => {
|
||||
const gev = window.__godsEyeView;
|
||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const d2r = Math.PI / 180;
|
||||
try {
|
||||
gev.viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
} catch {
|
||||
/* no flight in progress */
|
||||
}
|
||||
gev.viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -100 * d2r,
|
||||
latitude: 55 * d2r,
|
||||
height: 4_000_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
gev.viewer.scene.requestRender?.();
|
||||
window.__godsEyeView.engine.render.request?.();
|
||||
});
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
|
||||
const controls = await page.evaluate(
|
||||
() =>
|
||||
@@ -154,7 +148,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,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: tracked entities and native model axes validate Cesium aircraft heading.
|
||||
/**
|
||||
* qa-aircraft-heading.mjs — aircraft display-heading checks
|
||||
* (path-derived, rate-limited display course; Batch 3 overnight run).
|
||||
@@ -97,9 +98,10 @@
|
||||
* Exits non-zero if any assertion fails. Never commits anything.
|
||||
*/
|
||||
|
||||
import { whenSettled, whenFramesDrawn } 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 { installNavaraHeadingProbe } from './qa-heading-navara.mjs';
|
||||
import { stubPlanespotters } from './qa-photo-stub.mjs';
|
||||
|
||||
@@ -124,26 +126,7 @@ const HEADFUL = getFlag('--headful');
|
||||
const SHOT_DIR = path.resolve('qa-shots/aircraft-heading');
|
||||
const SHOT_DIR_V2 = path.resolve('qa-shots/heading-v2');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PASS/FAIL reporting
|
||||
@@ -157,7 +140,7 @@ function record(name, ok, detail) {
|
||||
|
||||
async function observeTrackedHostPaint(page, timeoutMs = 5000) {
|
||||
return page.evaluate((boundedTimeoutMs) => new Promise((resolve) => {
|
||||
const viewer = (window.__headingView ?? window.__godsEyeView?.viewer);
|
||||
const viewer = (window.__headingView ?? window.__godsEyeView?.engine);
|
||||
const scene = viewer?.scene;
|
||||
const history = [];
|
||||
const startedAt = performance.now();
|
||||
@@ -455,20 +438,9 @@ async function main() {
|
||||
fs.mkdirSync(SHOT_DIR, { recursive: true });
|
||||
fs.mkdirSync(SHOT_DIR_V2, { recursive: true });
|
||||
|
||||
const chromeExecutable = findChromeExecutable();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
...(HEADFUL ? [] : ['--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', ...(HEADFUL ? [] : []), '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
|
||||
});
|
||||
|
||||
const consoleErrors = [];
|
||||
@@ -588,8 +560,9 @@ async function main() {
|
||||
() => window.__godsEyeView?.layers && window.__godsEyeView.dataManager,
|
||||
{ timeout: 60000, 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.');
|
||||
|
||||
@@ -730,7 +703,7 @@ async function main() {
|
||||
}, { timeout: 40000, polling: 250 }, TURN.flights[0].icao).then(() => true).catch(() => false);
|
||||
record('flights: tracked 3D model rendered (ready+shown)', flModelUp, flModelUp ? 'model up' : 'model never became ready');
|
||||
if (!flModelUp) { finish(); return; }
|
||||
await sleep(1500); // let the follow camera + reconciliation settle
|
||||
await whenFramesDrawn(page); // let the follow camera + reconciliation settle
|
||||
|
||||
const readoutObservation = await observeTrackedHostPaint(page);
|
||||
const readoutHost = await page.evaluate(() => ({
|
||||
@@ -780,7 +753,7 @@ async function main() {
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.styleManager._setDetectionMode('DENSE');
|
||||
});
|
||||
await sleep(800);
|
||||
await whenFramesDrawn(page);
|
||||
const shots = [
|
||||
['flights-far-orbitA.png', async () => { /* default follow framing */ }],
|
||||
['flights-far-orbitB.png', () => page.evaluate(() => { (window.__headingView ?? window.__godsEyeView.viewer).camera.rotateRight(1.9); })],
|
||||
@@ -794,7 +767,7 @@ async function main() {
|
||||
];
|
||||
for (const [name, move] of shots) {
|
||||
try { await move(); } catch (e) { console.log(` (camera move for ${name} failed: ${e.message})`); }
|
||||
await sleep(1200); // let a few frames render at the new pose
|
||||
await whenFramesDrawn(page); // let a few frames render at the new pose
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, name) });
|
||||
console.log(` saved ${name}`);
|
||||
}
|
||||
@@ -819,7 +792,7 @@ async function main() {
|
||||
}, { timeout: 40000, polling: 250 }, TURN.military[0].hex).then(() => true).catch(() => false);
|
||||
record('military: tracked 3D model rendered (ready+shown)', milModelUp, milModelUp ? 'model up' : 'model never became ready');
|
||||
if (milModelUp) {
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
const milSamples = await page.evaluate(
|
||||
(icao) => window.__sampleModelCourse(icao, 180, 40000),
|
||||
TURN.military[0].hex,
|
||||
@@ -841,7 +814,7 @@ async function main() {
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.styleManager._setDetectionMode('DENSE');
|
||||
});
|
||||
await sleep(800);
|
||||
await whenFramesDrawn(page);
|
||||
for (const [name, move] of [
|
||||
['military-orbitA.png', async () => { }],
|
||||
['military-orbitB.png', () => page.evaluate(() => { (window.__headingView ?? window.__godsEyeView.viewer).camera.rotateRight(1.6); })],
|
||||
@@ -855,7 +828,7 @@ async function main() {
|
||||
})],
|
||||
]) {
|
||||
try { await move(); } catch (e) { console.log(` (camera move for ${name} failed: ${e.message})`); }
|
||||
await sleep(1200);
|
||||
await whenFramesDrawn(page);
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, name) });
|
||||
console.log(` saved ${name}`);
|
||||
}
|
||||
@@ -892,7 +865,7 @@ async function main() {
|
||||
// ---- Phase 3 — hovering helicopter --------------------------------------
|
||||
console.log('\nPhase 3 — hovering helicopter (klass=helicopter, drift ~4 m, track flips ±45°)');
|
||||
if (await trackFlightsAndWaitModel(TURN.hover.icao, 'hover-heli')) {
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
const heliSamples = await sampleCourse(180, 35000, TURN.hover.icao);
|
||||
analyzeHover('hover-heli', heliSamples, {});
|
||||
for (const [name, waitMs] of [['hover-heli-T0.png', 0], ['hover-heli-T8s.png', 8000]]) {
|
||||
@@ -905,7 +878,7 @@ async function main() {
|
||||
// ---- Phase 4 — 25 kt plane in a continuous 4°/s right turn --------------
|
||||
console.log('\nPhase 4 — slow plane in a tight turn (25 kt, 4°/s, R=184 m)');
|
||||
if (await trackFlightsAndWaitModel(TURN.slow.icao, 'slow-turn')) {
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
const slowSamples = await sampleCourse(180, 40000, TURN.slow.icao);
|
||||
analyzeSlowTurn('slow-turn', slowSamples, { scoreProgress: false });
|
||||
// Exercise the production dt clamp with render gaps above
|
||||
@@ -923,7 +896,7 @@ async function main() {
|
||||
['slowturn-near.png', () => page.evaluate(() => { (window.__headingView ?? window.__godsEyeView.viewer).camera.zoomIn(2500); })],
|
||||
]) {
|
||||
try { await move(); } catch (e) { console.log(` (camera move for ${name} failed: ${e.message})`); }
|
||||
await sleep(1200);
|
||||
await whenFramesDrawn(page);
|
||||
await page.screenshot({ path: path.join(SHOT_DIR_V2, name) });
|
||||
console.log(` saved ${name}`);
|
||||
}
|
||||
@@ -1097,7 +1070,7 @@ async function main() {
|
||||
|
||||
finish();
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
function finish() {
|
||||
|
||||
+12
-29
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: requestRender instrumentation measures Cesium request-mode cost.
|
||||
/**
|
||||
* qa-alpr-journey.mjs — ALPR cameras on a realistic Austin camera journey.
|
||||
*
|
||||
@@ -36,9 +37,10 @@
|
||||
* Writes <out>/result.json, <out>/clip.mp4 (CDP screencast with real frame
|
||||
* timing) and per-stop screenshots. Needs ffmpeg on PATH.
|
||||
*/
|
||||
import { whenSettled, whenTilesLoaded } 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 { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import sharp from 'sharp';
|
||||
import { installAlprFixture } from './fixtures/layers/alpr.mjs';
|
||||
@@ -163,14 +165,12 @@ const STOPS = [
|
||||
},
|
||||
];
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !argv.includes('--headful'),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
@@ -233,12 +233,10 @@ try {
|
||||
const url = new URL(appUrl);
|
||||
url.searchParams.set('welcome', '0');
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -286,22 +284,7 @@ try {
|
||||
await recorder.start();
|
||||
|
||||
async function waitTiles(maxMs = 20_000) {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
if (window.__godsEyeView.engine.kind === 'navara')
|
||||
return window.__godsEyeView.engine.surface.tilesLoaded();
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
if (scene.globe.show) return scene.globe.tilesLoaded;
|
||||
for (let i = 0; i < scene.primitives.length; i++) {
|
||||
const p = scene.primitives.get(i);
|
||||
if (p.show && p.tilesLoaded === false) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: maxMs, polling: 250 },
|
||||
)
|
||||
.catch(() => {});
|
||||
await whenTilesLoaded(page, { timeoutMs: maxMs }).catch(() => {});
|
||||
}
|
||||
|
||||
for (const [passIndex, mode] of passes.entries()) {
|
||||
@@ -653,7 +636,7 @@ try {
|
||||
path.join(outDir, 'result.json'),
|
||||
JSON.stringify(result, null, 1),
|
||||
);
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(
|
||||
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
|
||||
|
||||
+13
-13
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: sky commands, the Julian clock and post-process stages validate the Cesium sky.
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
/**
|
||||
* qa-anime-sky — the anime style must leave the sky without bands.
|
||||
*
|
||||
@@ -13,14 +16,13 @@
|
||||
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/]
|
||||
* Exit: 0 pass, 1 fail, 2 the anime style was not reached.
|
||||
*/
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const i = process.argv.indexOf('--url');
|
||||
const URL_ =
|
||||
i > 0 && process.argv[i + 1] ? process.argv[i + 1] : 'http://localhost:4333/';
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||
});
|
||||
try {
|
||||
@@ -41,17 +43,15 @@ try {
|
||||
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
||||
timeout: 120000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const sky = window.__godsEyeView.viewer.scene.skyBox;
|
||||
return (
|
||||
sky?._milkyWayCubeMap &&
|
||||
sky?._starCommand &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden')
|
||||
);
|
||||
return sky?._milkyWayCubeMap && sky?._starCommand;
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
const r = await page.evaluate(async () => {
|
||||
const entry = performance
|
||||
.getEntriesByType('resource')
|
||||
@@ -94,22 +94,22 @@ try {
|
||||
C.Cartesian3.cross(look, north, new C.Cartesian3()),
|
||||
new C.Cartesian3(),
|
||||
);
|
||||
s.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
s.tweens.removeAll();
|
||||
s.camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.Cartesian3.multiplyByScalar(look, 3e7, new C.Cartesian3()),
|
||||
orientation: {
|
||||
direction: look,
|
||||
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
|
||||
},
|
||||
});
|
||||
const gl = s.context._gl,
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2'),
|
||||
w = gl.drawingBufferWidth,
|
||||
h = gl.drawingBufferHeight,
|
||||
px = new Uint8Array(w * h * 4);
|
||||
const measure = async () => {
|
||||
await new Promise((res) => setTimeout(res, 2500));
|
||||
s.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
s.render();
|
||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
||||
let banded = 0;
|
||||
@@ -156,5 +156,5 @@ try {
|
||||
console.error(`UNREACHED: ${error.message}`);
|
||||
process.exitCode = 2;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
/** Prove standalone startup and terminal resource ownership in a real browser. */
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const urlIndex = args.indexOf('--url');
|
||||
const url = urlIndex >= 0 ? args[urlIndex + 1] : 'http://localhost:4173';
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH || (await puppeteer.executablePath()),
|
||||
args: ['--no-sandbox', '--use-gl=angle', '--use-angle=swiftshader'],
|
||||
args: ['--no-sandbox'],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
@@ -28,6 +27,7 @@ try {
|
||||
await page.waitForFunction(() => window.__godsEyeView?.voiceCommands, {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
const before = await page.evaluate(async () => {
|
||||
const entry = [...document.scripts].find((script) =>
|
||||
/\/src\/main\.js(?:\?|$)/.test(script.src),
|
||||
@@ -52,7 +52,7 @@ try {
|
||||
enabled,
|
||||
layers: app.dataManager.layers.size,
|
||||
annotationCount: annotation.drawn,
|
||||
viewerAlive: !app.viewer.isDestroyed(),
|
||||
viewerAlive: !window.__godsEyeView.engine.isDestroyed(),
|
||||
credits: Boolean(document.querySelector('#cesium-credits')),
|
||||
};
|
||||
});
|
||||
@@ -83,7 +83,7 @@ try {
|
||||
return {
|
||||
status: application.getState().status,
|
||||
samePromise,
|
||||
viewerDestroyed: app.viewer.isDestroyed(),
|
||||
viewerDestroyed: app.engine.isDestroyed(),
|
||||
layers: app.dataManager.layers.size,
|
||||
annotations: app.annotations.count(),
|
||||
governor: app.getRenderGovernorDiagnostics(),
|
||||
@@ -112,5 +112,5 @@ try {
|
||||
'PASS: terminal teardown releases runtime owners without late browser errors',
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Cesium-only probes: the native credit registry and lightbox are the attribution oracle.
|
||||
import { whenSettled, whenUiSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-attribution-b12.mjs — visual + state proof for Batch 12 (data attribution).
|
||||
*
|
||||
@@ -16,7 +18,7 @@
|
||||
*
|
||||
* Run: node scripts/qa-attribution-b12.mjs --url http://localhost:4300
|
||||
*/
|
||||
import puppeteer from 'puppeteer';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
@@ -32,19 +34,7 @@ const APP_ORIGIN = new URL(APP_URL).origin;
|
||||
const SHOT_DIR = resolve(__dirname, '..', 'qa-shots', 'b12');
|
||||
mkdirSync(SHOT_DIR, { recursive: true });
|
||||
|
||||
const CHROME_EXECUTABLE_CANDIDATES = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
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() {
|
||||
return CHROME_EXECUTABLE_CANDIDATES.find((candidate) => {
|
||||
try { return existsSync(candidate); } catch { return false; }
|
||||
}) || null;
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
@@ -70,10 +60,8 @@ const REQUIRED_CREDIT_SUBSTRINGS = [
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const chromeExecutable = findChromeExecutable();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -122,8 +110,9 @@ async function main() {
|
||||
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.viewer.creditDisplay,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
// Give Cesium a few frames to render the on-screen credit line (logo + link).
|
||||
await new Promise((r) => setTimeout(r, 2500));
|
||||
await whenUiSettled(page);
|
||||
|
||||
// ── (i) H11: static credits registered ────────────────────────────
|
||||
console.log('H11 — per-layer credits registered in viewer.creditDisplay');
|
||||
@@ -147,11 +136,11 @@ async function main() {
|
||||
// Force a render frame so the credit display flushes, then open the lightbox.
|
||||
const lightboxState = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
const cd = viewer.creditDisplay;
|
||||
cd.showLightbox();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
const box = document.querySelector('.cesium-credit-lightbox');
|
||||
const list = box?.querySelector(':scope > ul');
|
||||
@@ -263,7 +252,7 @@ async function main() {
|
||||
await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.resize();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
});
|
||||
const mobileLightbox = await page.evaluate(() => {
|
||||
@@ -311,7 +300,7 @@ async function main() {
|
||||
check('found datacenter + cable layer ids', !!dcId && !!cableId, `dc=${dcId} cable=${cableId}`);
|
||||
if (dcId) await page.evaluate((id) => window.__godsEyeView.dataManager.setEnabled(id, true), dcId);
|
||||
if (cableId) await page.evaluate((id) => window.__godsEyeView.dataManager.setEnabled(id, true), cableId);
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
await whenUiSettled(page);
|
||||
const afterEnableHtml = await page.evaluate(() =>
|
||||
(window.__godsEyeView.viewer.creditDisplay._staticCredits || []).map((c) => c.html),
|
||||
);
|
||||
@@ -348,7 +337,7 @@ async function main() {
|
||||
// ── (iii) clean-view: credit line STILL visible ───────────────────
|
||||
console.log('\nH10 — clean-view keeps the credit line visible');
|
||||
await page.evaluate(() => document.body.classList.add('ui-clean-view'));
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
await whenUiSettled(page);
|
||||
const cleanVis = await creditVisibility();
|
||||
await page.screenshot({ path: resolve(SHOT_DIR, 'clean-view.png') });
|
||||
check(
|
||||
@@ -366,7 +355,7 @@ async function main() {
|
||||
// ── (iv) recording-mode: credit line STILL visible ────────────────
|
||||
console.log('\nH10 — recording-mode keeps the credit line visible');
|
||||
await page.evaluate(() => document.body.classList.add('recording-mode'));
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
await whenUiSettled(page);
|
||||
const recVis = await creditVisibility();
|
||||
await page.screenshot({ path: resolve(SHOT_DIR, 'recording-mode.png') });
|
||||
check(
|
||||
@@ -377,7 +366,7 @@ async function main() {
|
||||
await page.evaluate(() => document.body.classList.remove('recording-mode'));
|
||||
|
||||
// baseline (normal) screenshot for comparison
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
await whenUiSettled(page);
|
||||
await page.screenshot({ path: resolve(SHOT_DIR, 'normal.png') });
|
||||
|
||||
check(
|
||||
@@ -391,7 +380,7 @@ async function main() {
|
||||
failedResponses.slice(0, 3).join(' | ') || 'clean',
|
||||
);
|
||||
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
|
||||
console.log('\n────────────────────────────────────────────────────────────');
|
||||
console.log(` RESULT: ${passed} passed, ${failed} failed`);
|
||||
|
||||
@@ -105,6 +105,8 @@ export function hookRenderErrors(page) {
|
||||
return page.evaluateOnNewDocument(() => {
|
||||
window.__qaRenderErrors = [];
|
||||
const hook = () => {
|
||||
// Cesium-only: its render loop catches errors before pageerror sees them.
|
||||
if (window.__godsEyeView?.engine?.kind === 'navara') return true;
|
||||
const scene = window.__godsEyeView?.viewer?.scene;
|
||||
if (!scene?.renderError) return false;
|
||||
scene.renderError.addEventListener((_, error) =>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native DataSource counts and scene-render timing measure the Cesium cable adapter.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
/**
|
||||
* qa-cables-overlay — submarine-cable label path measurement + contract gate.
|
||||
*
|
||||
@@ -20,7 +24,6 @@
|
||||
* Requires a running dev server. Headless; rAF throttling disabled so the
|
||||
* frame clock is honest (hidden-pane gotcha).
|
||||
*/
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv;
|
||||
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4214';
|
||||
@@ -40,35 +43,26 @@ function report(name, detail) {
|
||||
console.log(` [MEAS] ${name} — ${JSON.stringify(detail)}`);
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
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 {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 860 });
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
|
||||
await new Promise((r) => setTimeout(r, 12_000)); // boot flyTo + deferred init
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, { timeout: 90_000 });
|
||||
await whenSettled(page); // boot flyTo + deferred init
|
||||
|
||||
// Park mid-Atlantic (many cables + both coasts' landings in range) and
|
||||
// disable every layer so the cables layer 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: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
@@ -77,7 +71,7 @@ try {
|
||||
if (entry.enabled) { try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* measured via counts */ } }
|
||||
}
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 6_000)); // tiles + fades settle
|
||||
await whenSettled(page); // tiles + fades settle
|
||||
|
||||
// ── b. toggle-ON → labels visible ─────────────────────────────────────
|
||||
if (!control) {
|
||||
@@ -96,14 +90,14 @@ try {
|
||||
};
|
||||
while (performance.now() < deadline) {
|
||||
if (labelsUp()) return { ms: Math.round(performance.now() - t0), timedOut: false };
|
||||
gev.viewer.scene.requestRender?.();
|
||||
window.__godsEyeView.engine.render.request?.();
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
return { ms: 60_000, timedOut: true };
|
||||
}, LAYER_ID, legacy);
|
||||
report('toggle-ON -> labels visible (ms)', labelLatency);
|
||||
check('labels became visible after enable', labelLatency.timedOut === false, labelLatency);
|
||||
await new Promise((r) => setTimeout(r, 3_000)); // let load/labels settle
|
||||
await whenSettled(page); // let load/labels settle
|
||||
}
|
||||
|
||||
// ── c. drawn-set / label / host counts ────────────────────────────────
|
||||
@@ -190,11 +184,10 @@ try {
|
||||
: 'scene.render cost, cables ON, 10s orbit', frameCost);
|
||||
|
||||
// ── d. parked idle honesty (labels must not force continuous render) ──
|
||||
await new Promise((r) => setTimeout(r, 4_000)); // orbit stop + fades settle
|
||||
await whenSettled(page); // orbit stop + fades settle
|
||||
const idle = await page.evaluate(() => new Promise((resolve) => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
let renders = 0;
|
||||
const remove = scene.postRender.addEventListener(() => { renders += 1; });
|
||||
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => { renders += 1; });
|
||||
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
|
||||
}));
|
||||
report(control
|
||||
@@ -224,7 +217,7 @@ try {
|
||||
const cycle = await page.evaluate(async (layerId) => {
|
||||
const gev = window.__godsEyeView;
|
||||
await gev.dataManager.setEnabled(layerId, false, { origin: 'user' });
|
||||
gev.viewer.scene.requestRender?.();
|
||||
window.__godsEyeView.engine.render.request?.();
|
||||
await new Promise((r) => setTimeout(r, 1_200));
|
||||
const offDiag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
||||
const offEntries = offDiag.entriesBySource?.[layerId] || 0;
|
||||
@@ -236,7 +229,7 @@ try {
|
||||
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
||||
onPainted = diag.paintedBySource?.[layerId] || 0;
|
||||
if (onPainted > 0) break;
|
||||
gev.viewer.scene.requestRender?.();
|
||||
window.__godsEyeView.engine.render.request?.();
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
return { offEntries, offPainted, onPainted };
|
||||
@@ -245,7 +238,7 @@ try {
|
||||
check('re-enable restores painted labels', cycle.onPainted > 0, cycle);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
const passed = results.filter((r) => r.pass).length;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: manual native renders isolate Cesium cable render cost.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
/**
|
||||
* qa-cables-render-probe — replicates the owner-side perf probe methodology
|
||||
* for the cables layer (relative numbers; SwiftShader when run headless):
|
||||
@@ -11,35 +15,29 @@
|
||||
*
|
||||
* Usage: node scripts/qa-cables-render-probe.mjs [--url http://localhost:4216]
|
||||
*/
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv;
|
||||
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4216';
|
||||
const LAYER_ID = 'telegeography-submarine-cables';
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
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 {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 860 });
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
|
||||
await new Promise((r) => setTimeout(r, 12_000));
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, { timeout: 90_000 });
|
||||
await whenSettled(page);
|
||||
|
||||
await page.evaluate(async () => {
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const gev = window.__godsEyeView;
|
||||
gev.viewer.camera.cancelFlight();
|
||||
const ell = gev.viewer.scene.globe.ellipsoid;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ell.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
@@ -48,7 +46,7 @@ try {
|
||||
if (entry.enabled) { try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* probe */ } }
|
||||
}
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 5_000));
|
||||
await whenSettled(page);
|
||||
|
||||
const stats = (values) => ({
|
||||
mean: +(values.reduce((s, v) => s + v, 0) / Math.max(1, values.length)).toFixed(2),
|
||||
@@ -60,7 +58,7 @@ try {
|
||||
const gev = window.__godsEyeView;
|
||||
await gev.dataManager.setEnabled(id, on, { origin: 'user' });
|
||||
}, LAYER_ID, enabled);
|
||||
await new Promise((r) => setTimeout(r, enabled ? 6_000 : 3_000)); // load/teardown + settle
|
||||
await whenSettled(page); // load/teardown + settle
|
||||
|
||||
// 1. PARKED timed renders.
|
||||
const parked = await page.evaluate(() => {
|
||||
@@ -77,18 +75,17 @@ try {
|
||||
// 2. PAN-STOP cycles: move, raise moveEnd, wait 650 ms, 20 timed renders.
|
||||
const cycles = [];
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
const frames = await page.evaluate(async (step) => {
|
||||
const frames = await evaluateWithGeo(page, async (qaGeo, step) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const ell = v.scene.globe.ellipsoid;
|
||||
v.camera.setView({
|
||||
destination: ell.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (-40 + step * 3) * Math.PI / 180,
|
||||
latitude: 35 * Math.PI / 180,
|
||||
height: 4_500_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
v.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
await new Promise((resolve) => setTimeout(resolve, 650));
|
||||
const durations = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
@@ -112,5 +109,5 @@ try {
|
||||
console.log(`\nparked delta (ON-OFF mean): ${(on.parked.mean - off.parked.mean).toFixed(2)}ms`);
|
||||
console.log(`pan-stop worst: ON ${on.worst}ms vs OFF ${off.worst}ms`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
+15
-20
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-cables-shot — capture the cables layer at two fixed cameras for
|
||||
* before/after visual-identity comparison. Writes to gitignored qa-shots/.
|
||||
* Usage: node scripts/qa-cables-shot.mjs [--url http://localhost:4214] [--tag before]
|
||||
*/
|
||||
import puppeteer from 'puppeteer';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -14,24 +16,20 @@ const tag = argv.includes('--tag') ? argv[argv.indexOf('--tag') + 1] : 'shot';
|
||||
const LAYER_ID = 'telegeography-submarine-cables';
|
||||
mkdirSync(new URL('../qa-shots', import.meta.url), { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
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 {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 860 });
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
|
||||
await new Promise((r) => setTimeout(r, 12_000));
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, { timeout: 90_000 });
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async (layerId) => {
|
||||
const gev = window.__godsEyeView;
|
||||
gev.viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
for (const [id, entry] of gev.dataManager.layers) {
|
||||
if (entry.enabled && id !== layerId) {
|
||||
try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* shot only */ }
|
||||
@@ -45,11 +43,9 @@ try {
|
||||
{ name: 'ny-coast', lon: -73.5, lat: 40.4, height: 500_000 },
|
||||
];
|
||||
for (const view of views) {
|
||||
await page.evaluate((v) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const ell = viewer.scene.globe.ellipsoid;
|
||||
viewer.camera.setView({
|
||||
destination: ell.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo, v) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: v.lon * Math.PI / 180, latitude: v.lat * Math.PI / 180, height: v.height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
@@ -57,15 +53,14 @@ try {
|
||||
}, view);
|
||||
// Let the sweep + labels settle with frames flowing.
|
||||
await page.evaluate(() => new Promise((resolve) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
let ticks = 0;
|
||||
const tick = () => {
|
||||
v.scene.requestRender?.();
|
||||
window.__godsEyeView.engine.render.request?.();
|
||||
if (++ticks < 240) requestAnimationFrame(tick); else resolve();
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
}));
|
||||
await new Promise((r) => setTimeout(r, 1_000));
|
||||
await whenSettled(page);
|
||||
const path = fileURLToPath(
|
||||
new URL(`../qa-shots/cables-${tag}-${view.name}.png`, import.meta.url),
|
||||
);
|
||||
@@ -73,5 +68,5 @@ try {
|
||||
console.log(`saved ${path}`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/** Camera-panel browser acceptance with controlled sources, images and terrain. */
|
||||
import { whenSettled, whenUiSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
const origin = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
const selectAllModifier = process.platform === 'darwin' ? 'Meta' : 'Control';
|
||||
const sources = [
|
||||
@@ -31,13 +32,11 @@ const sources = [
|
||||
rangeM: 300,
|
||||
mountHeightM: 8,
|
||||
}));
|
||||
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();
|
||||
@@ -108,10 +107,10 @@ try {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.styleManager?._cctvControls &&
|
||||
window.__godsEyeView?.dataManager?.layers?.has('cctv') &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
window.__godsEyeView?.dataManager?.layers?.has('cctv'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.$eval('[data-collapse-target="cctv-panel"]', (button) =>
|
||||
button.click(),
|
||||
);
|
||||
@@ -357,22 +356,23 @@ try {
|
||||
fs.mkdirSync('qa-shots/camera-controls', { recursive: true });
|
||||
await page.screenshot({ path: 'qa-shots/camera-controls/desktop.png' });
|
||||
await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: window.__godsEyeView.engine.camera.pose().position,
|
||||
orientation: {
|
||||
heading: camera.heading + Math.PI / 3,
|
||||
heading:
|
||||
window.__godsEyeView.engine.camera.pose().heading + Math.PI / 3,
|
||||
pitch: -0.6,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
await whenUiSettled(page);
|
||||
await page.screenshot({ path: 'qa-shots/camera-controls/angle.png' });
|
||||
await page.setViewport({ width: 390, height: 844 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
await whenUiSettled(page);
|
||||
// Phones present CCTV in the Cameras tab's sheet.
|
||||
await page.click('#mobile-tab-bar [data-mobile-sheet="cameras"]');
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await whenUiSettled(page);
|
||||
await page.screenshot({ path: 'qa-shots/camera-controls/narrow.png' });
|
||||
check(
|
||||
'narrow camera panel remains within the viewport',
|
||||
@@ -411,6 +411,6 @@ try {
|
||||
);
|
||||
} finally {
|
||||
await Promise.all([...delayedFrames].map((respond) => respond()));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (failures) process.exitCode = 1;
|
||||
+61
-136
@@ -1,3 +1,6 @@
|
||||
// Cesium-only probes: native raycast fault injection, camera transforms and entity sentinels test the Cesium adapter.
|
||||
import { withGeo } from './qa/geometry.mjs';
|
||||
import { whenFramesDrawn, whenSettled, pollPage, whenTilesLoaded } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-cctv-v2.mjs — CCTV v2 subsystem proof harness
|
||||
*
|
||||
@@ -76,7 +79,7 @@
|
||||
* Exit 0 = all asserts passed. Non-zero = a hard failure.
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { watchBrowserPage, redact } from './qa-browserEvidence.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { installCctvFixture } from './fixtures/layers/cctv.mjs';
|
||||
@@ -126,30 +129,7 @@ const saveEvidence = () => {
|
||||
);
|
||||
};
|
||||
|
||||
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 = evidence.checks;
|
||||
function record(name, ok, detail) {
|
||||
@@ -189,32 +169,6 @@ const PROTOCOL_TIMEOUT_MS = 300_000;
|
||||
* The predicate must return a boolean: its result crosses CDP by value.
|
||||
* @returns {Promise<boolean>} True if the predicate held within the budget.
|
||||
*/
|
||||
async function waitForPredicate(
|
||||
page,
|
||||
predicate,
|
||||
{ timeoutMs, intervalMs = 250, args = [] },
|
||||
) {
|
||||
// performance.now() is monotonic and doesn't advance while the machine
|
||||
// sleeps, so neither a clock change nor a laptop sleep eats the budget.
|
||||
const deadline = performance.now() + timeoutMs;
|
||||
for (;;) {
|
||||
try {
|
||||
if (await page.evaluate(predicate, ...args)) return true;
|
||||
} catch (err) {
|
||||
// A navigation or Vite reload replaces the execution context: poll again.
|
||||
if (
|
||||
!/Execution context was destroyed|Cannot find context/.test(
|
||||
err?.message ?? '',
|
||||
)
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const remaining = deadline - performance.now();
|
||||
if (remaining <= 0) return false;
|
||||
await sleep(Math.min(intervalMs, remaining));
|
||||
}
|
||||
}
|
||||
|
||||
// SwiftShader can spend tens of seconds servicing the hover drillPick emitted
|
||||
// by page.mouse.move(), before the LEFT_DOWN under test even runs. Position the
|
||||
@@ -266,22 +220,21 @@ function installRenderFramesHelper(page) {
|
||||
return page.evaluate(() => {
|
||||
window.__qaRenderFrames = (count, capMs) =>
|
||||
new Promise((resolve) => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
let frames = 0;
|
||||
const finish = () => {
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
resolve(frames);
|
||||
};
|
||||
const remove = scene.postRender.addEventListener(() => {
|
||||
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
if (++frames >= count) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
const timer = setTimeout(finish, capMs);
|
||||
scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -462,23 +415,6 @@ const BEARING_SHIFT_SRC = `
|
||||
* @param {number} [timeoutMs=15000]
|
||||
* @returns {Promise<boolean>} True if tilesLoaded was observed within the timeout.
|
||||
*/
|
||||
function waitForTilesLoaded(page, timeoutMs = 15000) {
|
||||
return waitForPredicate(
|
||||
page,
|
||||
() => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
const prims = scene.primitives;
|
||||
for (let i = 0; i < prims.length; i++) {
|
||||
const p = prims.get(i);
|
||||
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
|
||||
return p.tilesLoaded === true;
|
||||
}
|
||||
}
|
||||
return true; // no tileset in the scene — nothing to wait for
|
||||
},
|
||||
{ timeoutMs },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a calibration patch (pitchDeg/fovDeg/rangeScale offsets) that
|
||||
@@ -542,24 +478,13 @@ async function main() {
|
||||
|
||||
fs.mkdirSync(SHOTS_DIR, { recursive: true });
|
||||
|
||||
const chromeExecutable = findChromeExecutable();
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: PROTOCOL_TIMEOUT_MS,
|
||||
// Talk CDP over a pipe, so no debugging port stays open on localhost for
|
||||
// the length of a headful run.
|
||||
pipe: true,
|
||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1280,800',
|
||||
],
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-gpu', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
|
||||
});
|
||||
|
||||
const consoleErrors = evidence.browserErrors;
|
||||
@@ -596,7 +521,7 @@ async function main() {
|
||||
});
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
const appReady = await waitForPredicate(
|
||||
const appReady = await pollPage(
|
||||
page,
|
||||
() =>
|
||||
Boolean(
|
||||
@@ -638,7 +563,7 @@ async function main() {
|
||||
if (!keyless) throw new Error('Expected a keyless globe before CCTV QA');
|
||||
}
|
||||
// Let the initial fly-to Austin and first tiles settle.
|
||||
await sleep(4000);
|
||||
await whenSettled(page);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Install raycast counters BEFORE the layer is enabled (§9.1-amended: the
|
||||
@@ -653,7 +578,7 @@ async function main() {
|
||||
if (
|
||||
!scene ||
|
||||
typeof scene.pickFromRay !== 'function' ||
|
||||
typeof scene.sampleHeight !== 'function'
|
||||
typeof window.__godsEyeView.engine.surface.sampleHeight !== 'function'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -663,7 +588,7 @@ async function main() {
|
||||
window.__qaCounters.pickFromRay += 1;
|
||||
return origPick(...args);
|
||||
};
|
||||
const origSample = scene.sampleHeight.bind(scene);
|
||||
const origSample = window.__godsEyeView.engine.surface.sampleHeight.bind(scene);
|
||||
scene.sampleHeight = (...args) => {
|
||||
window.__qaCounters.sampleHeight += 1;
|
||||
return origSample(...args);
|
||||
@@ -719,7 +644,7 @@ async function main() {
|
||||
console.log(
|
||||
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
|
||||
);
|
||||
const drained = await waitForPredicate(
|
||||
const drained = await pollPage(
|
||||
page,
|
||||
() => {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
@@ -763,7 +688,7 @@ async function main() {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.focusNearest({ durationSec: 0.1 });
|
||||
});
|
||||
await sleep(500); // let the activation's synchronous work (probe + geometry rewrite) land
|
||||
await whenFramesDrawn(page); // let the activation's synchronous work (probe + geometry rewrite) land
|
||||
const cAfterActivate = await readCounters();
|
||||
const activeId = await page.evaluate(
|
||||
() =>
|
||||
@@ -793,7 +718,7 @@ async function main() {
|
||||
.get('cctv')
|
||||
.module.setParams({ selectedCameraId: id });
|
||||
}, activeId);
|
||||
await sleep(400);
|
||||
await whenFramesDrawn(page);
|
||||
const cAfterReselect = await readCounters();
|
||||
const geomAfterReselect = await page.evaluate(`(() => {
|
||||
${SERIALIZE_GEOM_SRC}
|
||||
@@ -816,7 +741,7 @@ async function main() {
|
||||
// latency of the completion pass (update interval is 10s).
|
||||
// -----------------------------------------------------------------------
|
||||
console.log('Waiting for tiles + the one-shot completion pass...');
|
||||
const tilesSeenLoaded = await waitForTilesLoaded(page, 45000);
|
||||
const tilesSeenLoaded = await whenTilesLoaded(page, { timeoutMs: 45000 }).then(() => true).catch(() => false);
|
||||
let sampleFloorOk = false;
|
||||
if (tilesSeenLoaded && !terrainGlobe) {
|
||||
sampleFloorOk = await page
|
||||
@@ -866,10 +791,10 @@ async function main() {
|
||||
terrain.ok && heights?.length === 2 && heights.every(Number.isFinite);
|
||||
const mountResolved =
|
||||
terrainReady &&
|
||||
(await waitForPredicate(
|
||||
(await pollPage(
|
||||
page,
|
||||
(expected, base) => {
|
||||
const { viewer, dataManager, engine } = window.__godsEyeView;
|
||||
await withGeo((qaGeo, expected, base) => {
|
||||
const { dataManager, engine } = window.__godsEyeView;
|
||||
const module = dataManager.layers.get('cctv').module;
|
||||
const camera = module.getUIState().activeCamera;
|
||||
// The corner ray is an engine line; its first point (the mount)
|
||||
@@ -883,7 +808,7 @@ async function main() {
|
||||
const mount = drawn ? ray?.polyline?.positions?.[0] : undefined;
|
||||
const height =
|
||||
mount &&
|
||||
viewer.scene.globe.ellipsoid.cartesianToCartographic(mount)
|
||||
qaGeo.cartographic.fromCartesian(mount)
|
||||
.height;
|
||||
return (
|
||||
Number.isFinite(camera.groundPriorM) &&
|
||||
@@ -895,8 +820,8 @@ async function main() {
|
||||
) &&
|
||||
window.__qaCounters.sampleHeight === base
|
||||
);
|
||||
},
|
||||
{ timeoutMs: 30000, args: [heights, c0.sampleHeight] },
|
||||
}, heights, c0.sampleHeight),
|
||||
{ timeoutMs: 30000 },
|
||||
));
|
||||
record(
|
||||
'terrain-globe completion uses a real DEM height at the rendered mount without mesh samples',
|
||||
@@ -905,10 +830,10 @@ async function main() {
|
||||
);
|
||||
} else {
|
||||
console.log('Waiting for tiles + the one-shot completion pass...');
|
||||
const tilesSeenLoaded = await waitForTilesLoaded(page, 45000);
|
||||
const tilesSeenLoaded = await whenTilesLoaded(page, { timeoutMs: 45000 }).then(() => true).catch(() => false);
|
||||
let sampleFloorOk = false;
|
||||
if (tilesSeenLoaded) {
|
||||
sampleFloorOk = await waitForPredicate(
|
||||
sampleFloorOk = await pollPage(
|
||||
page,
|
||||
(base) => window.__qaCounters.sampleHeight - base >= 1,
|
||||
{ timeoutMs: 30000, args: [c0.sampleHeight] },
|
||||
@@ -993,7 +918,7 @@ async function main() {
|
||||
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
|
||||
});
|
||||
}, activeId);
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
const cAfterCal = await readCounters();
|
||||
const sampleDeltaCal = cAfterCal.sampleHeight - cBeforeCal.sampleHeight;
|
||||
const pickDeltaCal = cAfterCal.pickFromRay - cBeforeCal.pickFromRay;
|
||||
@@ -1016,7 +941,7 @@ async function main() {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
||||
}, activeId);
|
||||
await sleep(300);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
// =========================================================================
|
||||
// Group 2: geometry contract
|
||||
@@ -1098,7 +1023,7 @@ async function main() {
|
||||
? JSON.stringify(geomInfo.plane.dimensions)
|
||||
: null;
|
||||
for (let attempt = 0; attempt < 3 && !patchGeometryApplied; attempt++) {
|
||||
await waitForTilesLoaded(page, 15000);
|
||||
await whenTilesLoaded(page, { timeoutMs: 15000 }).then(() => true).catch(() => false);
|
||||
await page.evaluate(
|
||||
({ camId, patch }) => {
|
||||
const mod =
|
||||
@@ -1107,7 +1032,7 @@ async function main() {
|
||||
},
|
||||
{ camId: activeId, patch: safePatch },
|
||||
);
|
||||
await sleep(400);
|
||||
await whenFramesDrawn(page);
|
||||
geomForCorners = await page.evaluate(
|
||||
`(function(camId){ ${SERIALIZE_GEOM_SRC} return serializeGeom(camId); })(${JSON.stringify(activeId)})`,
|
||||
);
|
||||
@@ -1204,7 +1129,7 @@ async function main() {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
||||
}, activeId);
|
||||
await sleep(300);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
|
||||
// Byte-stable over 30s idle: serialize + compare after 30s. Nothing
|
||||
@@ -1283,7 +1208,7 @@ async function main() {
|
||||
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
|
||||
});
|
||||
}, activeId);
|
||||
await sleep(300);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
// Save-gated persistence (v3 design §3e): a PATCH edits the live pose
|
||||
// only — the store must stay untouched and the camera must read as
|
||||
@@ -1321,7 +1246,7 @@ async function main() {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ calibration: { cameraId: camId, save: true } });
|
||||
}, activeId);
|
||||
await sleep(200);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
const storeEntry = await page.evaluate((camId) => {
|
||||
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
|
||||
@@ -1371,7 +1296,7 @@ async function main() {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
||||
}, activeId);
|
||||
await sleep(300);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
const storeAfterReset = await page.evaluate((camId) => {
|
||||
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
|
||||
@@ -1483,7 +1408,7 @@ async function main() {
|
||||
const gev = window.__godsEyeView;
|
||||
const viewer = gev.viewer;
|
||||
const scene = viewer.scene;
|
||||
const canvas = scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const { hitTest } = (
|
||||
await import('/src/overlays/worldOverlay.js')
|
||||
@@ -1535,28 +1460,28 @@ async function main() {
|
||||
const mod = gev.dataManager.layers.get('cctv').module;
|
||||
const snapshotPose = () => ({
|
||||
position: [
|
||||
viewer.camera.positionWC.x,
|
||||
viewer.camera.positionWC.y,
|
||||
viewer.camera.positionWC.z,
|
||||
window.__godsEyeView.engine.camera.pose().position.x,
|
||||
window.__godsEyeView.engine.camera.pose().position.y,
|
||||
window.__godsEyeView.engine.camera.pose().position.z,
|
||||
],
|
||||
direction: [
|
||||
viewer.camera.directionWC.x,
|
||||
viewer.camera.directionWC.y,
|
||||
viewer.camera.directionWC.z,
|
||||
window.__godsEyeView.engine.camera.pose().direction.x,
|
||||
window.__godsEyeView.engine.camera.pose().direction.y,
|
||||
window.__godsEyeView.engine.camera.pose().direction.z,
|
||||
],
|
||||
up: [viewer.camera.upWC.x, viewer.camera.upWC.y, viewer.camera.upWC.z],
|
||||
up: [window.__godsEyeView.engine.camera.pose().up.x, window.__godsEyeView.engine.camera.pose().up.y, window.__godsEyeView.engine.camera.pose().up.z],
|
||||
right: [
|
||||
viewer.camera.rightWC.x,
|
||||
viewer.camera.rightWC.y,
|
||||
viewer.camera.rightWC.z,
|
||||
window.__godsEyeView.engine.camera.pose().right.x,
|
||||
window.__godsEyeView.engine.camera.pose().right.y,
|
||||
window.__godsEyeView.engine.camera.pose().right.z,
|
||||
],
|
||||
transform: Array.from(
|
||||
{ length: 16 },
|
||||
(_, i) => viewer.camera.transform[i],
|
||||
),
|
||||
heading: viewer.camera.heading,
|
||||
pitch: viewer.camera.pitch,
|
||||
roll: viewer.camera.roll,
|
||||
heading: window.__godsEyeView.engine.camera.pose().heading,
|
||||
pitch: window.__godsEyeView.engine.camera.pose().pitch,
|
||||
roll: window.__godsEyeView.engine.camera.pose().roll,
|
||||
trackedId: viewer.trackedEntity?.id ?? null,
|
||||
});
|
||||
const cameraPoseMatches = (a, b, epsilon = 1e-5) => {
|
||||
@@ -1626,7 +1551,7 @@ async function main() {
|
||||
.module.setParams({ calibrationMode: true });
|
||||
});
|
||||
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
||||
await sleep(250);
|
||||
await whenFramesDrawn(page);
|
||||
const adjustClick = await page.evaluate(() => {
|
||||
const evidence = window.__qaCctvClickEvidence;
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
@@ -1673,7 +1598,7 @@ async function main() {
|
||||
distance,
|
||||
new ray.origin.constructor(),
|
||||
);
|
||||
const screen = scene.cartesianToCanvasCoordinates(candidate);
|
||||
const screen = window.__godsEyeView.engine.projector.worldToWindow(candidate);
|
||||
if (
|
||||
screen &&
|
||||
Math.hypot(screen.x - canvasPoint.x, screen.y - canvasPoint.y) < 2
|
||||
@@ -1695,7 +1620,7 @@ async function main() {
|
||||
});
|
||||
window.__qaCctvSiblingOwner = owner;
|
||||
const framesRendered = await window.__qaRenderFrames(3, 10000);
|
||||
const rect = scene.canvas.getBoundingClientRect();
|
||||
const rect = window.__godsEyeView.engine.host.canvas.getBoundingClientRect();
|
||||
const picked = scene.pick(canvasPoint);
|
||||
const pickedId = picked?.id?.id ?? picked?.id ?? null;
|
||||
window.__qaCctvClickEvidence.siblingBaselineTransitions =
|
||||
@@ -1735,7 +1660,7 @@ async function main() {
|
||||
);
|
||||
if (siblingOwnsPick) {
|
||||
await page.mouse.click(siblingTarget.x, siblingTarget.y);
|
||||
await sleep(250);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
const siblingClick = await page.evaluate(() => {
|
||||
const evidence = window.__qaCctvClickEvidence;
|
||||
@@ -1769,7 +1694,7 @@ async function main() {
|
||||
|
||||
if (emptyClickPoint) {
|
||||
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
||||
await sleep(250);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
const firstEmptyClick = await page.evaluate(() => {
|
||||
const evidence = window.__qaCctvClickEvidence;
|
||||
@@ -1810,7 +1735,7 @@ async function main() {
|
||||
|
||||
if (emptyClickPoint) {
|
||||
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
||||
await sleep(250);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
const repeatEmptyClick = await page.evaluate(() => {
|
||||
const evidence = window.__qaCctvClickEvidence;
|
||||
@@ -1894,7 +1819,7 @@ async function main() {
|
||||
mod.setParams({ coverageMode: 'viewshed' });
|
||||
return mod.getUIState().coverageMode;
|
||||
});
|
||||
await sleep(400);
|
||||
await whenFramesDrawn(page);
|
||||
record(
|
||||
'coverageMode setParams round-trip (viewshed)',
|
||||
modeAfterSet === 'viewshed',
|
||||
@@ -1918,7 +1843,7 @@ async function main() {
|
||||
const on = mod.getUIState().coverageMode;
|
||||
return { off, on };
|
||||
});
|
||||
await sleep(200);
|
||||
await whenFramesDrawn(page);
|
||||
const volumesInOnMode = await countVolumes();
|
||||
record(
|
||||
'boolean showCoverage compat (false→off, true→on)',
|
||||
@@ -1938,7 +1863,7 @@ async function main() {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
mod.setParams({ coverageMode: 'viewshed' });
|
||||
});
|
||||
await sleep(400);
|
||||
await whenFramesDrawn(page);
|
||||
// NEXT starts an asynchronous footprint-height lookup. Its completion is
|
||||
// allowed to update geometry; the unchanged 8s idle window starts after it.
|
||||
const terrainDeadline = Date.now() + 35000;
|
||||
@@ -1998,7 +1923,7 @@ async function main() {
|
||||
// Frame that actual camera before requiring an on-screen physical drag.
|
||||
await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('cctv')
|
||||
.module.setParams({ focusSelected: true, focusDurationSec: 0.2 }));
|
||||
await sleep(800);
|
||||
await whenFramesDrawn(page);
|
||||
const GIZMO_PARTS = [
|
||||
'ring-heading',
|
||||
'ring-pitch',
|
||||
@@ -2113,7 +2038,7 @@ async function main() {
|
||||
grab.x + eastDrag.ux * i * 12,
|
||||
grab.y + eastDrag.uy * i * 12,
|
||||
);
|
||||
await sleep(40);
|
||||
await whenFramesDrawn(page);
|
||||
transientDragStates.push(
|
||||
await page.evaluate(() => {
|
||||
const mod =
|
||||
@@ -2132,7 +2057,7 @@ async function main() {
|
||||
}
|
||||
const candidateCountersBeforeRelease = await readCounters();
|
||||
await page.mouse.up();
|
||||
await sleep(400);
|
||||
await whenFramesDrawn(page);
|
||||
const candidateEast = await page.evaluate(() => {
|
||||
const mod =
|
||||
window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||
@@ -2328,7 +2253,7 @@ async function main() {
|
||||
// Screenshots supplement this state/geometry oracle. Positive rendered
|
||||
// media pixels are a separate mandatory pixel gate.
|
||||
// -----------------------------------------------------------------------
|
||||
await sleep(1000);
|
||||
await whenFramesDrawn(page);
|
||||
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
|
||||
await saveReviewShot(page, shot, 'visual review only, not a gate');
|
||||
|
||||
@@ -2344,7 +2269,7 @@ async function main() {
|
||||
}
|
||||
} finally {
|
||||
await fixture?.dispose();
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
evidence.pass =
|
||||
results.length > 0 &&
|
||||
results.every((r) => r.ok === true) &&
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* Rendered proof for backdrop-selective callout plates at GROUND LEVEL.
|
||||
*
|
||||
@@ -46,13 +47,14 @@
|
||||
* node scripts/qa-cockpit-plates.mjs --url http://localhost:4268 --tag before
|
||||
* node scripts/qa-cockpit-plates.mjs --url http://localhost:4268 --teeth
|
||||
*
|
||||
* Defaults to ANGLE/Metal on macOS and software GL elsewhere. `--swiftshader` selects software GL,
|
||||
* QA_ANGLE selects the renderer (Metal by default); use swiftshader for software GL,
|
||||
* which is deterministic but is NOT real-GPU evidence; the banner says which
|
||||
* backend actually ran and the run records it alongside the results.
|
||||
*/
|
||||
import { whenSettled, whenTilesLoaded } 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';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const getOpt = (name, fallback) => {
|
||||
@@ -64,7 +66,7 @@ const APP_ORIGIN = new URL(APP_URL).origin;
|
||||
const TAG = getOpt('--tag', 'after');
|
||||
const HEADFUL = argv.includes('--headful');
|
||||
const TEETH = argv.includes('--teeth');
|
||||
const SWIFTSHADER = argv.includes('--swiftshader') || process.platform !== 'darwin';
|
||||
const SWIFTSHADER = /swiftshader|software/i.test(process.env.QA_ANGLE || 'metal');
|
||||
const SHOT_DIR = path.resolve('qa-shots/cockpitplates');
|
||||
|
||||
/** Mirrors `SKY_PLATE_SCALE` in src/overlays/worldOverlayTokens.js. */
|
||||
@@ -134,14 +136,6 @@ const POSES = [
|
||||
},
|
||||
];
|
||||
|
||||
const CHROME_CANDIDATES = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
// Version-pinned Chrome-for-Testing over the auto-updating system Chrome:
|
||||
// its software-GL behavior shifts across majors and has produced false
|
||||
// negatives in this repo's harnesses before.
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
].filter(Boolean);
|
||||
|
||||
const results = [];
|
||||
function record(name, ok, detail) {
|
||||
@@ -213,25 +207,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; off-Mac the GPU branch has to fall
|
||||
// back to the software rasterizer or WebGL init fails outright.
|
||||
...(SWIFTSHADER
|
||||
? ['--use-gl=angle', '--use-angle=swiftshader']
|
||||
: ['--use-angle=metal', '--enable-gpu', '--ignore-gpu-blocklist']),
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1280,800',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(SWIFTSHADER ? [] : ['--enable-gpu', '--ignore-gpu-blocklist']), '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
|
||||
});
|
||||
|
||||
const consoleErrors = [];
|
||||
@@ -276,10 +254,7 @@ async function main() {
|
||||
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 90_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
|
||||
renderer = await page.evaluate(() => {
|
||||
const gl = document.createElement('canvas').getContext('webgl2')
|
||||
@@ -302,10 +277,10 @@ async function main() {
|
||||
|
||||
// Inject the field. The layers keep their real pipelines; only the source
|
||||
// of observations is replaced, exactly as scripts/qa-labels.mjs does.
|
||||
await page.evaluate((payload) => {
|
||||
const { viewer, dataManager, styleManager } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
const Cartesian3 = viewer.camera.position.constructor;
|
||||
await evaluateWithGeo(page, async (qaGeo, payload) => {
|
||||
const { dataManager, styleManager } = window.__godsEyeView;
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
window.__PLATE_QA_FIELD = payload.contacts.map((contact) => ({
|
||||
sourceId: contact.id.toLowerCase(),
|
||||
id: contact.id,
|
||||
@@ -330,17 +305,16 @@ async function main() {
|
||||
|
||||
for (const pose of POSES) {
|
||||
console.log(`\n — pose "${pose.name}" (${pose.note})`);
|
||||
await page.evaluate((payload) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
const Cartesian3 = viewer.camera.position.constructor;
|
||||
viewer.camera.setView({
|
||||
await evaluateWithGeo(page, async (qaGeo, payload) => {
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: Cartesian3.fromDegrees(payload.lon, payload.lat, payload.height),
|
||||
orientation: { heading: 0, pitch: payload.pitchDeg * Math.PI / 180, roll: 0 },
|
||||
});
|
||||
}, { lon: JFK_LON, lat: JFK_LAT, height: pose.height, pitchDeg: pose.pitchDeg });
|
||||
|
||||
// Let the tiles settle and the label solve run before the tap window.
|
||||
await new Promise((resolve) => setTimeout(resolve, 6_000));
|
||||
await whenTilesLoaded(page);
|
||||
await page.evaluate(() => {
|
||||
window.__PLATE_TAP.events = [];
|
||||
window.__PLATE_TAP.on = true;
|
||||
@@ -356,10 +330,9 @@ async function main() {
|
||||
// OUTPUT has to ask for its own renders. Production is unaffected: real
|
||||
// Cockpit holds, and a parked map keeps showing the last correct frame
|
||||
// precisely because nothing changed.
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
const pump = () => {
|
||||
if (!window.__PLATE_TAP.on) return;
|
||||
scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
requestAnimationFrame(pump);
|
||||
};
|
||||
pump();
|
||||
@@ -522,7 +495,7 @@ async function main() {
|
||||
consoleErrors.slice(0, 3).join(' | ') || 'clean',
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: tracked-entity and local-frame coordinates verify the native cockpit handoff.
|
||||
/** Focused rendered proof for adaptive Cockpit Display/Radio layout. */
|
||||
import { whenSettled, whenUiSettled } 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';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { stubPlanespotters } from './qa-photo-stub.mjs';
|
||||
|
||||
@@ -13,22 +15,9 @@ const appUrl = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
const headful = process.argv.includes('--headful');
|
||||
fs.mkdirSync(shotsDir, { recursive: true });
|
||||
|
||||
const chromeCandidates = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
].filter(Boolean);
|
||||
const executablePath = chromeCandidates.find((candidate) => fs.existsSync(candidate));
|
||||
const browser = await puppeteer.launch({
|
||||
headless: headful ? false : 'new',
|
||||
...(executablePath ? { executablePath } : {}),
|
||||
// Metal is a macOS-only ANGLE backend; anywhere else it fails WebGL init.
|
||||
args: [
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
'--no-sandbox',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [...(process.platform === 'darwin' ? ['--enable-gpu'] : []), '--no-sandbox'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await blockPaidQaMaps(page);
|
||||
@@ -171,10 +160,7 @@ try {
|
||||
page.on('request', routeQaRequest);
|
||||
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(() => (
|
||||
typeof window.__gevQaRegisterLayer === 'function'
|
||||
&& typeof window.__gevQaUnregisterLayer === 'function'
|
||||
@@ -675,6 +661,7 @@ try {
|
||||
() => Boolean(window.__godsEyeView.viewer.trackedEntity?.position),
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
const preselectedContactAdoption = await page.evaluate(async () => {
|
||||
const { styleManager, dataManager, viewer } = window.__godsEyeView;
|
||||
const flights = dataManager.layers.get('flights')?.module;
|
||||
@@ -767,7 +754,7 @@ try {
|
||||
} : null,
|
||||
navigation: snapshot?.navigation || null,
|
||||
afterId: trackedInfo?.icao24 || null,
|
||||
viewerTrackedId: viewer.trackedEntity?.gevTrackedId || null,
|
||||
viewerTrackedId: window.__godsEyeView.engine.camera.following()?.label?.id || null,
|
||||
contextVisible: !document.getElementById('military-awareness-panel')?.hidden,
|
||||
entryAvailableAfterSettlement: Boolean(entry && !entry.hidden),
|
||||
blockerStillRegistered: dataManager.layers.has(blockerId),
|
||||
@@ -854,8 +841,8 @@ try {
|
||||
const readinessUrl = new URL(appUrl);
|
||||
readinessUrl.searchParams.set('welcome', '0');
|
||||
await readinessPage.goto(readinessUrl.href, { waitUntil: 'domcontentloaded' });
|
||||
await readinessPage.waitForFunction(() => window.__godsEyeView?.dataManager
|
||||
&& document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60000 });
|
||||
await readinessPage.waitForFunction(() => window.__godsEyeView?.dataManager, { timeout: 60000 });
|
||||
await whenSettled(readinessPage);
|
||||
await readinessPage.evaluate(async () => {
|
||||
const manager = window.__godsEyeView.dataManager;
|
||||
await manager.setEnabled('flights', true, { origin: 'user' });
|
||||
@@ -1100,7 +1087,7 @@ try {
|
||||
const awareness = dataManager.layers.get('military-awareness')?.module;
|
||||
const before = awareness?.getContextSnapshot?.()?.subject || null;
|
||||
const entityBefore = viewer.trackedEntity;
|
||||
viewer.camera.zoomOut(1_500_000);
|
||||
window.__godsEyeView.engine.camera.zoomOut(1_500_000);
|
||||
const cameraRange = () => Math.hypot(
|
||||
viewer.camera.position.x,
|
||||
viewer.camera.position.y,
|
||||
@@ -1465,7 +1452,7 @@ try {
|
||||
remove();
|
||||
reject(new Error('Cockpit camera ownership check did not render a frame'));
|
||||
}, 5000);
|
||||
const remove = viewer.scene.postRender.addEventListener(() => {
|
||||
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
remove();
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
@@ -1473,10 +1460,10 @@ try {
|
||||
});
|
||||
// Let revoked listeners retire before comparing ownership across exit.
|
||||
await nextFrame();
|
||||
const listenersBeforeExit = viewer.scene.preUpdate.numberOfListeners;
|
||||
const listenersBeforeExit = window.__godsEyeView.engine.frames.preUpdate.numberOfListeners;
|
||||
const exited = styleManager.cockpitView.exit() === true;
|
||||
await nextFrame();
|
||||
const listenersAfterExit = viewer.scene.preUpdate.numberOfListeners;
|
||||
const listenersAfterExit = window.__godsEyeView.engine.frames.preUpdate.numberOfListeners;
|
||||
const refocused = awareness?.focusCurrent?.() === true;
|
||||
await nextFrame();
|
||||
return {
|
||||
@@ -1485,7 +1472,7 @@ try {
|
||||
sameEntity: viewer.trackedEntity === entity,
|
||||
listenersBeforeExit,
|
||||
listenersAfterExit,
|
||||
listenersAfterFocus: viewer.scene.preUpdate.numberOfListeners,
|
||||
listenersAfterFocus: window.__godsEyeView.engine.frames.preUpdate.numberOfListeners,
|
||||
};
|
||||
});
|
||||
check(
|
||||
@@ -2088,7 +2075,7 @@ try {
|
||||
JSON.stringify(cyberStyleStates));
|
||||
for (const height of [987, 720, 640, 900]) {
|
||||
await page.setViewport({ width: 1440, height, deviceScaleFactor: 1 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await whenUiSettled(page);
|
||||
const geometry = await page.evaluate(() => {
|
||||
const signal = document.getElementById('cockpit-signal-stream');
|
||||
const utility = document.getElementById('cockpit-utility-controls');
|
||||
@@ -2379,7 +2366,7 @@ try {
|
||||
await page.waitForFunction(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
if (!viewer) return false;
|
||||
const height = viewer.camera.positionCartographic?.height;
|
||||
const height = window.__godsEyeView.engine.camera.pose().cartographic?.height;
|
||||
return Math.abs(height - 18_000_000) < 150_000;
|
||||
}, { timeout: 6_000 });
|
||||
const resetState = await page.evaluate(() => {
|
||||
@@ -2387,7 +2374,7 @@ try {
|
||||
const qa = window.__qaCockpitReset;
|
||||
const awareness = gev.dataManager.layers.get('military-awareness')?.module;
|
||||
const subjectId = awareness?.getContextSnapshot?.()?.subject?.id || null;
|
||||
const height = gev.viewer.camera.positionCartographic?.height;
|
||||
const height = window.__godsEyeView.engine.camera.pose().cartographic?.height;
|
||||
gev.styleManager._locationNavigation.resetToGlobeView = qa.original;
|
||||
delete window.__qaCockpitReset;
|
||||
return {
|
||||
@@ -2440,7 +2427,7 @@ try {
|
||||
else hud.style.removeProperty('--cockpit-utility-top');
|
||||
if (!prior.cockpit) document.body.classList.remove('cockpit-mode');
|
||||
}).catch(() => {});
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
console.log(`RESULT: ${failures.length ? 'NOT_READY' : 'READY'} (${failures.length} failures)`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* qa-context-arrivals.mjs — search arrivals above the surface, and Contacts
|
||||
* listing mapped installations around a moving tracked aircraft.
|
||||
@@ -25,9 +26,10 @@
|
||||
* Writes <out>/result.json, screenshots and <out>/clip.mp4 (CDP screencast
|
||||
* with real frame timing; needs ffmpeg).
|
||||
*/
|
||||
import { whenSettled, whenFramesDrawn, whenTilesLoaded } 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 { stubPlanespotters } from './qa-photo-stub.mjs';
|
||||
import { createScreencast, moveCamera } from './qa-journey-recorder.mjs';
|
||||
|
||||
@@ -62,14 +64,12 @@ const check = (name, ok, detail = '') => {
|
||||
if (!ok) result.failures.push(`${name}${detail ? ` — ${detail}` : ''}`);
|
||||
};
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !argv.includes('--headful'),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
@@ -171,12 +171,10 @@ async function openPage({ aircraft = false } = {}) {
|
||||
const url = new URL(appUrl);
|
||||
url.searchParams.set('welcome', '0');
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -185,31 +183,16 @@ async function openPage({ aircraft = false } = {}) {
|
||||
}
|
||||
|
||||
async function waitTiles(page, maxMs = 30_000) {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
if (scene.globe.show) return scene.globe.tilesLoaded;
|
||||
for (let i = 0; i < scene.primitives.length; i++) {
|
||||
const p = scene.primitives.get(i);
|
||||
if (p.show && p.tilesLoaded === false) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: maxMs, polling: 250 },
|
||||
)
|
||||
.catch(() => {});
|
||||
await whenTilesLoaded(page, { timeoutMs: maxMs }).catch(() => {});
|
||||
}
|
||||
|
||||
/** Camera height above the rendered surface under it and under the view centre. */
|
||||
function clearance(page) {
|
||||
return page.evaluate(() => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
const { scene, camera } = viewer;
|
||||
const carto = camera.positionCartographic.clone();
|
||||
return evaluateWithGeo(page, async (qaGeo) => {
|
||||
const carto = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
const sample = (c) => {
|
||||
try {
|
||||
const h = scene.sampleHeight(c);
|
||||
const h = window.__godsEyeView.engine.surface.sampleHeight(c);
|
||||
return Number.isFinite(h) ? h : null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -217,12 +200,12 @@ function clearance(page) {
|
||||
};
|
||||
const under = sample(carto);
|
||||
let centre = null;
|
||||
const canvas = scene.canvas;
|
||||
const hit = scene.pickPosition?.({
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const hit = window.__godsEyeView.engine.picker.pickPosition?.({
|
||||
x: canvas.clientWidth / 2,
|
||||
y: canvas.clientHeight / 2,
|
||||
});
|
||||
if (hit) centre = scene.globe.ellipsoid.cartesianToCartographic(hit);
|
||||
if (hit) centre = qaGeo.cartographic.fromCartesian(hit);
|
||||
const centreSurface = centre ? sample(centre) : null;
|
||||
const surface = Math.max(under ?? -Infinity, centreSurface ?? -Infinity);
|
||||
return {
|
||||
@@ -234,7 +217,9 @@ function clearance(page) {
|
||||
clearanceM: Number.isFinite(surface)
|
||||
? Math.round(carto.height - surface)
|
||||
: null,
|
||||
pitchDeg: Math.round((camera.pitch * 180) / Math.PI),
|
||||
pitchDeg: Math.round(
|
||||
(window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -255,47 +240,32 @@ async function arrivals() {
|
||||
});
|
||||
await page.type('#location-search', query, { delay: 40 });
|
||||
await page.keyboard.press('Enter');
|
||||
// Wait for the flight to land: the camera still for 1.5 s.
|
||||
const landed = Date.now();
|
||||
let last = null;
|
||||
let stillSince = null;
|
||||
while (Date.now() - landed < 30_000) {
|
||||
const now = await page.evaluate(() => {
|
||||
const c = window.__godsEyeView.viewer.camera.positionWC;
|
||||
return [c.x, c.y, c.z];
|
||||
});
|
||||
const moved =
|
||||
!last ||
|
||||
Math.hypot(now[0] - last[0], now[1] - last[1], now[2] - last[2]) > 0.5;
|
||||
last = now;
|
||||
if (moved) stillSince = null;
|
||||
else stillSince ??= Date.now();
|
||||
if (
|
||||
Date.now() - landed > 4000 &&
|
||||
stillSince &&
|
||||
Date.now() - stillSince > 1500
|
||||
)
|
||||
break;
|
||||
await sleep(250);
|
||||
}
|
||||
await whenSettled(page);
|
||||
await waitTiles(page);
|
||||
await sleep(2500); // the ground guard measures after tiles stream in
|
||||
await whenSettled(page); // the ground guard measures after tiles stream in
|
||||
const arrived = await clearance(page);
|
||||
const slug = query.replace(/\W+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
||||
await page.screenshot({ path: path.join(dir, `arrival-${slug}.png`) });
|
||||
// Look around: a gentle orbit, then check the eye still clears the ground.
|
||||
const view = await page.evaluate(() => {
|
||||
const c = window.__godsEyeView.viewer.camera;
|
||||
return {
|
||||
lat: (c.positionCartographic.latitude * 180) / Math.PI,
|
||||
lon: (c.positionCartographic.longitude * 180) / Math.PI,
|
||||
height: c.positionCartographic.height,
|
||||
heading: (c.heading * 180) / Math.PI,
|
||||
pitch: (c.pitch * 180) / Math.PI,
|
||||
lat:
|
||||
(window.__godsEyeView.engine.camera.pose().cartographic.latitude *
|
||||
180) /
|
||||
Math.PI,
|
||||
lon:
|
||||
(window.__godsEyeView.engine.camera.pose().cartographic.longitude *
|
||||
180) /
|
||||
Math.PI,
|
||||
height: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
heading:
|
||||
(window.__godsEyeView.engine.camera.pose().heading * 180) / Math.PI,
|
||||
pitch:
|
||||
(window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
|
||||
};
|
||||
});
|
||||
await moveCamera(page, { ...view, heading: view.heading + 25 }, 2.5);
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
const lookedAround = await clearance(page);
|
||||
result.arrivals.push({ query, arrived, lookedAround });
|
||||
check(
|
||||
@@ -323,7 +293,7 @@ async function contacts() {
|
||||
{ lat: 30.35, lon: -97.76, height: 900_000, pitch: -90 },
|
||||
0,
|
||||
);
|
||||
await sleep(3000);
|
||||
await whenFramesDrawn(page);
|
||||
// In-page probe: row text and installation entity churn every 250 ms.
|
||||
await page.evaluate(() => {
|
||||
const samples = [];
|
||||
@@ -368,7 +338,7 @@ async function contacts() {
|
||||
entities: sites.length,
|
||||
created,
|
||||
tracked:
|
||||
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ?? null,
|
||||
window.__godsEyeView.engine.camera.following()?.label?.id ?? null,
|
||||
cockpit: document.body.classList.contains('cockpit-mode'),
|
||||
});
|
||||
if (samples.length > 5000) samples.splice(0, 1000);
|
||||
@@ -383,7 +353,7 @@ async function contacts() {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ===
|
||||
window.__godsEyeView.engine.camera.following()?.label?.id ===
|
||||
'flights:aaa051',
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
@@ -478,7 +448,7 @@ async function contacts() {
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.cockpitView.exit?.(),
|
||||
);
|
||||
await sleep(2000);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
// SEARCH NEARBY SITES, as the user presses it.
|
||||
await page.evaluate(() => {
|
||||
@@ -504,7 +474,7 @@ async function contacts() {
|
||||
const toast = await page.evaluate(() =>
|
||||
document.getElementById('toast').textContent.trim(),
|
||||
);
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
const afterSearch = (
|
||||
await readSamples((await page.evaluate(() => performance.now())) - 1000)
|
||||
).at(-1);
|
||||
@@ -519,13 +489,13 @@ async function contacts() {
|
||||
await page.evaluate(() =>
|
||||
document.getElementById('global-context-flights-btn').click(),
|
||||
);
|
||||
await sleep(2000);
|
||||
await whenFramesDrawn(page);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.layers
|
||||
.get('flights')
|
||||
.module.trackById('aaa052'),
|
||||
);
|
||||
await sleep(3000);
|
||||
await whenFramesDrawn(page);
|
||||
const t1 = await page.evaluate(() => performance.now());
|
||||
await page.evaluate(() =>
|
||||
document.getElementById('global-context-flights-btn').click(),
|
||||
@@ -554,7 +524,7 @@ async function contacts() {
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
await sleep(2000);
|
||||
await whenFramesDrawn(page);
|
||||
await page.screenshot({ path: path.join(dir, 'contacts-fort-cavazos.png') });
|
||||
result.contacts.cavazos = cavazos;
|
||||
check(
|
||||
@@ -600,7 +570,7 @@ try {
|
||||
path.join(outDir, 'result.json'),
|
||||
JSON.stringify(result, null, 1),
|
||||
);
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(
|
||||
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
|
||||
|
||||
+44
-43
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native primitive inspection independently verifies the Cesium route geometry.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenUiSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
/**
|
||||
* Browser proof of the Directions layer: real chip clicks, real globe clicks,
|
||||
* a real route from the real proxy, a real camera flight.
|
||||
@@ -6,10 +9,10 @@
|
||||
* Usage: node scripts/qa-directions.mjs [--url http://localhost:4173]
|
||||
* [--shots qa-shots/directions] [--label keyed] [--skip denver]
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
@@ -34,9 +37,8 @@ const check = (name, passed, detail = '') => {
|
||||
if (!passed) failures += 1;
|
||||
};
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: process.env.QA_HEADFUL === '1' ? false : true,
|
||||
executablePath: chromeExecutablePath(),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
@@ -62,12 +64,12 @@ const shot = async (name) => {
|
||||
|
||||
/** Put the camera over a place, instantly, and let the scene settle. */
|
||||
async function lookAt(lat, lon, height) {
|
||||
await page.evaluate(
|
||||
(la, lo, h) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const Cartesian3 = viewer.camera.positionWC.constructor;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, la, lo, h) => {
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: Cartesian3.fromDegrees(lo, la, h),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2.2, roll: 0 },
|
||||
});
|
||||
@@ -80,17 +82,16 @@ async function lookAt(lat, lon, height) {
|
||||
|
||||
/** Canvas position of a lon/lat on the rendered surface, or null. */
|
||||
const screenAt = (lat, lon) =>
|
||||
page.evaluate(
|
||||
(la, lo) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const scene = viewer.scene;
|
||||
const Cartesian3 = viewer.camera.positionWC.constructor;
|
||||
const Cartographic = viewer.camera.positionCartographic.constructor;
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, la, lo) => {
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
const carto = Cartographic.fromDegrees(lo, la);
|
||||
let ground = scene.globe?.getHeight?.(carto);
|
||||
let ground = window.__godsEyeView.engine.surface.terrainHeight?.(carto);
|
||||
if (!Number.isFinite(ground)) ground = 0;
|
||||
const world = Cartesian3.fromDegrees(lo, la, ground);
|
||||
const win = scene.cartesianToCanvasCoordinates(world);
|
||||
const win = window.__godsEyeView.engine.projector.worldToWindow(world);
|
||||
if (!win || !Number.isFinite(win.x)) return null;
|
||||
const x = Math.round(win.x);
|
||||
const y = Math.round(win.y);
|
||||
@@ -150,7 +151,7 @@ const clickChip = async (chipId) => {
|
||||
button.click();
|
||||
return true;
|
||||
}, chipId);
|
||||
await sleep(150);
|
||||
await whenUiSettled(page);
|
||||
return clicked;
|
||||
};
|
||||
|
||||
@@ -180,7 +181,7 @@ const waitForMode = (word) =>
|
||||
|
||||
const cameraPose = () =>
|
||||
page.evaluate(() => {
|
||||
const c = window.__godsEyeView.viewer.camera.positionWC;
|
||||
const c = window.__godsEyeView.engine.camera.pose().position;
|
||||
return { x: c.x, y: c.y, z: c.z };
|
||||
});
|
||||
const poseDelta = (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
@@ -191,7 +192,7 @@ async function placeEndpoints(a, b) {
|
||||
const atA = await screenAt(a.lat, a.lon);
|
||||
if (!atA) return false;
|
||||
await page.mouse.click(atA.x, atA.y);
|
||||
await sleep(700);
|
||||
await whenUiSettled(page);
|
||||
await clickChip('set-b');
|
||||
const atB = await screenAt(b.lat, b.lon);
|
||||
if (!atB) return false;
|
||||
@@ -207,6 +208,7 @@ try {
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 90000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
// Two module reads the assertions need. Both are the app's OWN module
|
||||
// instances (Vite serves one copy), so this observes rather than simulates.
|
||||
await page.evaluate(async () => {
|
||||
@@ -311,7 +313,7 @@ try {
|
||||
},
|
||||
};
|
||||
});
|
||||
await sleep(3000);
|
||||
await whenSettled(page);
|
||||
|
||||
// ── 1. Open the UTILITIES panel, then toggle the row on. ────────────────
|
||||
const opened = await page.evaluate(() => {
|
||||
@@ -328,7 +330,7 @@ try {
|
||||
.getElementById('utilities-panel')
|
||||
.classList.contains('collapsed');
|
||||
});
|
||||
await sleep(700);
|
||||
await whenUiSettled(page);
|
||||
check('the UTILITIES panel opens from its own header control', opened);
|
||||
const toggled = await page.evaluate(() => {
|
||||
const row = document.querySelector(
|
||||
@@ -340,7 +342,7 @@ try {
|
||||
button.click();
|
||||
return true;
|
||||
});
|
||||
await sleep(1200);
|
||||
await whenUiSettled(page);
|
||||
check(
|
||||
'the Directions row is in the Data Layers panel and toggles on',
|
||||
toggled,
|
||||
@@ -388,13 +390,13 @@ try {
|
||||
);
|
||||
close?.click();
|
||||
});
|
||||
await sleep(500);
|
||||
await whenUiSettled(page);
|
||||
|
||||
// ── 2. Place A on the Ferry Building, B in Golden Gate Park. ─────────────
|
||||
const FERRY = { lat: 37.7955, lon: -122.3937 };
|
||||
const GGPARK = { lat: 37.7694, lon: -122.4862 };
|
||||
await lookAt(37.782, -122.44, 16000);
|
||||
await sleep(5000);
|
||||
await whenSettled(page);
|
||||
|
||||
await clickChip('set-a');
|
||||
state = await layerState();
|
||||
@@ -411,7 +413,7 @@ try {
|
||||
let at = await screenAt(FERRY.lat, FERRY.lon);
|
||||
check('the Ferry Building is on screen', !!at, JSON.stringify(at));
|
||||
await page.mouse.click(at.x, at.y);
|
||||
await sleep(800);
|
||||
await whenUiSettled(page);
|
||||
state = await layerState();
|
||||
check(
|
||||
'placing A releases the pointer claim',
|
||||
@@ -425,7 +427,7 @@ try {
|
||||
// used to place B and deselect whatever was under it in the same gesture.
|
||||
await page.evaluate(() => {
|
||||
window.__gevQaLateHandler = [];
|
||||
const canvas = window.__godsEyeView.viewer.scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
canvas.addEventListener('pointerup', () => {
|
||||
window.__gevQaLateHandler.push(window.__gevQa.pointerOwner());
|
||||
});
|
||||
@@ -443,7 +445,7 @@ try {
|
||||
`saw ${JSON.stringify(lateOwners)}`,
|
||||
);
|
||||
await waitForRoute();
|
||||
await sleep(2000);
|
||||
await whenSettled(page);
|
||||
state = await layerState();
|
||||
check(
|
||||
'once that click is over, the pointer claim is returned',
|
||||
@@ -488,7 +490,7 @@ try {
|
||||
items[2]?.click();
|
||||
return { active, tabIndex: items[2]?.tabIndex };
|
||||
});
|
||||
await sleep(600);
|
||||
await whenUiSettled(page);
|
||||
const afterListClick = await layerState();
|
||||
check(
|
||||
'a list row takes keyboard focus and selects its maneuver',
|
||||
@@ -525,7 +527,7 @@ try {
|
||||
// ── 4. Swap endpoints. ──────────────────────────────────────────────────
|
||||
const beforeSwap = await layerState();
|
||||
await clickChip('swap');
|
||||
await sleep(3500);
|
||||
await waitForRoute();
|
||||
const afterSwap = await layerState();
|
||||
check(
|
||||
'SWAP reverses the route',
|
||||
@@ -534,7 +536,7 @@ try {
|
||||
`"${beforeSwap.listText[0]}" -> "${afterSwap.listText[0]}"`,
|
||||
);
|
||||
await clickChip('swap');
|
||||
await sleep(3500);
|
||||
await waitForRoute();
|
||||
|
||||
// ── 5. Three rapid reroutes leave exactly one route. ─────────────────────
|
||||
await page.evaluate(() => {
|
||||
@@ -543,7 +545,7 @@ try {
|
||||
manager.setLayerParams('directions', { mode: 'bike' }, { origin: 'user' });
|
||||
manager.setLayerParams('directions', { mode: 'car' }, { origin: 'user' });
|
||||
});
|
||||
await sleep(6000);
|
||||
await waitForRoute();
|
||||
const rapid = await layerState();
|
||||
check(
|
||||
'three rapid reroutes leave exactly one route',
|
||||
@@ -656,7 +658,7 @@ try {
|
||||
),
|
||||
);
|
||||
await waitForRoute();
|
||||
await sleep(1200);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
await cdp.send('Fetch.disable').catch(() => {});
|
||||
|
||||
@@ -686,7 +688,7 @@ try {
|
||||
// A reroute replaces the route the dolly is flying; the dolly has to land
|
||||
// first rather than keep flying a route that no longer exists.
|
||||
await setMode('foot');
|
||||
await sleep(1200);
|
||||
await whenUiSettled(page);
|
||||
const afterReroute = await layerState();
|
||||
check(
|
||||
'changing profile mid-flight lands the flight it replaced',
|
||||
@@ -696,7 +698,7 @@ try {
|
||||
await waitForMode('Walk');
|
||||
await setMode('car');
|
||||
await waitForMode('Drive');
|
||||
await sleep(800);
|
||||
await whenUiSettled(page);
|
||||
check('FLY is offered again after the reroute', await clickChip('fly'));
|
||||
await sleep(2000);
|
||||
|
||||
@@ -768,7 +770,7 @@ try {
|
||||
// ── 9. Denver: maneuver dots sit on the ground, not at sea level. ────────
|
||||
if (!SKIP.has('denver')) {
|
||||
await lookAt(39.744, -104.999, 7000);
|
||||
await sleep(6000);
|
||||
await whenSettled(page);
|
||||
const placed = await placeEndpoints(
|
||||
{ lat: 39.7392, lon: -104.9903 },
|
||||
{ lat: 39.7487, lon: -105.0077 },
|
||||
@@ -777,10 +779,9 @@ try {
|
||||
if (placed) {
|
||||
await waitForRoute();
|
||||
// Let the shared ground-floor cells warm and the dots re-anchor.
|
||||
await sleep(9000);
|
||||
const denver = await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const Cartographic = viewer.camera.positionCartographic.constructor;
|
||||
await whenTilesLoaded(page);
|
||||
const denver = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
|
||||
const heights = stepsShown.map(
|
||||
(position) => Cartographic.fromCartesian(position).height,
|
||||
@@ -817,7 +818,7 @@ try {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
const off = await page.evaluate(() => {
|
||||
const drawn = window.__gevQaDirections.drawn();
|
||||
const row = document.querySelector(
|
||||
@@ -877,7 +878,7 @@ try {
|
||||
check('harness completed', false, error?.stack || String(error));
|
||||
await shot(`99-${LABEL}-failure.jpg`).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
await fs.writeFile(
|
||||
path.join(SHOTS, `qa-directions-${LABEL}.log`),
|
||||
`${lines.join('\n')}\n${failures} failure(s)\n`,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
/** Verify imported camera directions through real rendering and camera authority handoffs. */
|
||||
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';
|
||||
const output = path.resolve('qa-shots/director-camera');
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||
});
|
||||
@@ -23,12 +24,10 @@ try {
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
const result = await page.evaluate(async () => {
|
||||
const { sceneDirector: d, styleManager: style } = window.__godsEyeView;
|
||||
const file = {
|
||||
@@ -137,7 +136,7 @@ try {
|
||||
};
|
||||
});
|
||||
for (const [name, passed] of Object.entries(result)) check(name, passed);
|
||||
await new Promise((r) => setTimeout(r, 6000));
|
||||
await whenSettled(page);
|
||||
await page.screenshot({ path: path.join(output, 'austin.png') });
|
||||
await page.evaluate(() => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
@@ -145,7 +144,7 @@ try {
|
||||
d._setCameraView({ ...pose, heading: pose.heading + 25, pitch: -60 });
|
||||
d.viewer.scene.requestRender();
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
await whenSettled(page);
|
||||
await page.screenshot({ path: path.join(output, 'angle.png') });
|
||||
const inputStarted = await page.evaluate(async () => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
@@ -205,6 +204,6 @@ try {
|
||||
);
|
||||
check('no uncaught browser errors', errors.length === 0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (failed) process.exitCode = 1;
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
/** Real pointer/keyboard acceptance using a synthetic, self-authored feature pack. */
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
const output = 'qa-shots/director-interactions';
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||
});
|
||||
@@ -52,12 +53,10 @@ try {
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0#v=2&lat=30.2672&lon=-97.742&alt=4000&heading=0&pitch=-90&l=`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
const camera = {
|
||||
@@ -163,11 +162,8 @@ try {
|
||||
window.__godsEyeView.sceneDirector.getInteractionState().count === 4,
|
||||
),
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.sceneDirector.engine.surface.tilesLoaded(),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||
await whenSettled(page);
|
||||
const point = await page.evaluate(async () => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
const { cartesian3 } = await import('/src/geo/index.js');
|
||||
@@ -366,6 +362,6 @@ try {
|
||||
);
|
||||
check('no uncaught browser errors', errors.length === 0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native primitive readiness and entity counts detect Cesium resource leaks.
|
||||
/** Synthetic, self-authored assets exercise import, rendering and pack resource lifetime. */
|
||||
import { whenSettled, whenTilesLoaded, until } 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 sharp from 'sharp';
|
||||
const output = path.resolve('qa-shots/director-packs');
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
@@ -65,7 +67,7 @@ const geojson = {
|
||||
},
|
||||
],
|
||||
};
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||
});
|
||||
@@ -105,12 +107,10 @@ try {
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
const manifest = (id, format, file, placement) => ({
|
||||
@@ -230,14 +230,11 @@ try {
|
||||
() => window.__godsEyeView.mapStackController.getActiveId() === 'photoreal',
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.tileset?.tilesLoaded === true,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||
const evidence = await page.evaluate(() => ({
|
||||
mapStack: window.__godsEyeView.mapStackController.getActiveId(),
|
||||
tilesSettled: window.__godsEyeView.tileset?.tilesLoaded === true,
|
||||
tilesSettled: window.__godsEyeView.engine.surface.tilesLoaded() === true,
|
||||
}));
|
||||
fs.writeFileSync(
|
||||
path.join(output, 'report.json'),
|
||||
@@ -262,11 +259,8 @@ try {
|
||||
pitch: -85,
|
||||
});
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.tileset?.tilesLoaded === true,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||
await whenSettled(page);
|
||||
await page.screenshot({ path: path.join(output, 'angle.png') });
|
||||
const life = await page.evaluate(async () => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
@@ -306,9 +300,10 @@ try {
|
||||
'slow',
|
||||
);
|
||||
});
|
||||
const deadline = Date.now() + 15000;
|
||||
while (!pending && Date.now() < deadline)
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await until(() => pending, {
|
||||
timeoutMs: 15000,
|
||||
label: 'pending pack transport',
|
||||
});
|
||||
check('pending transport reached', !!pending);
|
||||
const cancelled = await page.evaluate(async () => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
@@ -354,6 +349,6 @@ try {
|
||||
if (errors.length)
|
||||
console.error(errors.map((e) => e.replace(/https?:\/\/\S+/g, '[URL]')));
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (failures) process.exitCode = 1;
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
/** Sharing/authoring acceptance with local synthetic assets and real installed controls. */
|
||||
import { whenSettled, whenTilesLoaded } 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';
|
||||
const output = path.resolve('qa-shots/director-sharing');
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
const geometry = {
|
||||
@@ -66,7 +67,7 @@ const project = {
|
||||
};
|
||||
const file = path.join(output, 'project.json');
|
||||
fs.writeFileSync(file, JSON.stringify(project));
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||
});
|
||||
@@ -110,12 +111,10 @@ try {
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.click('[data-collapse-target="scene-panel"]');
|
||||
const before = await page.evaluate(() =>
|
||||
JSON.stringify(window.__godsEyeView.sceneDirector._project),
|
||||
@@ -304,16 +303,14 @@ try {
|
||||
window.__godsEyeView.sceneDirector.getInteractionState().count === 1,
|
||||
)),
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.tileset?.tilesLoaded === true,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||
await page.screenshot({ path: path.join(output, 'rendered.png') });
|
||||
fs.writeFileSync(
|
||||
path.join(output, 'report.json'),
|
||||
JSON.stringify(
|
||||
await page.evaluate(() => ({
|
||||
tilesSettled: window.__godsEyeView.tileset?.tilesLoaded === true,
|
||||
tilesSettled:
|
||||
window.__godsEyeView.engine.surface.tilesLoaded() === true,
|
||||
})),
|
||||
),
|
||||
);
|
||||
@@ -480,6 +477,6 @@ try {
|
||||
);
|
||||
check('no uncaught browser errors', errors.length === 0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
/** Exercise scene-clock ownership and authored seek behavior in the composed browser app. */
|
||||
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';
|
||||
const shots = path.resolve('qa-shots/director-timing');
|
||||
fs.mkdirSync(shots, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||
});
|
||||
@@ -23,12 +24,10 @@ try {
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
const result = await page.evaluate(async () => {
|
||||
const director = window.__godsEyeView.sceneDirector;
|
||||
const camera = director.styleManager.getCameraState();
|
||||
@@ -172,7 +171,7 @@ try {
|
||||
'existing Nepal scene and authored shots load',
|
||||
content.exists && content.shots >= 25 && content.loaded,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2500));
|
||||
await whenSettled(page);
|
||||
await page.screenshot({ path: path.join(shots, 'nepal-regional.png') });
|
||||
await page.evaluate(() => {
|
||||
const g = window.__godsEyeView;
|
||||
@@ -184,7 +183,7 @@ try {
|
||||
pitch: -55,
|
||||
});
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await whenSettled(page);
|
||||
await page.screenshot({ path: path.join(shots, 'nepal-angle.png') });
|
||||
check(
|
||||
'no pending clocks after Stop',
|
||||
@@ -196,6 +195,6 @@ try {
|
||||
);
|
||||
check('no browser exceptions', errors.length === 0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (failures) process.exitCode = 1;
|
||||
+142
-158
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: the stock input action and DataSource counts verify Cesium input restoration.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Rendered acceptance for UTILITIES ▸ Draw: real clicks, real keys, real teardown. */
|
||||
import { whenSettled, whenTilesLoaded } 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 { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
|
||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
@@ -19,21 +22,14 @@ const VIEW = {
|
||||
pitch: -85,
|
||||
};
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !HEADFUL,
|
||||
...(HEADFUL
|
||||
? {
|
||||
executablePath: chromeExecutablePath(),
|
||||
}
|
||||
: {}),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
...(HEADFUL ? ['--window-size=1280,860'] : []),
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -68,7 +64,6 @@ const shot = (name) =>
|
||||
type: 'jpeg',
|
||||
quality: 82,
|
||||
});
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/** A clip of the globe canvas, clear of the panels on every side. */
|
||||
const CANVAS_CLIP = { x: 340, y: 200, width: 560, height: 380 };
|
||||
@@ -163,7 +158,7 @@ async function pressFinish() {
|
||||
/** One click on the globe canvas, the way a person makes one. */
|
||||
async function clickWorld(x, y, options = {}) {
|
||||
await page.mouse.click(x, y, options);
|
||||
await wait(options.settle ?? 140);
|
||||
await whenSettled(page);
|
||||
}
|
||||
|
||||
const drawState = () =>
|
||||
@@ -211,50 +206,34 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__gevDrawTool &&
|
||||
window.__gevAnnotations &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__gevDrawTool && window.__gevAnnotations,
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await page.evaluate((view) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.scene.tweens?.removeAll?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: (view.heading * Math.PI) / 180,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, VIEW);
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let index = 0; index < primitives.length; index++) {
|
||||
const primitive = primitives.get(index);
|
||||
if (
|
||||
typeof primitive.tilesLoaded === 'boolean' &&
|
||||
!primitive.tilesLoaded
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
await whenSettled(page);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, view) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: (view.heading * Math.PI) / 180,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
VIEW,
|
||||
);
|
||||
const settled = await whenTilesLoaded(page, { timeoutMs: 90_000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
check('visible tile content settles before drawing', settled);
|
||||
|
||||
// UTILITIES starts collapsed on a first run: open it the way a user does.
|
||||
@@ -321,9 +300,9 @@ try {
|
||||
const before = await drawState();
|
||||
for (let cycle = 0; cycle < 3; cycle += 1) {
|
||||
await page.click('#draw-toggle');
|
||||
await wait(120);
|
||||
await whenSettled(page);
|
||||
await page.click('#draw-toggle');
|
||||
await wait(120);
|
||||
await whenSettled(page);
|
||||
}
|
||||
const after = await drawState();
|
||||
check(
|
||||
@@ -379,7 +358,7 @@ try {
|
||||
await clickWorld(600, 470);
|
||||
const beforeBackspace = (await drawState()).vertices;
|
||||
await page.keyboard.press('Backspace');
|
||||
await wait(120);
|
||||
await whenSettled(page);
|
||||
const afterBackspace = (await drawState()).vertices;
|
||||
check(
|
||||
'Backspace removes the last vertex only',
|
||||
@@ -415,7 +394,7 @@ try {
|
||||
);
|
||||
await page.mouse.click(410, 500, { count: 2 });
|
||||
const areaLanded = await waitForMarks(3);
|
||||
await wait(900);
|
||||
await whenSettled(page);
|
||||
const afterDoubleClick = await page.evaluate(() =>
|
||||
window.__gevAnnotations.count(),
|
||||
);
|
||||
@@ -440,7 +419,7 @@ try {
|
||||
await page.click(rowControl(areaId, 'name'));
|
||||
await page.keyboard.type('Civic Center block');
|
||||
await page.keyboard.press('Enter');
|
||||
await wait(300);
|
||||
await whenSettled(page);
|
||||
const areaMark = (await marks()).find(
|
||||
(mark) => mark.label === 'Civic Center block',
|
||||
);
|
||||
@@ -453,7 +432,7 @@ try {
|
||||
'naming a shape keeps Draw on and the shape in progress untouched',
|
||||
(await drawState()).active === true,
|
||||
);
|
||||
await wait(1200);
|
||||
await whenSettled(page);
|
||||
const amberAfter = await amberPixels();
|
||||
check(
|
||||
'the finished area actually PAINTS — amber pixels appear on this surface',
|
||||
@@ -512,32 +491,31 @@ try {
|
||||
// Fly-to frames the line by its whole path: once the flight settles, the
|
||||
// middle of the line sits near the middle of the view.
|
||||
await page.click(rowControl(ids[1], 'fly'));
|
||||
await page
|
||||
.waitForFunction(() => !window.__godsEyeView.viewer.camera._currentFlight, {
|
||||
timeout: 15_000,
|
||||
})
|
||||
.catch(() => {});
|
||||
await wait(300);
|
||||
const framed = await page.evaluate((id) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
// No Cesium global in the page: borrow Cartesian3 from a live object.
|
||||
const Cartesian3 = viewer.camera.positionWC.constructor;
|
||||
const mark = window.__gevAnnotations.list().find((m) => m.id === id);
|
||||
const lons = mark.path.map((p) => p.lon);
|
||||
const lats = mark.path.map((p) => p.lat);
|
||||
const middle = Cartesian3.fromDegrees(
|
||||
(Math.min(...lons) + Math.max(...lons)) / 2,
|
||||
(Math.min(...lats) + Math.max(...lats)) / 2,
|
||||
);
|
||||
const at = viewer.scene.cartesianToCanvasCoordinates(middle);
|
||||
const canvas = viewer.scene.canvas;
|
||||
return (
|
||||
at && {
|
||||
dx: Math.abs(at.x - canvas.clientWidth / 2) / canvas.clientWidth,
|
||||
dy: Math.abs(at.y - canvas.clientHeight / 2) / canvas.clientHeight,
|
||||
}
|
||||
);
|
||||
}, ids[1]);
|
||||
await whenSettled(page).catch(() => {});
|
||||
await whenSettled(page);
|
||||
const framed = await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, id) => {
|
||||
// No Cesium global in the page: borrow Cartesian3 from a live object.
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
const mark = window.__gevAnnotations.list().find((m) => m.id === id);
|
||||
const lons = mark.path.map((p) => p.lon);
|
||||
const lats = mark.path.map((p) => p.lat);
|
||||
const middle = Cartesian3.fromDegrees(
|
||||
(Math.min(...lons) + Math.max(...lons)) / 2,
|
||||
(Math.min(...lats) + Math.max(...lats)) / 2,
|
||||
);
|
||||
const at = window.__godsEyeView.engine.projector.worldToWindow(middle);
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
return (
|
||||
at && {
|
||||
dx: Math.abs(at.x - canvas.clientWidth / 2) / canvas.clientWidth,
|
||||
dy: Math.abs(at.y - canvas.clientHeight / 2) / canvas.clientHeight,
|
||||
}
|
||||
);
|
||||
},
|
||||
ids[1],
|
||||
);
|
||||
check(
|
||||
'fly to frames that shape: its middle is near the middle of the view',
|
||||
Boolean(framed) && framed.dx < 0.2 && framed.dy < 0.2,
|
||||
@@ -550,11 +528,11 @@ try {
|
||||
await page.click(rowControl(lineId, 'swatch'));
|
||||
await page.click(rowControl(lineId, 'color-amber'));
|
||||
await markReaches(lineId, { color: 'amber', alpha: 1 });
|
||||
await wait(800);
|
||||
await whenSettled(page);
|
||||
const shown = await canvasShot();
|
||||
await page.click(rowControl(lineId, 'toggle'));
|
||||
await markReaches(lineId, { hidden: true, alpha: 0 });
|
||||
await wait(800);
|
||||
await whenSettled(page);
|
||||
const hiddenShot = await canvasShot();
|
||||
// Amber the shown line had and the hidden one lost; amber that appears
|
||||
// only once it is hidden is noise, and must stay small beside it.
|
||||
@@ -566,7 +544,7 @@ try {
|
||||
`${gone} amber pixels went with the line, ${noise} appeared`,
|
||||
);
|
||||
await page.click(rowControl(lineId, 'toggle'));
|
||||
await wait(1200);
|
||||
await whenSettled(page);
|
||||
const back = await amberOnlyIn(await canvasShot(), hiddenShot);
|
||||
check(
|
||||
'and paints again when shown',
|
||||
@@ -581,7 +559,7 @@ try {
|
||||
await clickWorld(560, 340);
|
||||
check('a new shape is in progress', (await drawState()).vertices === 2);
|
||||
await page.keyboard.press('Escape');
|
||||
await wait(150);
|
||||
await whenSettled(page);
|
||||
const afterFirstEscape = await drawState();
|
||||
check(
|
||||
'Escape cancels the shape in progress and stays in draw mode',
|
||||
@@ -589,7 +567,7 @@ try {
|
||||
`vertices ${afterFirstEscape.vertices}, active ${afterFirstEscape.active}`,
|
||||
);
|
||||
await page.keyboard.press('Escape');
|
||||
await wait(150);
|
||||
await whenSettled(page);
|
||||
const afterSecondEscape = await drawState();
|
||||
check(
|
||||
'a second Escape leaves draw mode and gives the pointer back',
|
||||
@@ -667,7 +645,7 @@ try {
|
||||
);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.keyboard.press('Escape');
|
||||
await wait(150);
|
||||
await whenSettled(page);
|
||||
}
|
||||
|
||||
// ── what a reload actually does ─────────────────────────────────────────
|
||||
@@ -675,12 +653,10 @@ try {
|
||||
window.__gevAnnotations.count(),
|
||||
);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__gevAnnotations &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__gevAnnotations, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
const afterReload = await page.evaluate(() =>
|
||||
window.__gevAnnotations.count(),
|
||||
);
|
||||
@@ -722,25 +698,28 @@ try {
|
||||
// flight left a low, near-horizontal view there) with a boot flight that can
|
||||
// land after a setView: set the view until it holds.
|
||||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||||
await page.evaluate((view) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
}, VIEW);
|
||||
await wait(2500);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, view) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
VIEW,
|
||||
);
|
||||
await whenSettled(page);
|
||||
const held = await page.evaluate((view) => {
|
||||
const carto = window.__godsEyeView.viewer.camera.positionCartographic;
|
||||
const carto = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return (
|
||||
Math.abs((carto.latitude * 180) / Math.PI - view.lat) < 0.01 &&
|
||||
Math.abs((carto.longitude * 180) / Math.PI - view.lon) < 0.01 &&
|
||||
@@ -767,7 +746,7 @@ try {
|
||||
const beforeButtonEnter = await drawState();
|
||||
await page.focus('#draw-clear');
|
||||
await page.keyboard.press('Enter');
|
||||
await wait(600);
|
||||
await whenSettled(page);
|
||||
const afterButtonEnter = await drawState();
|
||||
check(
|
||||
'Enter on a keyboard-focused button activates the button, it does not finish the shape',
|
||||
@@ -790,7 +769,7 @@ try {
|
||||
await waitForMarks(1),
|
||||
);
|
||||
await page.click('#draw-clear');
|
||||
await wait(400);
|
||||
await whenSettled(page);
|
||||
const cleared = await drawState();
|
||||
check(
|
||||
'Clear removes every mark from the board',
|
||||
@@ -815,7 +794,7 @@ try {
|
||||
`${degenerateVertices} vertices`,
|
||||
);
|
||||
await pressFinish();
|
||||
await wait(600);
|
||||
await whenSettled(page);
|
||||
const degenerate = await drawState();
|
||||
check(
|
||||
'three points in a line are refused with a reason, not placed',
|
||||
@@ -824,20 +803,19 @@ try {
|
||||
);
|
||||
|
||||
// ── a shape drawn across the antimeridian lands where it was drawn ──────
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (179.9995 * Math.PI) / 180,
|
||||
latitude: 0,
|
||||
height: 3000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await wait(2500);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
const tool = window.__gevDrawTool;
|
||||
tool.cancel();
|
||||
@@ -865,7 +843,7 @@ try {
|
||||
await page.evaluate(() => window.__gevAnnotations.clear());
|
||||
|
||||
await page.click('#draw-toggle');
|
||||
await wait(200);
|
||||
await whenSettled(page);
|
||||
const finalState = await drawState();
|
||||
check(
|
||||
'leaving draw mode gives back the pointer, the handler and the double-click',
|
||||
@@ -880,7 +858,7 @@ try {
|
||||
() => window.__godsEyeView.viewer.dataSources.length,
|
||||
);
|
||||
await page.click('#draw-toggle'); // destroy from an ACTIVE session: the harder case
|
||||
await wait(200);
|
||||
await whenSettled(page);
|
||||
const destroyed = await page.evaluate(async () => {
|
||||
const tool = window.__gevDrawTool;
|
||||
const wasActive = tool.active;
|
||||
@@ -936,7 +914,7 @@ try {
|
||||
errors.join(' | '),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -975,20 +953,23 @@ async function findClickableTarget() {
|
||||
.catch(() => null);
|
||||
|
||||
if (camera) {
|
||||
await page.evaluate((spot) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 600,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-80 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, camera);
|
||||
await wait(4000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, spot) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 600,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-80 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
camera,
|
||||
);
|
||||
await whenSettled(page);
|
||||
const spot = await onScreen(['cctv']);
|
||||
if (spot) return { ...spot, kind: 'cctv', layer: 'CCTV camera' };
|
||||
}
|
||||
@@ -1021,20 +1002,23 @@ async function findClickableTarget() {
|
||||
})
|
||||
.catch(() => null);
|
||||
if (!site) return null;
|
||||
await page.evaluate((spot) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 2500,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, site);
|
||||
await wait(4000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, spot) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 2500,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
site,
|
||||
);
|
||||
await whenSettled(page);
|
||||
const spot = await onScreen([
|
||||
'local-datacenters-tips',
|
||||
'local-datacenters-pins',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"metrics": {
|
||||
"ownLaunch": 99,
|
||||
"fixedSleeps": 86,
|
||||
"loadingScreenReadiness": 1,
|
||||
"cesiumOnlyReads": 76,
|
||||
"ownWaitHelpers": 19
|
||||
"ownLaunch": 8,
|
||||
"fixedSleeps": 69,
|
||||
"loadingScreenReadiness": 0,
|
||||
"cesiumOnlyReads": 47,
|
||||
"ownWaitHelpers": 16
|
||||
}
|
||||
}
|
||||
+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();
|
||||
|
||||
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -52,8 +53,8 @@ export function frameStats(frames) {
|
||||
export async function installFrameProbe(page) {
|
||||
await page.evaluate(() => {
|
||||
if (window.__gevFrameProbe) return;
|
||||
const { viewer, engine } = window.__godsEyeView;
|
||||
const events = viewer?.scene ?? engine.frames;
|
||||
const { engine } = window.__godsEyeView;
|
||||
const events = engine.frames;
|
||||
const probe = { frames: [], started: 0 };
|
||||
window.__gevFrameProbe = probe;
|
||||
let last = performance.now();
|
||||
@@ -102,58 +103,25 @@ export function pageNow(page) {
|
||||
* @param {number} seconds Flight duration; 0 jumps.
|
||||
*/
|
||||
export async function moveCamera(page, view, seconds) {
|
||||
if (
|
||||
await page.evaluate(() => window.__godsEyeView.engine?.kind === 'navara')
|
||||
) {
|
||||
await page.evaluate(
|
||||
async (v, duration) => {
|
||||
const geo = await import('/src/geo/index.js');
|
||||
const camera = window.__godsEyeView.engine.camera;
|
||||
camera.cancelFlight();
|
||||
await new Promise((resolve) =>
|
||||
camera.flyTo({
|
||||
destination: geo.cartesian3.fromDegrees(v.lon, v.lat, v.height),
|
||||
orientation: {
|
||||
heading: ((v.heading ?? 0) * Math.PI) / 180,
|
||||
pitch: ((v.pitch ?? -45) * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
duration,
|
||||
maximumHeight: Math.max(v.height, camera.height()),
|
||||
complete: resolve,
|
||||
cancel: resolve,
|
||||
}),
|
||||
);
|
||||
},
|
||||
view,
|
||||
seconds,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await page.evaluate(
|
||||
(v, duration) =>
|
||||
new Promise((resolve) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
const C = viewer.camera;
|
||||
C.cancelFlight();
|
||||
C.flyTo({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
latitude: (v.lat * Math.PI) / 180,
|
||||
longitude: (v.lon * Math.PI) / 180,
|
||||
height: v.height,
|
||||
}),
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (geo, v, duration) => {
|
||||
const camera = window.__godsEyeView.engine.camera;
|
||||
camera.cancelFlight();
|
||||
await new Promise((resolve) =>
|
||||
camera.flyTo({
|
||||
destination: geo.cartesian3.fromDegrees(v.lon, v.lat, v.height),
|
||||
orientation: {
|
||||
heading: ((v.heading ?? 0) * Math.PI) / 180,
|
||||
pitch: ((v.pitch ?? -45) * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
duration,
|
||||
// Straight, eased motion like a drag or wheel zoom, not an arc.
|
||||
maximumHeight: Math.max(v.height, C.positionCartographic.height),
|
||||
complete: resolve,
|
||||
cancel: resolve,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
view,
|
||||
seconds,
|
||||
);
|
||||
|
||||
+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);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* Real-browser proof for manager-owned periodic work and document visibility.
|
||||
* Uses the app's development-only layer registration seam; no provider keys.
|
||||
* Run: node scripts/qa-layer-visibility.mjs --url http://localhost:4173
|
||||
* Hiding uses a real background tab, never a patched document.hidden getter.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -17,8 +18,7 @@ const url = new URL(
|
||||
'http://localhost:4173',
|
||||
);
|
||||
url.searchParams.set('welcome', '0');
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: chromeExecutablePath(),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
@@ -39,6 +39,7 @@ try {
|
||||
await page.waitForFunction(
|
||||
() => window.__gevQaRegisterLayer && window.__godsEyeView?.dataManager,
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const manager = window.__godsEyeView.dataManager;
|
||||
window.__qaVisibilityUpdates = 0;
|
||||
@@ -120,5 +121,5 @@ try {
|
||||
);
|
||||
console.log('RESULT: 7 passed');
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
/** Location UI acceptance; controlled search results isolate UI races from geocoder availability. */
|
||||
import { whenSettled, whenTilesLoaded } 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: [
|
||||
'--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,11 +25,10 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.styleManager?._locationControls &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__godsEyeView?.styleManager?._locationControls,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.click('#location-bar-toggle');
|
||||
await page.click('[data-pin-target="location-bar"]');
|
||||
const cityId = await page.$eval(
|
||||
@@ -114,10 +112,7 @@ try {
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => !window.__godsEyeView.viewer.camera._currentFlight,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
check(
|
||||
'native geo URI search shows the decimal coordinate readout',
|
||||
await page.evaluate(() => {
|
||||
@@ -135,7 +130,7 @@ try {
|
||||
check(
|
||||
'geo URI navigation settles near the requested coordinate',
|
||||
await page.evaluate(() => {
|
||||
const position = window.__godsEyeView.viewer.camera.positionCartographic;
|
||||
const position = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
const degrees = 180 / Math.PI;
|
||||
return (
|
||||
Math.abs(position.latitude * degrees - 39.7392) < 0.05 &&
|
||||
@@ -252,36 +247,17 @@ try {
|
||||
document.getElementById('poi-row').classList.contains('expanded'),
|
||||
);
|
||||
// Finish the camera's existing city flight before inspecting both layouts.
|
||||
await page.waitForFunction(
|
||||
() => !window.__godsEyeView.viewer.camera._currentFlight,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
),
|
||||
);
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
for (let index = 0; index < scene.primitives.length; index++) {
|
||||
const primitive = scene.primitives.get(index);
|
||||
if (
|
||||
typeof primitive.tilesLoaded === 'boolean' &&
|
||||
!primitive.tilesLoaded
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const settled = await whenTilesLoaded(page, { timeoutMs: 60000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
check('visible city tiles settle before screenshots', settled);
|
||||
fs.mkdirSync('qa-shots/location-controls', { recursive: true });
|
||||
await page.screenshot({ path: 'qa-shots/location-controls/desktop.png' });
|
||||
@@ -301,7 +277,8 @@ try {
|
||||
if (!pending) return false;
|
||||
await pending;
|
||||
return (
|
||||
!ui.orbitController.active && !window.__godsEyeView.viewer.trackedEntity
|
||||
!ui.orbitController.active &&
|
||||
!window.__godsEyeView.engine.camera.following()
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -329,7 +306,7 @@ try {
|
||||
);
|
||||
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;
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Browser acceptance for Map Source control lifecycle and truthful selection. */
|
||||
import { whenSettled, whenTilesLoaded } 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: [
|
||||
'--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();
|
||||
@@ -25,17 +25,7 @@ try {
|
||||
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
|
||||
url.searchParams.set('welcome', '0');
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return (
|
||||
window.__godsEyeView?.styleManager?._mapSourceControls &&
|
||||
cover?.classList.contains('hidden') &&
|
||||
Number(getComputedStyle(cover).opacity) === 0
|
||||
);
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const controller = manager.mapStackController;
|
||||
@@ -132,19 +122,17 @@ try {
|
||||
});
|
||||
document.querySelector('.map-stack-chip.active')?.focus();
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.scene.tweens?.removeAll?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (-97.7431 * Math.PI) / 180,
|
||||
latitude: (30.2568 * Math.PI) / 180,
|
||||
height: 1200,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -0.85, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await page.evaluate(
|
||||
() =>
|
||||
@@ -152,26 +140,10 @@ try {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
),
|
||||
);
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let i = 0; i < primitives.length; i++) {
|
||||
const primitive = primitives.get(i);
|
||||
if (
|
||||
typeof primitive.tilesLoaded === 'boolean' &&
|
||||
!primitive.tilesLoaded
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const settled = await whenTilesLoaded(page, { timeoutMs: 60_000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
check('visible tile content settles before visual captures', settled);
|
||||
fs.mkdirSync('qa-shots/map-source-controls', { recursive: true });
|
||||
await page.screenshot({ path: 'qa-shots/map-source-controls/desktop.png' });
|
||||
@@ -206,7 +178,7 @@ try {
|
||||
check('no uncaught browser errors', errors.length === 0);
|
||||
if (errors.length) console.log(JSON.stringify(errors));
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/** Focused browser proof for the responsive, accessible Map Source tray. */
|
||||
import { whenSettled, whenUiSettled } 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';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const shotsDir = process.env.QA_SHOTS_DIR || path.join(repoRoot, 'qa-shots', 'map-source-tray');
|
||||
@@ -18,26 +19,12 @@ const headful = process.argv.includes('--headful');
|
||||
// branch.
|
||||
const forceKeyless = process.argv.includes('--keyless')
|
||||
|| process.env.QA_MAP_SOURCE_TRAY_KEYLESS === '1';
|
||||
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH
|
||||
|| await puppeteer.executablePath().catch(() => null);
|
||||
|
||||
if (!executablePath || !fs.existsSync(executablePath)) {
|
||||
throw new Error('Puppeteer Chrome for Testing is unavailable');
|
||||
}
|
||||
fs.mkdirSync(shotsDir, { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: headful ? false : 'new',
|
||||
executablePath,
|
||||
// Metal is a macOS-only ANGLE backend: passing it on Windows or Linux makes
|
||||
// WebGL initialization fail outright, so Cesium never constructs and this
|
||||
// harness dies at the boot wait before a single assertion runs.
|
||||
args: [
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
'--no-sandbox',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [...(process.platform === 'darwin' ? ['--enable-gpu'] : []), '--no-sandbox'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const failures = [];
|
||||
@@ -154,7 +141,7 @@ const pressTabs = async (count, backwards = false) => {
|
||||
if (backwards) await page.keyboard.up('Shift');
|
||||
}
|
||||
// The row scrolls smoothly when native Tab brings an edge button into view.
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
};
|
||||
// Phones (≤720px) present the dock's Presets inline in the More tab's sheet.
|
||||
// It opens from the keyboard, as a keyboard user would, so the focus it moves
|
||||
@@ -218,7 +205,7 @@ const tabTo = async (selector, { backwards = false, limit = 160 } = {}) => {
|
||||
for (let step = 1; step <= limit; step += 1) {
|
||||
await page.keyboard.press('Tab');
|
||||
if (await page.evaluate((match) => document.activeElement?.matches(match), selector)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
return { reached: true, steps: step };
|
||||
}
|
||||
}
|
||||
@@ -304,10 +291,7 @@ try {
|
||||
// tray assertion into a mission or voice action in a pristine browser.
|
||||
await page.goto(`${appUrl}/?welcome=0`, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
|
||||
const presentation = await page.evaluate(() => ({
|
||||
ids: [...document.querySelectorAll('.map-stack-chip')].map((chip) => chip.dataset.stackId),
|
||||
@@ -1103,9 +1087,9 @@ try {
|
||||
const clickTileThenLeave = async (stackId) => {
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager
|
||||
.setPanelCollapsed('control-panel', false, { explicit: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 240));
|
||||
await whenUiSettled(page);
|
||||
await page.click(`[data-stack-id="${stackId}"]`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
await whenUiSettled(page);
|
||||
const afterClick = await readTrayState();
|
||||
await page.mouse.move(20, 20); // leave the dock entirely → pointerleave
|
||||
// Past the 420 ms unpinned close delay with room for a slow frame.
|
||||
@@ -1121,7 +1105,7 @@ try {
|
||||
await setControlPanelPinned(false);
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager
|
||||
.setPanelCollapsed('control-panel', true, { explicit: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await whenUiSettled(page);
|
||||
await page.focus('#control-panel-toggle');
|
||||
await page.keyboard.press('Enter'); // opens and hands focus to the active tile
|
||||
const selectedForHold = await page.evaluate(() => window.__godsEyeView.styleManager.mapStackController.getActiveId());
|
||||
@@ -1136,7 +1120,7 @@ try {
|
||||
await page.keyboard.press('Tab');
|
||||
if (activeIsLastTile) await page.keyboard.up('Shift');
|
||||
await page.keyboard.press('Enter'); // activate it from the keyboard
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await whenUiSettled(page);
|
||||
const keyboardAfterActivate = await page.evaluate(() => ({
|
||||
focusedStack: document.activeElement?.dataset?.stackId || null,
|
||||
isChip: !!document.activeElement?.classList?.contains('map-stack-chip'),
|
||||
@@ -1148,7 +1132,7 @@ try {
|
||||
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
||||
});
|
||||
await page.mouse.move(chipPoint.x, chipPoint.y);
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
await whenUiSettled(page);
|
||||
await page.mouse.move(20, 20);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const keyboardHold = await readTrayState();
|
||||
@@ -1179,7 +1163,7 @@ try {
|
||||
await page.mouse.move(pinApproach.x, pinApproach.y);
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager
|
||||
.setPanelCollapsed('control-panel', false, { explicit: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 240));
|
||||
await whenUiSettled(page);
|
||||
check(
|
||||
'a tile click does not exempt the unpinned tray from mouse-away auto-dismiss',
|
||||
dismissAfterTileClick.afterClick.collapsed === false
|
||||
@@ -1205,7 +1189,7 @@ try {
|
||||
await checkResponsiveStyleFocus(1000);
|
||||
|
||||
await page.setViewport({ width: 620, height: 900, deviceScaleFactor: 1 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 180));
|
||||
await whenUiSettled(page);
|
||||
await waitForShellSettled();
|
||||
await openPhoneMoreSheet();
|
||||
const at620 = await trayMetrics();
|
||||
@@ -1222,7 +1206,7 @@ try {
|
||||
await checkResponsiveStyleFocus(620);
|
||||
|
||||
await page.setViewport({ width: 480, height: 900, deviceScaleFactor: 1 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 180));
|
||||
await whenUiSettled(page);
|
||||
const at480 = await trayMetrics();
|
||||
const allRectsInside = at480.chips.every((chip) => (
|
||||
chip.left >= 0 && chip.right <= at480.viewport.width
|
||||
@@ -1294,10 +1278,7 @@ try {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
}
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.styleManager.mapStackController.getState()?.status !== 'switching',
|
||||
@@ -1429,7 +1410,7 @@ try {
|
||||
await page.keyboard.press('Enter');
|
||||
await page.focus('#control-panel-toggle');
|
||||
await page.keyboard.press('Enter');
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await whenUiSettled(page);
|
||||
const pinnedSibling = await locationState();
|
||||
check('Location: pinned tray survives sibling opening',
|
||||
pinnedSibling.pinned && pinnedSibling.locationExpanded === 'true'
|
||||
@@ -1449,14 +1430,14 @@ try {
|
||||
const searchTab = '#mobile-tab-bar [data-mobile-sheet="search"]';
|
||||
await page.focus(searchTab);
|
||||
await page.keyboard.press('Enter');
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
const field = await focusMetrics('#location-search');
|
||||
check(`Search: ${width} px keyboard opens the sheet on the location field`,
|
||||
field.focusedId === 'location-search' && field.visible && field.insideViewport,
|
||||
JSON.stringify(field));
|
||||
await page.screenshot({ path: path.join(shotsDir, `${width}-search-open.png`) });
|
||||
await page.keyboard.press('Escape');
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
const tab = await focusMetrics(searchTab);
|
||||
const closedSheet = await page.evaluate(() => !document.body.dataset.mobileSheet);
|
||||
check(`Search: ${width} px Escape closes the sheet with a visible ring on its tab`,
|
||||
@@ -1747,7 +1728,7 @@ try {
|
||||
|
||||
check('no new page or console errors', consoleErrors.length === 0, consoleErrors.join(' | '));
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Named military areas: continent performance, close identities and a recorded zoom. */
|
||||
import { whenSettled, whenFramesDrawn } 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 {
|
||||
checkInstallationPan,
|
||||
checkInstallationSelection,
|
||||
@@ -34,14 +36,10 @@ const result = {
|
||||
video: null,
|
||||
};
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 180000,
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
args: ['--disable-background-timer-throttling', '--disable-renderer-backgrounding'],
|
||||
});
|
||||
let recorder;
|
||||
try {
|
||||
@@ -69,10 +67,10 @@ try {
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
window.__godsEyeView?.dataManager,
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -92,11 +90,10 @@ try {
|
||||
for (const [id] of g.dataManager.layers)
|
||||
await g.dataManager.setEnabled(id, false);
|
||||
g.styleManager.setDetection({ enabled: false });
|
||||
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
assert.equal(result.packs.length, 0, 'names pack stays unloaded at startup');
|
||||
result.renderer = await page.evaluate(() => {
|
||||
const gl = window.__godsEyeView.viewer.scene.context._gl;
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return ext
|
||||
? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
|
||||
@@ -111,7 +108,7 @@ try {
|
||||
.module.getStats(),
|
||||
);
|
||||
const settle = async () => {
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const s = window.__godsEyeView.dataManager.layers
|
||||
@@ -121,7 +118,7 @@ try {
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await sleep(1200);
|
||||
await whenFramesDrawn(page);
|
||||
};
|
||||
const us = { lon: -100, lat: 39, height: 6500000, pitch: -90 };
|
||||
await moveCamera(page, us, 0);
|
||||
@@ -213,7 +210,7 @@ try {
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.setCleanView(true),
|
||||
);
|
||||
await sleep(300);
|
||||
await whenFramesDrawn(page);
|
||||
result.cleanUi = await page.evaluate(async () => {
|
||||
const host = (await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
|
||||
const root = document.getElementById('world-overlay-root');
|
||||
@@ -278,7 +275,7 @@ try {
|
||||
const screen = new C.Cartesian2();
|
||||
window.__installationFrames = [];
|
||||
window.__stopInstallationFrames =
|
||||
g.viewer.scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
const stats = layer.getStats();
|
||||
const markers = [],
|
||||
labels = [],
|
||||
@@ -286,11 +283,7 @@ try {
|
||||
layer.visitNamedMarkers((id, point) => {
|
||||
const rect = getPaintRect('military-installations', id);
|
||||
if (rect) rects.push(rect);
|
||||
const p = C.SceneTransforms.worldToWindowCoordinates(
|
||||
g.viewer.scene,
|
||||
point.position,
|
||||
screen,
|
||||
);
|
||||
const p = window.__godsEyeView.engine.projector.worldToWindow(point.position, screen);
|
||||
if (
|
||||
point.show &&
|
||||
p &&
|
||||
@@ -316,7 +309,7 @@ try {
|
||||
)
|
||||
overlaps++;
|
||||
}
|
||||
const altitude = g.viewer.camera.positionCartographic.height;
|
||||
const altitude = window.__godsEyeView.engine.camera.pose().cartographic.height;
|
||||
window.__installationFrames.push({
|
||||
t: performance.now(),
|
||||
altitude,
|
||||
@@ -331,7 +324,7 @@ try {
|
||||
});
|
||||
});
|
||||
const clipStart = await pageNow(page);
|
||||
await sleep(1200);
|
||||
await whenFramesDrawn(page);
|
||||
for (const view of [
|
||||
{ lon: -116, lat: 35, height: 1500000, pitch: -85 },
|
||||
{ lon: -117.2, lat: 33, height: 200000, pitch: -80 },
|
||||
@@ -381,11 +374,11 @@ try {
|
||||
for (const [key, expected, lon, lat, height] of sites) {
|
||||
await moveCamera(page, { lon, lat, height, pitch: -80 }, 1.5);
|
||||
await settle();
|
||||
const snapshot = await page.evaluate(
|
||||
(lat, lon) => {
|
||||
const snapshot = await evaluateWithGeo(page,
|
||||
async (qaGeo, lat, lon) => {
|
||||
const g = window.__godsEyeView;
|
||||
const layer = g.dataManager.layers.get('military-installations').module;
|
||||
const position = g.viewer.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
const position = qaGeo.cartographic.toCartesian(
|
||||
{
|
||||
longitude: (lon * Math.PI) / 180,
|
||||
latitude: (lat * Math.PI) / 180,
|
||||
@@ -466,7 +459,7 @@ try {
|
||||
);
|
||||
assert.match(result.cardText, /Fort Hood/);
|
||||
assert.match(result.cardText, /TA-25/);
|
||||
await sleep(1800);
|
||||
await whenFramesDrawn(page);
|
||||
await page.$eval(
|
||||
'#military-awareness-panel .military-awareness-names',
|
||||
(el) => {
|
||||
@@ -481,5 +474,5 @@ try {
|
||||
if (recorder)
|
||||
result.video = await recorder.stop().catch((e) => ({ error: e.message }));
|
||||
await fs.writeFile(`${dir}/result.json`, JSON.stringify(result, null, 2));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
// Composed-app media policy/lifecycle checks. Pinokio UA is a policy fixture,
|
||||
// not native-host acceptance; the YouTube SDK clock below is deterministic.
|
||||
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';
|
||||
|
||||
const output = path.resolve(
|
||||
process.env.QA_OUTPUT_DIR || 'qa-shots/nepal-media',
|
||||
);
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: process.env.QA_HEADFUL !== '1',
|
||||
args: process.env.QA_HEADFUL === '1' ? ['--use-angle=metal'] : [],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: process.env.QA_HEADFUL === '1' ? [] : [],
|
||||
});
|
||||
let assertions = 0;
|
||||
const check = (name, value) => {
|
||||
@@ -84,12 +85,10 @@ try {
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector &&
|
||||
document.querySelector('#loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
const result = await page.evaluate(async (pinokio) => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
const scene = d._project.scenes.find((s) => /Nepal/.test(s.title));
|
||||
@@ -200,5 +199,5 @@ try {
|
||||
}
|
||||
console.log(`PASS: ${assertions} assertions`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -17,10 +17,11 @@
|
||||
* projection must fit one frame; Traffic does no outline projection and the
|
||||
* warm ask reuses it. Traffic activation Long Tasks are reported separately.
|
||||
*/
|
||||
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 path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const valueAfter = (flag, fallback) => {
|
||||
@@ -155,19 +156,13 @@ function workEvidence(entries, label) {
|
||||
|
||||
await fs.mkdir(path.dirname(OUTPUT), { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: args.includes('--headful') ? false : 'new',
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 240_000,
|
||||
...(process.env.PUPPETEER_EXECUTABLE_PATH
|
||||
? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH }
|
||||
: {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-gl=angle',
|
||||
...(args.includes('--headful')
|
||||
? []
|
||||
: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader']),
|
||||
...(args.includes('--headful') ? [] : ['--enable-unsafe-swiftshader']),
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -251,15 +246,15 @@ async function settleOutline(page, label) {
|
||||
}
|
||||
if (!annotation || annotation.pendingOutline)
|
||||
throw new Error(`${label}: outline did not settle`);
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
await new Promise((resolve) => {
|
||||
const timer = setTimeout(resolve, 1500);
|
||||
const remove = scene.postRender.addEventListener(() => {
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
resolve();
|
||||
});
|
||||
scene.requestRender();
|
||||
const remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
resolve();
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await new Promise((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
@@ -414,13 +409,13 @@ try {
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.viewer &&
|
||||
window.__godsEyeView?.engine &&
|
||||
window.__godsEyeView?.dataManager &&
|
||||
window.__gevVoiceCommands?.runner &&
|
||||
window.__gevAnnotations &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
window.__gevAnnotations,
|
||||
{ timeout: 120_000, polling: 250 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -428,10 +423,10 @@ try {
|
||||
window.__godsEyeView.styleManager.setDetection({ enabled: false }),
|
||||
);
|
||||
await page.evaluate(async (target) => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
await dataManager.setEnabled('traffic', false);
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: window.__CESIUM__.Cartesian3.fromDegrees(
|
||||
target.longitude,
|
||||
target.latitude,
|
||||
@@ -443,11 +438,11 @@ try {
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
window.__gevAnnotations.clear();
|
||||
performance.clearMeasures();
|
||||
}, TARGET);
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
|
||||
if (run === 0) {
|
||||
phase = 'overlap';
|
||||
@@ -469,7 +464,7 @@ try {
|
||||
await sleep(100);
|
||||
await heldTargetRequest.continue();
|
||||
const overlap = await settleOutline(page, 'QA Capitol overlap');
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
const trafficStats = await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.layers
|
||||
.get('traffic')
|
||||
@@ -538,7 +533,7 @@ try {
|
||||
},
|
||||
{ timeout: 180_000, polling: 250 },
|
||||
);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
const trafficMeasures = relevantMeasures(
|
||||
await page.evaluate(() =>
|
||||
performance.getEntriesByType('measure').map((entry) => ({
|
||||
@@ -582,7 +577,7 @@ try {
|
||||
|
||||
phase = 'outline-cold';
|
||||
const cold = await driveOutline(page, `QA Capitol cold ${run}`);
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
cold.measures = relevantMeasures(cold.measures);
|
||||
cold.work = workEvidence(cold.measures, 'cold');
|
||||
cold.network = network.filter(
|
||||
@@ -596,7 +591,7 @@ try {
|
||||
await page.evaluate(() => window.__gevAnnotations.clear());
|
||||
phase = 'outline-warm';
|
||||
const warm = await driveOutline(page, `QA Capitol warm ${run}`);
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
warm.measures = relevantMeasures(warm.measures);
|
||||
warm.work = workEvidence(warm.measures, 'warm');
|
||||
warm.network = network.filter(
|
||||
@@ -674,7 +669,7 @@ try {
|
||||
assert.equal(warmDecodes.length, 0, 'decoded-cache hit performs no decode');
|
||||
|
||||
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)
|
||||
@@ -849,5 +844,5 @@ try {
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await fs.writeFile(OUTPUT, `${JSON.stringify(report, null, 2)}\n`);
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
// Cesium-only probes: native entity, DataSource and primitive counts independently audit Cesium ownership.
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* Phase 0 world-overlay baseline harness.
|
||||
*
|
||||
@@ -25,13 +28,13 @@
|
||||
* and primitives (none of them a layer's own any more).
|
||||
*/
|
||||
|
||||
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 { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
|
||||
const CHROME_EXECUTABLE = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const CHROME_EXECUTABLE = chromeExecutablePath();
|
||||
const DEFAULT_URL = 'http://localhost:4176';
|
||||
const VIEWPORT = Object.freeze({ width: 1440, height: 900 });
|
||||
const SAMPLE_MS = 5_000;
|
||||
@@ -366,20 +369,19 @@ async function waitForApp(page) {
|
||||
() => Boolean(window.__godsEyeView?.engine && window.__godsEyeView?.dataManager),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
const { engine, viewer } = window.__godsEyeView;
|
||||
const { engine } = window.__godsEyeView;
|
||||
try { engine.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
try { viewer?.scene.tweens?.removeAll?.(); } catch { /* no tween collection */ }
|
||||
});
|
||||
await sleep(1_250);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
|
||||
async function setCamera(page, camera) {
|
||||
if (!camera) return;
|
||||
await page.evaluate(([destination, heading, pitch]) => {
|
||||
const { engine, viewer } = window.__godsEyeView;
|
||||
const { engine } = window.__godsEyeView;
|
||||
engine.camera.cancelFlight();
|
||||
viewer?.scene.tweens?.removeAll?.();
|
||||
engine.camera.setView({
|
||||
destination,
|
||||
orientation: { heading, pitch, roll: 0 },
|
||||
@@ -587,7 +589,7 @@ async function prepareCctv(page, heightM) {
|
||||
}, heightM);
|
||||
if (!camera) return 'CCTV catalog did not expose a geolocated camera';
|
||||
await setCamera(page, camera);
|
||||
await sleep(7_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -600,14 +602,14 @@ async function prepareFirms(page) {
|
||||
});
|
||||
if (!result.fire) return `FIRMS unavailable (${result.stats.error || 'no detections'})`;
|
||||
await setCamera(page, [result.fire.longitude, result.fire.latitude, 60_000, 0, -1.45]);
|
||||
await sleep(4_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function prepareVessels(page) {
|
||||
const state = await readLayerState(page, 'ais-live-vessels');
|
||||
if (!(state?.stats?.count > 0)) return `AIS unavailable (${state?.stats?.error || 'no live vessels'})`;
|
||||
await sleep(2_500);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -621,7 +623,7 @@ async function prepareDetection(page, densityPct) {
|
||||
densityPct: density,
|
||||
}), densityPct);
|
||||
if (!control?.ok) return `Detection control failed (${control?.error || 'unknown error'})`;
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -650,7 +652,7 @@ async function prepareTrackedFlight(page, cockpit) {
|
||||
} catch {
|
||||
return 'Civil tracking did not create a tracked entity';
|
||||
}
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
if (!cockpit) return null;
|
||||
const entryAvailable = await page.evaluate(() => {
|
||||
const button = document.getElementById('cockpit-entry');
|
||||
@@ -664,7 +666,7 @@ async function prepareTrackedFlight(page, cockpit) {
|
||||
} catch {
|
||||
return 'Cockpit entry control did not enter cockpit mode';
|
||||
}
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -683,7 +685,7 @@ async function prepareMission(page) {
|
||||
} catch {
|
||||
return 'Mission selection did not reach the layer probe';
|
||||
}
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -703,7 +705,7 @@ async function prepareScene(page, scene) {
|
||||
if (!skipReason && scene.detectionDensity) skipReason = await prepareDetection(page, scene.detectionDensity);
|
||||
if (!skipReason && scene.trackedFlight) skipReason = await prepareTrackedFlight(page, scene.cockpit);
|
||||
if (!skipReason && scene.missionSelected) skipReason = await prepareMission(page);
|
||||
await sleep(DEFAULT_SETTLE_MS);
|
||||
await whenFramesDrawn(page);
|
||||
return { layerActivations, skipReason };
|
||||
}
|
||||
|
||||
@@ -868,7 +870,7 @@ async function runScene(browser, scene, shotsDir) {
|
||||
const motion = await samplePhase(page, true);
|
||||
const afterMotionShot = await captureShot(page, shotsDir, scene.id, 'after-motion');
|
||||
if (afterMotionShot) result.screenshots.push(afterMotionShot);
|
||||
await sleep(BETWEEN_PHASE_SETTLE_MS);
|
||||
await whenFramesDrawn(page);
|
||||
const rest = await samplePhase(page, false);
|
||||
result.samples = { motion, rest };
|
||||
result.entityInventoryAfter = await readEntityInventory(page);
|
||||
@@ -904,9 +906,6 @@ function unavailableRenderer(renderer) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(CHROME_EXECUTABLE)) {
|
||||
throw new Error(`Required Chrome executable not found: ${CHROME_EXECUTABLE}`);
|
||||
}
|
||||
if (!Number.isFinite(DPR) || DPR <= 0) throw new Error(`Invalid --dpr value: ${DPR}`);
|
||||
const scenes = selectedScenes();
|
||||
if (DIST_DIR && (!fs.existsSync(DIST_DIR) || !fs.statSync(DIST_DIR).isDirectory())) {
|
||||
@@ -917,18 +916,9 @@ async function main() {
|
||||
if (!response.ok) throw new Error(`Dev server unavailable at ${APP_URL}: ${response.status || response.statusText}`);
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROME_EXECUTABLE,
|
||||
headless: HEADFUL ? false : 'new',
|
||||
args: [
|
||||
...(process.env.QA_ANGLE ? ['--use-gl=angle', `--use-angle=${process.env.QA_ANGLE}`] : HARDWARE_GPU ? [] : ['--enable-unsafe-swiftshader', '--use-gl=swiftshader']),
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
`--window-size=${VIEWPORT.width},${VIEWPORT.height}`,
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [...(process.env.QA_ANGLE ? [] : HARDWARE_GPU ? [] : ['--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 run = { generatedAt: new Date().toISOString(), context: null, scenes: [] };
|
||||
@@ -974,13 +964,9 @@ async function main() {
|
||||
webglRenderer: browserContext.webglRenderer,
|
||||
},
|
||||
viewport: { ...VIEWPORT, dpr: browserContext.dpr },
|
||||
gpuMode: process.env.QA_ANGLE || (HARDWARE_GPU ? 'hardware' : 'swiftshader'),
|
||||
gpuMode: process.env.QA_ANGLE || 'metal',
|
||||
chromeExecutable: CHROME_EXECUTABLE,
|
||||
chromeArgs: process.env.QA_ANGLE
|
||||
? ['--use-gl=angle', `--use-angle=${process.env.QA_ANGLE}`, '--no-sandbox']
|
||||
: HARDWARE_GPU
|
||||
? ['--no-sandbox']
|
||||
: ['--enable-unsafe-swiftshader', '--use-gl=swiftshader', '--no-sandbox'],
|
||||
chromeArgs: browser.process()?.spawnargs.slice(1),
|
||||
sampleMs: SAMPLE_MS,
|
||||
captureMode: DIST_DIR ? `static-dist:${path.resolve(DIST_DIR)}` : 'dev-server',
|
||||
};
|
||||
@@ -992,7 +978,7 @@ async function main() {
|
||||
printScene(result);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
run.summary = {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native primitives and globe instrumentation isolate Cesium paint and offload cost.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* qa-overpass-offload.mjs — keyless/keyed source replacement acceptance gate.
|
||||
*
|
||||
@@ -25,10 +27,11 @@
|
||||
* Screenshots and a JSON result go to qa-shots/ (gitignored). Exits nonzero on
|
||||
* a failed assertion. Works against keyed or keyless dev servers.
|
||||
*/
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { tilesForBounds } from '../src/data/tomtomTiles.js';
|
||||
import { trafficDetailBounds } from '../src/layers/traffic/source.js';
|
||||
|
||||
@@ -55,19 +58,13 @@ const profile = args.includes('--profile');
|
||||
const coverage = args.includes('--coverage');
|
||||
const shots = path.resolve('qa-shots');
|
||||
await fs.mkdir(shots, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !args.includes('--headful'),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
...(process.env.PUPPETEER_EXECUTABLE_PATH
|
||||
? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH }
|
||||
: {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-gl=angle',
|
||||
...(args.includes('--software-gl')
|
||||
? ['--use-angle=swiftshader', '--enable-unsafe-swiftshader']
|
||||
: []),
|
||||
...(args.includes('--software-gl') ? ['--enable-unsafe-swiftshader'] : []),
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -346,14 +343,10 @@ try {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60_000, polling: 500 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000, polling: 250 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
// Startup restoration owns its camera flight until the loading cover is gone.
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
@@ -362,7 +355,7 @@ try {
|
||||
window.__godsEyeView.styleManager.setDetection({ enabled: false }),
|
||||
);
|
||||
result.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)
|
||||
@@ -374,13 +367,13 @@ try {
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
async function fly(lat, lon, height = 2000, heading = 0, pitch = -75) {
|
||||
await page.evaluate(
|
||||
async (view) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, view) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
await new Promise((resolve) =>
|
||||
viewer.camera.flyTo({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
height: view.height,
|
||||
@@ -400,22 +393,7 @@ try {
|
||||
);
|
||||
}
|
||||
async function settleTiles() {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
if (scene.globe.show) return scene.globe.tilesLoaded;
|
||||
let found = false;
|
||||
for (let i = 0; i < scene.primitives.length; i++) {
|
||||
const primitive = scene.primitives.get(i);
|
||||
if (!primitive.show || typeof primitive.tilesLoaded !== 'boolean')
|
||||
continue;
|
||||
found = true;
|
||||
if (!primitive.tilesLoaded) return false;
|
||||
}
|
||||
return found;
|
||||
},
|
||||
{ timeout: 90_000, polling: 500 },
|
||||
);
|
||||
await whenTilesLoaded(page, { timeoutMs: 90_000 });
|
||||
}
|
||||
async function shot(name) {
|
||||
await settleTiles();
|
||||
@@ -440,30 +418,35 @@ try {
|
||||
const milliseconds = await page.evaluate(
|
||||
(allowEmpty) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const start = performance.now();
|
||||
const cleanup = () => {
|
||||
remove();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
const remove = viewer.scene.postRender.addEventListener(() => {
|
||||
const stats = dataManager.layers.get('traffic').module.getStats();
|
||||
if (stats.count <= 0) {
|
||||
if (
|
||||
allowEmpty &&
|
||||
!stats.loading &&
|
||||
stats.lastUpdate &&
|
||||
performance.now() - start > 500
|
||||
) {
|
||||
const remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
() => {
|
||||
const stats = dataManager.layers
|
||||
.get('traffic')
|
||||
.module.getStats();
|
||||
if (stats.count <= 0) {
|
||||
if (
|
||||
allowEmpty &&
|
||||
!stats.loading &&
|
||||
stats.lastUpdate &&
|
||||
performance.now() - start > 500
|
||||
) {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
cleanup();
|
||||
resolve(elapsed);
|
||||
});
|
||||
resolve(elapsed);
|
||||
},
|
||||
);
|
||||
const timer = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error('No traffic dots within 180 seconds'));
|
||||
@@ -551,23 +534,24 @@ try {
|
||||
.map((p) => C.Cartesian3.clone(p.position)),
|
||||
);
|
||||
for (const position of positions) {
|
||||
const screen = C.SceneTransforms.worldToWindowCoordinates(
|
||||
scene,
|
||||
position,
|
||||
);
|
||||
const screen =
|
||||
window.__godsEyeView.engine.projector.worldToWindow(position);
|
||||
if (
|
||||
!screen ||
|
||||
screen.x < 0 ||
|
||||
screen.x >= scene.canvas.clientWidth ||
|
||||
screen.x >= window.__godsEyeView.engine.host.canvas.clientWidth ||
|
||||
screen.y < 0 ||
|
||||
screen.y >= scene.canvas.clientHeight
|
||||
screen.y >= window.__godsEyeView.engine.host.canvas.clientHeight
|
||||
)
|
||||
continue;
|
||||
inView++;
|
||||
const carto = C.Cartographic.fromCartesian(position);
|
||||
const height = scene.globe.show
|
||||
? scene.globe.getHeight(carto)
|
||||
: scene.sampleHeight(carto, collections);
|
||||
? window.__godsEyeView.engine.surface.terrainHeight(carto)
|
||||
: window.__godsEyeView.engine.surface.sampleHeight(
|
||||
carto,
|
||||
collections,
|
||||
);
|
||||
if (Number.isFinite(height) && Math.abs(height) <= 9000) {
|
||||
sampled++;
|
||||
const delta = carto.height - height;
|
||||
@@ -645,13 +629,13 @@ try {
|
||||
);
|
||||
view.layers = await waitForSources();
|
||||
view.actualCamera = await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const position = camera.positionCartographic;
|
||||
const position = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return {
|
||||
lat: (position.latitude * 180) / Math.PI,
|
||||
lon: (position.longitude * 180) / Math.PI,
|
||||
height: position.height,
|
||||
pitch: (camera.pitch * 180) / Math.PI,
|
||||
pitch:
|
||||
(window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
|
||||
};
|
||||
});
|
||||
assert.ok(
|
||||
@@ -736,14 +720,16 @@ try {
|
||||
surface.stats.mode === 'live' ? mode : 'osm',
|
||||
);
|
||||
const actualCamera = await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const p = camera.positionCartographic;
|
||||
const p = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return {
|
||||
lat: (p.latitude * 180) / Math.PI,
|
||||
lon: (p.longitude * 180) / Math.PI,
|
||||
height: p.height,
|
||||
pitch: (camera.pitch * 180) / Math.PI,
|
||||
heading: (camera.heading * 180) / Math.PI,
|
||||
pitch:
|
||||
(window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
|
||||
heading:
|
||||
(window.__godsEyeView.engine.camera.pose().heading * 180) /
|
||||
Math.PI,
|
||||
};
|
||||
});
|
||||
for (const [key, expected, tolerance] of [
|
||||
@@ -868,9 +854,7 @@ try {
|
||||
}
|
||||
} else if (pan) {
|
||||
const sessionBudget = 32; // <= 0.54% of the default 6,000/day upstream budget.
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
await page.evaluate(() => {});
|
||||
beginView('pan-start');
|
||||
await fly(30.2672, -97.7431, 450, 0, -35);
|
||||
await enableTrafficTimed('pan-start');
|
||||
@@ -992,9 +976,7 @@ try {
|
||||
// A continent jump can temporarily use the previous city's terrain height
|
||||
// for collision avoidance (São Paulo -> Austin raises a 450 m target).
|
||||
// Pin the prescribed camera fixtures so the revisit covers identical tiles.
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
await page.evaluate(() => {});
|
||||
for (const city of [
|
||||
['austin', 30.2672, -97.7431],
|
||||
['london', 51.5074, -0.1278],
|
||||
@@ -1043,7 +1025,7 @@ try {
|
||||
{ timeout: 120_000, polling: 500 },
|
||||
);
|
||||
const austin = await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const traffic = dataManager.layers.get('traffic').module.getStats();
|
||||
const alpr = dataManager.layers.get('alpr-cameras').module.getStats();
|
||||
// The camera badges: the layer's sprite mover sets (normal and
|
||||
@@ -1059,7 +1041,8 @@ try {
|
||||
alpr,
|
||||
cameraEntities,
|
||||
sourceLabel: traffic.loadingLabel,
|
||||
cameraHeight: viewer.camera.positionCartographic.height,
|
||||
cameraHeight:
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
};
|
||||
});
|
||||
result.traffic = austin.traffic;
|
||||
@@ -1219,14 +1202,10 @@ try {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document
|
||||
.getElementById('loading-screen')
|
||||
?.classList.contains('hidden'),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -1282,9 +1261,9 @@ try {
|
||||
{ timeout: 120_000, polling: 500 },
|
||||
before,
|
||||
);
|
||||
result.military = await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const center = viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
result.military = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const center = qaGeo.cartographic.toCartesian({
|
||||
latitude: (30.314 * Math.PI) / 180,
|
||||
longitude: (-97.763 * Math.PI) / 180,
|
||||
height: 0,
|
||||
@@ -1367,7 +1346,7 @@ try {
|
||||
priorFort,
|
||||
);
|
||||
result.fortCavazos = await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
// Drawn site discs (context handles, see Camp Mabry) plus the named
|
||||
// markers the stats count.
|
||||
let markers = 0,
|
||||
@@ -1533,5 +1512,5 @@ try {
|
||||
path.join(shots, 'overpass-offload-result.json'),
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
+156
-197
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native flight interception and entity sentinels verify Cesium camera ownership.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* Deterministic browser proof for the Radio companion layer.
|
||||
*
|
||||
@@ -11,10 +13,11 @@
|
||||
*
|
||||
* Run: node scripts/qa-radio.mjs --url http://localhost:4193
|
||||
*/
|
||||
import { whenSettled, whenFramesDrawn } 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';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { stations } from './fixtures/layers/radio.mjs';
|
||||
import { isExpectedPreviewSetupError } from './qa-browserEvidence.mjs';
|
||||
|
||||
@@ -36,15 +39,6 @@ const RADIO_URL = new URL(APP_URL);
|
||||
RADIO_URL.searchParams.set('welcome', '0');
|
||||
const HEADFUL = args.includes('--headful');
|
||||
|
||||
const chromeCandidates = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
].filter(Boolean);
|
||||
const chrome = chromeCandidates.find((candidate) => {
|
||||
try { return fs.existsSync(candidate); } catch { return false; }
|
||||
});
|
||||
|
||||
|
||||
const results = [];
|
||||
@@ -86,14 +80,13 @@ async function restoreRadioPanels(page, prior) {
|
||||
// not on wall-clock sleeps, particularly with software rendering.
|
||||
async function settleRadioFrames(page) {
|
||||
await page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
let remaining = 4;
|
||||
const timeout = setTimeout(() => { remove(); reject(new Error('Radio rendered-frame deadline exceeded')); }, 10_000);
|
||||
const remove = scene.postRender.addEventListener(() => {
|
||||
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
if (--remaining === 0) { remove(); clearTimeout(timeout); resolve(); }
|
||||
else requestAnimationFrame(() => scene.requestRender());
|
||||
else requestAnimationFrame(() => window.__godsEyeView.engine.render.request());
|
||||
});
|
||||
scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -105,17 +98,9 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
fs.mkdirSync(SHOTS_DIR, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(chrome ? { executablePath: chrome } : {}),
|
||||
args: [
|
||||
'--no-sandbox', '--disable-setuid-sandbox',
|
||||
...(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=1440,900',
|
||||
],
|
||||
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=1440,900'],
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -370,17 +355,15 @@ async function main() {
|
||||
|
||||
await page.goto(RADIO_URL.href, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, { timeout: 60_000 });
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager?._dataManager?.layers?.has('radio'), { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(() => (
|
||||
typeof window.__gevQaRegisterLayer === 'function'
|
||||
&& typeof window.__gevQaUnregisterLayer === 'function'
|
||||
));
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed('pp-toggles', true);
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed('cctv-panel', true);
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed('radio-panel', true);
|
||||
@@ -470,7 +453,7 @@ async function main() {
|
||||
const selectedBefore = module.getUIState().selected?.id || null;
|
||||
const playsBefore = window.__qaRadioPlayCalls.length;
|
||||
const camera = gev.viewer.camera;
|
||||
const originalFlyTo = camera.flyTo;
|
||||
const originalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
const flyToCalls = [];
|
||||
camera.flyTo = (options) => flyToCalls.push(options);
|
||||
const beganTuning = module.beginTuning();
|
||||
@@ -643,7 +626,7 @@ async function main() {
|
||||
const scroller = document.querySelector('#global-context-panel .global-context-panel-inner');
|
||||
const radio = document.getElementById('radio-panel');
|
||||
scroller.scrollTop = Math.max(0, radio.offsetTop - scroller.clientHeight + 70);
|
||||
const camera = gev.viewer.camera.positionWC;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().position;
|
||||
return {
|
||||
scrollTop: scroller.scrollTop,
|
||||
pageY: window.scrollY,
|
||||
@@ -716,7 +699,7 @@ async function main() {
|
||||
const viewport = scroller.getBoundingClientRect();
|
||||
const directory = document.querySelector('#radio-panel .radio-directory-row').getBoundingClientRect();
|
||||
const play = document.getElementById('radio-play-btn').getBoundingClientRect();
|
||||
const camera = gev.viewer.camera.positionWC;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().position;
|
||||
return {
|
||||
scrollTop: scroller.scrollTop,
|
||||
pageY: window.scrollY,
|
||||
@@ -762,23 +745,21 @@ async function main() {
|
||||
{ name: 'global', height: 24_200_000, limit: 16 },
|
||||
{ name: 'near', height: 500_000, limit: 32 },
|
||||
]) {
|
||||
await page.evaluate(({ lon, height }) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo, { lon, height }) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: lon * Math.PI / 180,
|
||||
latitude: 6 * Math.PI / 180,
|
||||
height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
}, { lon: spec.lon, height: view.height });
|
||||
await sleep(700);
|
||||
await whenFramesDrawn(page);
|
||||
await settleRadioFrames(page);
|
||||
const sample = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
@@ -806,11 +787,11 @@ async function main() {
|
||||
});
|
||||
};
|
||||
const timeout = setTimeout(finish, 500);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
finish();
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
});
|
||||
singletonViews.push({ ...spec, ...view, ...sample });
|
||||
@@ -1362,17 +1343,15 @@ async function main() {
|
||||
&& allPalette.music?.[1] > allPalette.music?.[2],
|
||||
JSON.stringify(allPalette),
|
||||
);
|
||||
const clusterBadge = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const clusterBadge = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
// Start from another view, so the clusters at the view below are a new
|
||||
// cohort the host must solve (the old check forced one by switching
|
||||
// Cesium's clustering off and on).
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: 68 * Math.PI / 180,
|
||||
latitude: 7 * Math.PI / 180,
|
||||
height: 24_200_000,
|
||||
@@ -1382,15 +1361,15 @@ async function main() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await drawn.frames(3);
|
||||
const baselineSolveRevision = getWorldOverlayDiagnostics().solveRevision;
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -98 * Math.PI / 180,
|
||||
latitude: 38 * Math.PI / 180,
|
||||
height: 18_000_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
// The layer clusters on screen itself now (render/display.js,
|
||||
// ../screenClusters.js) into the radio-clusters point set; the cluster
|
||||
@@ -1405,7 +1384,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
done({ rect: null, host: getWorldOverlayDiagnostics() });
|
||||
}, 150);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const host = getWorldOverlayDiagnostics();
|
||||
@@ -1421,7 +1400,7 @@ async function main() {
|
||||
host,
|
||||
});
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
}
|
||||
const position = typeof painted?.position === 'function' ? painted.position() : painted?.position;
|
||||
@@ -1451,30 +1430,27 @@ async function main() {
|
||||
&& clusterBadge?.rect?.h > 0,
|
||||
JSON.stringify(clusterBadge),
|
||||
);
|
||||
await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: 68 * Math.PI / 180,
|
||||
latitude: 7 * Math.PI / 180,
|
||||
height: 24_200_000,
|
||||
}),
|
||||
orientation: { heading: 15 * Math.PI / 180, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
});
|
||||
await settleRadioFrames(page);
|
||||
const highGlobalClusterLabels = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const highGlobalClusterLabels = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
const ellipsoid = qaGeo.ellipsoid.WGS84;
|
||||
const { getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const { distanceFade } = await import('/src/overlays/worldOverlayDraw.js');
|
||||
const source = radio.getOverlayDiagnostics();
|
||||
const cameraPosition = viewer.camera.positionWC;
|
||||
const cameraPosition = window.__godsEyeView.engine.camera.pose().position;
|
||||
const cameraRadius = Math.hypot(cameraPosition.x, cameraPosition.y, cameraPosition.z);
|
||||
const horizonDistance = Math.sqrt(Math.max(
|
||||
0,
|
||||
@@ -1486,8 +1462,8 @@ async function main() {
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
const { getPaintRect: getOverlayPaintRect } = window.__gevWorldOverlay;
|
||||
let clusterPoint = null;
|
||||
const width = viewer.canvas.clientWidth;
|
||||
const height = viewer.canvas.clientHeight;
|
||||
const width = window.__godsEyeView.engine.host.canvas.clientWidth;
|
||||
const height = window.__godsEyeView.engine.host.canvas.clientHeight;
|
||||
const anchors = source.clusterIds
|
||||
.map((entryId) => getOverlayPaintRect('radio', entryId)?.entry?.position)
|
||||
.map((position) => drawn.canvasPoint(typeof position === 'function' ? position() : position))
|
||||
@@ -1503,7 +1479,7 @@ async function main() {
|
||||
return {
|
||||
source,
|
||||
host: getWorldOverlayDiagnostics(),
|
||||
altitude: viewer.camera.positionCartographic.height,
|
||||
altitude: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
horizonDistance,
|
||||
horizonDistanceAlpha: distanceFade(horizonDistance, { maxDistance: 50_000_000 }),
|
||||
clusterPoint,
|
||||
@@ -1521,10 +1497,8 @@ async function main() {
|
||||
JSON.stringify(highGlobalClusterLabels),
|
||||
);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'high-global-all-labels.png') });
|
||||
const clusterContinuity = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const clusterContinuity = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const paintedClusters = () => new Promise((resolve) => {
|
||||
let removePostRender = null;
|
||||
@@ -1532,7 +1506,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
resolve({ clusters: [], fadingCount: -1, host: null, source: null });
|
||||
}, 500);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const source = radio.getOverlayDiagnostics();
|
||||
@@ -1553,20 +1527,20 @@ async function main() {
|
||||
source,
|
||||
});
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
const longitudes = [-25, -24, -23, -22, -23, -24, -25];
|
||||
const samples = [];
|
||||
for (const longitude of longitudes) {
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: longitude * Math.PI / 180,
|
||||
latitude: 38 * Math.PI / 180,
|
||||
height: 10_000_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
for (const delay of [50, 100, 200, 350]) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
samples.push({ longitude, delay, ...(await paintedClusters()) });
|
||||
@@ -1621,11 +1595,10 @@ async function main() {
|
||||
JSON.stringify(clusterContinuity),
|
||||
);
|
||||
const directoryRefreshContinuity = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const before = radio.getOverlayDiagnostics().clusterMemberships;
|
||||
await radio.update();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||
const after = radio.getOverlayDiagnostics().clusterMemberships;
|
||||
const beforeByMembership = new Map(before.map((entry) => [entry.membershipId, entry.entryId]));
|
||||
@@ -2451,7 +2424,7 @@ async function main() {
|
||||
await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
window.__qaFirstPlayFlyToCalls = [];
|
||||
window.__qaFirstPlayOriginalFlyTo = camera.flyTo;
|
||||
window.__qaFirstPlayOriginalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
camera.flyTo = (options) => window.__qaFirstPlayFlyToCalls.push(options);
|
||||
});
|
||||
const firstPlayPanels = await revealRadioControls(page, 'radio-play-btn');
|
||||
@@ -2496,11 +2469,10 @@ async function main() {
|
||||
check('first Play never moves the camera', firstPlay.flyToCalls === 0, `${firstPlay.flyToCalls} flyTo calls`);
|
||||
|
||||
await page.select('#radio-filter', 'news');
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -98 * Math.PI / 180,
|
||||
latitude: 38 * Math.PI / 180,
|
||||
height: 18_000_000,
|
||||
@@ -2508,7 +2480,7 @@ async function main() {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
});
|
||||
await sleep(700);
|
||||
await whenFramesDrawn(page);
|
||||
await settleRadioFrames(page);
|
||||
const newsClusterTarget = await page.evaluate(() => {
|
||||
// A drawn cluster: a painted cluster entry whose anchor picks a
|
||||
@@ -2555,7 +2527,7 @@ async function main() {
|
||||
);
|
||||
|
||||
await page.select('#radio-filter', 'weather');
|
||||
await sleep(700);
|
||||
await whenFramesDrawn(page);
|
||||
const weatherCluster = await page.evaluate(() => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
return { texts: radio.getOverlayDiagnostics().clusterTexts };
|
||||
@@ -2607,19 +2579,18 @@ async function main() {
|
||||
);
|
||||
const idleHorizonScans = await page.evaluate(async () => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
const positionBefore = {
|
||||
x: camera.positionWC.x,
|
||||
y: camera.positionWC.y,
|
||||
z: camera.positionWC.z,
|
||||
x: window.__godsEyeView.engine.camera.pose().position.x,
|
||||
y: window.__godsEyeView.engine.camera.pose().position.y,
|
||||
z: window.__godsEyeView.engine.camera.pose().position.z,
|
||||
};
|
||||
const before = radio.getStats().horizonScans;
|
||||
await new Promise((resolve) => setTimeout(resolve, 850));
|
||||
const dx = camera.positionWC.x - positionBefore.x;
|
||||
const dy = camera.positionWC.y - positionBefore.y;
|
||||
const dz = camera.positionWC.z - positionBefore.z;
|
||||
const dx = window.__godsEyeView.engine.camera.pose().position.x - positionBefore.x;
|
||||
const dy = window.__godsEyeView.engine.camera.pose().position.y - positionBefore.y;
|
||||
const dz = window.__godsEyeView.engine.camera.pose().position.z - positionBefore.z;
|
||||
return {
|
||||
before,
|
||||
after: radio.getStats().horizonScans,
|
||||
@@ -2729,21 +2700,19 @@ async function main() {
|
||||
&& slider.getBoundingClientRect().width > 0;
|
||||
});
|
||||
|
||||
const horizon = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
const horizon = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
// What is drawn, through the engine's picks at every fixture station's
|
||||
// canvas point: a station behind the globe projects onto the canvas
|
||||
// too (the point sets draw without a depth test), so a pick of one
|
||||
// there means it was not culled.
|
||||
const setView = (lon, lat = 0) => {
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({ longitude: lon * Math.PI / 180, latitude: lat * Math.PI / 180, height: 2_000_000 }),
|
||||
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: 2_000_000 }),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
};
|
||||
setView(-54);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
@@ -2894,7 +2863,7 @@ async function main() {
|
||||
buttons: type === 'pointercancel' || type === 'pointerup' ? 0 : 1,
|
||||
}));
|
||||
const camera = gev.viewer.camera;
|
||||
const originalFlyTo = camera.flyTo;
|
||||
const originalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
const flyToCalls = [];
|
||||
camera.flyTo = (options) => flyToCalls.push(options);
|
||||
const before = {
|
||||
@@ -3208,7 +3177,7 @@ async function main() {
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(tunerCommitTarget.targetX, tunerCommitTarget.y, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
await sleep(650);
|
||||
await whenFramesDrawn(page);
|
||||
const tunerCommit = await page.evaluate((target) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const slider = document.getElementById('radio-tuner-slider');
|
||||
@@ -3317,7 +3286,7 @@ async function main() {
|
||||
&& sample.ratio >= microDragSamples[index - 1].ratio)),
|
||||
JSON.stringify(microDragSamples),
|
||||
);
|
||||
const viewportTuner = await page.evaluate(async (selectedId) => {
|
||||
const viewportTuner = await evaluateWithGeo(page, async (qaGeo, selectedId) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const style = gev.styleManager;
|
||||
const before = {
|
||||
@@ -3326,11 +3295,11 @@ async function main() {
|
||||
selectedIndex: style._radioControls._radioTunerStations.findIndex((station) => station.id === selectedId),
|
||||
needle: Number(document.getElementById('radio-tuner').style.getPropertyValue('--radio-tuner-ratio')),
|
||||
};
|
||||
const current = gev.viewer.camera.positionCartographic;
|
||||
const current = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
const oppositeLongitude = ((current.longitude + 2 * Math.PI) % (2 * Math.PI)) - Math.PI;
|
||||
gev.viewer.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
gev.viewer.camera.setView({
|
||||
destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.host.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: oppositeLongitude,
|
||||
latitude: -current.latitude,
|
||||
height: 2_000_000,
|
||||
@@ -3373,7 +3342,7 @@ async function main() {
|
||||
};
|
||||
});
|
||||
await page.$eval('#radio-next-btn', (button) => button.click());
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
const nextNeedleAfter = await page.evaluate(() => {
|
||||
const style = window.__godsEyeView.styleManager;
|
||||
const selectedId = window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().selected?.id;
|
||||
@@ -3568,10 +3537,9 @@ async function main() {
|
||||
&& fullPoolNavigation.afterNextWrap === fullPoolNavigation.expectedNextWrap,
|
||||
JSON.stringify(fullPoolNavigation),
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: 2_000_000,
|
||||
@@ -3579,7 +3547,7 @@ async function main() {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
});
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
// Next left the Radio stopped; a release plays only from an active stream.
|
||||
await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('radio').module.togglePlayback({ origin: 'user' }));
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
@@ -3630,8 +3598,7 @@ async function main() {
|
||||
);
|
||||
await page.select('#radio-filter', 'news');
|
||||
|
||||
const markerTarget = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const markerTarget = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
// Radio clusters on screen in its own display now (no Cesium
|
||||
@@ -3645,26 +3612,26 @@ async function main() {
|
||||
origin: 'programmatic',
|
||||
});
|
||||
}
|
||||
viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
const markerView = {
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -174 * Math.PI / 180,
|
||||
latitude: 6 * Math.PI / 180,
|
||||
height: 500_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
};
|
||||
viewer.camera.setView(markerView);
|
||||
window.__godsEyeView.engine.camera.setView(markerView);
|
||||
await new Promise((resolve) => {
|
||||
let removePostRender = null;
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
removePostRender?.();
|
||||
resolve();
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
radio.setFilter('news');
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
return new Promise((resolve) => {
|
||||
@@ -3677,13 +3644,13 @@ async function main() {
|
||||
resolve(result);
|
||||
};
|
||||
const probe = () => {
|
||||
const camera = viewer.camera.positionCartographic;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
const longitude = camera.longitude * 180 / Math.PI;
|
||||
const latitude = camera.latitude * 180 / Math.PI;
|
||||
if (Math.abs(longitude + 174) > 0.5 || Math.abs(latitude - 6) > 0.5) {
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView(markerView);
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView(markerView);
|
||||
window.__godsEyeView.engine.render.request();
|
||||
return;
|
||||
}
|
||||
const diagnostics = radio.getOverlayDiagnostics();
|
||||
@@ -3726,16 +3693,16 @@ async function main() {
|
||||
overlayEntryCount: diagnostics.entryCount,
|
||||
worldOverlay: getWorldOverlayDiagnostics(),
|
||||
camera: {
|
||||
longitude: viewer.camera.positionCartographic.longitude * 180 / Math.PI,
|
||||
latitude: viewer.camera.positionCartographic.latitude * 180 / Math.PI,
|
||||
height: viewer.camera.positionCartographic.height,
|
||||
longitude: window.__godsEyeView.engine.camera.pose().cartographic.longitude * 180 / Math.PI,
|
||||
latitude: window.__godsEyeView.engine.camera.pose().cartographic.latitude * 180 / Math.PI,
|
||||
height: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
},
|
||||
};
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
};
|
||||
timeout = setTimeout(() => finish(lastObservation || { found: false, reason: 'no post-render probe' }), 8000);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(probe);
|
||||
viewer.scene.requestRender();
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(probe);
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
});
|
||||
check('Radio exposes a visible marker for click-tolerance QA', markerTarget?.found === true, JSON.stringify(markerTarget));
|
||||
@@ -3761,14 +3728,13 @@ async function main() {
|
||||
check('an offset click within 8px reliably selects the intended Radio dot', offsetSelected === markerTarget.id, offsetSelected);
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const camera = gev.viewer.camera;
|
||||
const radio = gev.dataManager.layers.get('radio').module;
|
||||
camera.cancelFlight();
|
||||
gev.viewer.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
camera.setView({
|
||||
destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.host.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: 2_000_000,
|
||||
@@ -3792,12 +3758,12 @@ async function main() {
|
||||
window.__qaRadioFailedNearest = false;
|
||||
window.__qaRadioDelayNextFailure = true;
|
||||
window.__qaRadioFlyToCalls = [];
|
||||
window.__qaRadioOriginalFlyTo = camera.flyTo;
|
||||
window.__qaRadioCameraHeight = camera.positionCartographic.height;
|
||||
window.__qaRadioOriginalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
window.__qaRadioCameraHeight = window.__godsEyeView.engine.camera.pose().cartographic.height;
|
||||
camera.flyTo = (options) => window.__qaRadioFlyToCalls.push(options);
|
||||
gev.dataManager.layers.get('flights').enabled = true;
|
||||
});
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('global-context-panel', false);
|
||||
@@ -3805,7 +3771,7 @@ async function main() {
|
||||
});
|
||||
await page.waitForSelector('#radio-next-btn', { visible: true });
|
||||
await page.evaluate(() => {
|
||||
window.__qaRadioCameraHeight = window.__godsEyeView.viewer.camera.positionCartographic.height;
|
||||
window.__qaRadioCameraHeight = window.__godsEyeView.engine.camera.pose().cartographic.height;
|
||||
});
|
||||
const radioOnlyNextDispatched = await page.evaluate(() => {
|
||||
const button = document.getElementById('radio-next-btn');
|
||||
@@ -3815,10 +3781,9 @@ async function main() {
|
||||
});
|
||||
if (!radioOnlyNextDispatched) throw new Error('Radio next button unavailable');
|
||||
await page.waitForFunction(() => typeof window.__qaRejectRadioPlay === 'function');
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: 2_400_000,
|
||||
@@ -3828,12 +3793,11 @@ async function main() {
|
||||
window.__qaRejectRadioPlay();
|
||||
});
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
const playing = await page.evaluate(async () => {
|
||||
const playing = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const { getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const flightTargets = window.__qaRadioFlyToCalls.map(({ destination }) => {
|
||||
const position = viewer.scene.globe.ellipsoid.cartesianToCartographic(destination);
|
||||
const position = qaGeo.cartographic.fromCartesian(destination);
|
||||
return {
|
||||
lat: position.latitude * 180 / Math.PI,
|
||||
lon: position.longitude * 180 / Math.PI,
|
||||
@@ -3887,7 +3851,7 @@ async function main() {
|
||||
const run = async (ownerPrefix, buttonId) => {
|
||||
const owner = viewer.entities.add({
|
||||
id: `qa:${ownerPrefix}:radio-camera-owner`,
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
});
|
||||
owner.gevTrackedId = `${ownerPrefix}:qa-radio-camera-owner`;
|
||||
viewer.trackedEntity = owner;
|
||||
@@ -3903,7 +3867,7 @@ async function main() {
|
||||
afterStationId: radio.getUIState().selected?.id || null,
|
||||
flyToDelta: window.__qaRadioFlyToCalls.length - beforeFlyToCalls,
|
||||
trackedIdentityPreserved: viewer.trackedEntity === owner,
|
||||
trackedId: viewer.trackedEntity?.gevTrackedId || null,
|
||||
trackedId: window.__godsEyeView.engine.camera.following()?.label?.id || null,
|
||||
};
|
||||
viewer.trackedEntity = undefined;
|
||||
viewer.entities.remove(owner);
|
||||
@@ -3937,7 +3901,7 @@ async function main() {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const owner = viewer.entities.add({
|
||||
id: 'qa:flights:delayed-radio-fallback-owner',
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
});
|
||||
owner.gevTrackedId = 'flights:qa-delayed-radio-fallback-owner';
|
||||
viewer.trackedEntity = owner;
|
||||
@@ -3952,7 +3916,7 @@ async function main() {
|
||||
const result = {
|
||||
flyToDelta: window.__qaRadioFlyToCalls.length - beforeFlyToCalls,
|
||||
trackedIdentityPreserved: viewer.trackedEntity === owner,
|
||||
trackedId: viewer.trackedEntity?.gevTrackedId || null,
|
||||
trackedId: window.__godsEyeView.engine.camera.following()?.label?.id || null,
|
||||
selectedId: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().selected?.id || null,
|
||||
};
|
||||
viewer.trackedEntity = undefined;
|
||||
@@ -3968,10 +3932,9 @@ async function main() {
|
||||
&& Boolean(delayedFallbackOwnership.selectedId),
|
||||
JSON.stringify(delayedFallbackOwnership),
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: window.__qaRadioCameraHeight,
|
||||
@@ -3992,21 +3955,20 @@ async function main() {
|
||||
&& playing.host.entriesBySource?.radio === playing.overlay.entryCount,
|
||||
JSON.stringify({ overlay: playing.overlay, host: playing.host.entriesBySource }),
|
||||
);
|
||||
const closeSelectedText = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const closeSelectedText = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const selected = radio.getUIState().selected;
|
||||
window.__qaRadioVisualView = {
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
direction: viewer.camera.directionWC.clone(),
|
||||
up: viewer.camera.upWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
direction: window.__godsEyeView.engine.camera.pose().direction,
|
||||
up: window.__godsEyeView.engine.camera.pose().up,
|
||||
};
|
||||
const destination = viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
const destination = qaGeo.cartographic.toCartesian({
|
||||
longitude: selected.lon * Math.PI / 180,
|
||||
latitude: selected.lat * Math.PI / 180,
|
||||
height: 5_000,
|
||||
});
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination,
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
@@ -4033,7 +3995,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
resolve({ rect: null, host: getWorldOverlayDiagnostics() });
|
||||
}, 150);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const rect = getOverlayPaintRect('radio', expectedEntryId);
|
||||
@@ -4042,7 +4004,7 @@ async function main() {
|
||||
host: getWorldOverlayDiagnostics(),
|
||||
});
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
shown = selectedShown();
|
||||
}
|
||||
@@ -4051,7 +4013,7 @@ async function main() {
|
||||
rect: painted?.rect || null,
|
||||
host: painted?.host || getWorldOverlayDiagnostics(),
|
||||
selectedEntityShow: shown,
|
||||
cameraHeight: viewer.camera.positionCartographic.height,
|
||||
cameraHeight: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
};
|
||||
});
|
||||
check(
|
||||
@@ -4062,13 +4024,11 @@ async function main() {
|
||||
JSON.stringify(closeSelectedText),
|
||||
);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'selected-label-desktop.png') });
|
||||
const highGlobalSelectedText = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const highGlobalSelectedText = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const selected = radio.getUIState().selected;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (selected.lon + 10) * Math.PI / 180,
|
||||
latitude: selected.lat * Math.PI / 180,
|
||||
height: 5_000_000,
|
||||
@@ -4086,7 +4046,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
resolve(null);
|
||||
}, 150);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const painted = getOverlayPaintRect('radio', expectedEntryId);
|
||||
@@ -4097,7 +4057,7 @@ async function main() {
|
||||
h: painted.h,
|
||||
} : null);
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
}
|
||||
// The selected point and its bracket are instances of the
|
||||
@@ -4129,14 +4089,14 @@ async function main() {
|
||||
entityAnchor: entityAnchor ? { x: entityAnchor.x, y: entityAnchor.y } : null,
|
||||
entityDistance: entityPosition
|
||||
? Math.hypot(
|
||||
viewer.camera.positionWC.x - entityPosition.x,
|
||||
viewer.camera.positionWC.y - entityPosition.y,
|
||||
viewer.camera.positionWC.z - entityPosition.z,
|
||||
window.__godsEyeView.engine.camera.pose().position.x - entityPosition.x,
|
||||
window.__godsEyeView.engine.camera.pose().position.y - entityPosition.y,
|
||||
window.__godsEyeView.engine.camera.pose().position.z - entityPosition.z,
|
||||
)
|
||||
: null,
|
||||
overlay: radio.getOverlayDiagnostics(),
|
||||
host: getWorldOverlayDiagnostics(),
|
||||
cameraHeight: viewer.camera.positionCartographic.height,
|
||||
cameraHeight: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
};
|
||||
});
|
||||
check(
|
||||
@@ -4164,7 +4124,7 @@ async function main() {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
window.__godsEyeView.styleManager.toggleCleanView(true);
|
||||
viewer.resize();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
});
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'selected-label-mobile.png') });
|
||||
@@ -4173,19 +4133,18 @@ async function main() {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
window.__godsEyeView.styleManager.toggleCleanView(false);
|
||||
viewer.resize();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const view = window.__qaRadioVisualView;
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: view.position,
|
||||
orientation: { direction: view.direction, up: view.up },
|
||||
});
|
||||
delete window.__qaRadioVisualView;
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.__qaRadioFlyToCalls = [];
|
||||
@@ -4202,10 +4161,10 @@ async function main() {
|
||||
radioOnlyNextClicked,
|
||||
`clicked=${radioOnlyNextClicked}`,
|
||||
);
|
||||
const radioOnlyFocusCalls = await page.evaluate(() => {
|
||||
const radioOnlyFocusCalls = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const calls = window.__qaRadioFlyToCalls.length;
|
||||
const heights = window.__qaRadioFlyToCalls.map((options) => (
|
||||
window.__godsEyeView.viewer.scene.globe.ellipsoid.cartesianToCartographic(options.destination).height
|
||||
qaGeo.cartographic.fromCartesian(options.destination).height
|
||||
));
|
||||
window.__godsEyeView.viewer.camera.flyTo = window.__qaRadioOriginalFlyTo;
|
||||
delete window.__qaRadioOriginalFlyTo;
|
||||
@@ -4218,19 +4177,19 @@ async function main() {
|
||||
JSON.stringify(radioOnlyFocusCalls),
|
||||
);
|
||||
|
||||
const stagedGlobeNavigation = await page.evaluate(async () => {
|
||||
const stagedGlobeNavigation = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const viewer = gev.viewer;
|
||||
const camera = viewer.camera;
|
||||
const radio = gev.dataManager.layers.get('radio').module;
|
||||
const stationIds = radio.getTunerStations(3).map((station) => station.id);
|
||||
if (stationIds.length < 3) throw new Error('Radio recenter QA requires three visible stations');
|
||||
const originalFlyTo = camera.flyTo;
|
||||
const originalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
const calls = [];
|
||||
camera.flyTo = (options) => calls.push(options);
|
||||
const setCamera = (height, pitchDegrees, headingDegrees = 38) => {
|
||||
camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -133.9483 * Math.PI / 180,
|
||||
latitude: 23.4741 * Math.PI / 180,
|
||||
height,
|
||||
@@ -4241,18 +4200,18 @@ async function main() {
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
};
|
||||
const completeFlight = (index) => {
|
||||
const flight = calls[index];
|
||||
camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: flight.destination,
|
||||
orientation: flight.orientation,
|
||||
});
|
||||
flight.complete?.();
|
||||
};
|
||||
const describe = (flight) => {
|
||||
const cartographic = viewer.scene.globe.ellipsoid.cartesianToCartographic(flight.destination);
|
||||
const cartographic = qaGeo.cartographic.fromCartesian(flight.destination);
|
||||
return {
|
||||
lat: cartographic.latitude * 180 / Math.PI,
|
||||
lon: cartographic.longitude * 180 / Math.PI,
|
||||
@@ -4279,7 +4238,7 @@ async function main() {
|
||||
radio.cycleStation(1, { rotate: true, autoplay: false, stationIds });
|
||||
const owner = viewer.entities.add({
|
||||
id: 'qa:flights:radio-recenter-stage-owner',
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
});
|
||||
owner.gevTrackedId = 'flights:qa-radio-recenter-stage-owner';
|
||||
viewer.trackedEntity = owner;
|
||||
@@ -4509,7 +4468,7 @@ async function main() {
|
||||
);
|
||||
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager.setPanelCollapsed('radio-panel', false));
|
||||
await sleep(350);
|
||||
await whenFramesDrawn(page);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'desktop.png') });
|
||||
const desktop = await page.$eval('#radio-panel', (element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -4531,7 +4490,7 @@ async function main() {
|
||||
await page.click('#radio-play-btn');
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 });
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
await page.$eval('#radio-panel', (element) => element.scrollIntoView({ block: 'nearest' }));
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'mobile.png') });
|
||||
const mobile = await page.$eval('#radio-panel', (element) => {
|
||||
@@ -4599,7 +4558,7 @@ async function main() {
|
||||
failedResponses.slice(0, 3).join(' | '),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
const failures = results.filter((result) => !result.ok).length;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native imagery stacks and split position verify the Cesium imagery adapter.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenUiSettled, whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* Rendered acceptance for DATA ▸ Recent Imagery against the real NASA
|
||||
* endpoints (CMR catalog, GIBS tiles, Worldview snapshots): the rail panel,
|
||||
@@ -17,9 +20,9 @@
|
||||
* 4 s ZOOM IN wait and 0.8 s height-stability tail (defaults: 2 s / 1.2 s).
|
||||
* Every wait is bounded. Without NASA access the run skips with code 0.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
|
||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
@@ -51,21 +54,14 @@ if (!(await networkAvailable())) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !HEADFUL,
|
||||
...(HEADFUL
|
||||
? {
|
||||
executablePath: chromeExecutablePath(),
|
||||
}
|
||||
: {}),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
...(HEADFUL ? ['--window-size=1280,860'] : []),
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -200,7 +196,7 @@ const state = () =>
|
||||
: null,
|
||||
splitLeft: split ? split.getBoundingClientRect().left : null,
|
||||
splitPosition: gev.viewer.scene.splitPosition,
|
||||
canvasWidth: gev.viewer.scene.canvas.clientWidth,
|
||||
canvasWidth: window.__godsEyeView.engine.host.canvas.clientWidth,
|
||||
activeStack: qa.activeStack(),
|
||||
gibsOnGlobe: hosts.globe,
|
||||
gibsOnTileset: hosts.tileset,
|
||||
@@ -212,25 +208,27 @@ const hostsDetail = (s) =>
|
||||
|
||||
/** Put the camera straight down over a point at a height, no flight. */
|
||||
const setView = (view) =>
|
||||
page.evaluate((target) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.scene.tweens?.removeAll?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (target.lon * Math.PI) / 180,
|
||||
latitude: (target.lat * Math.PI) / 180,
|
||||
height: target.height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, view);
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, target) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (target.lon * Math.PI) / 180,
|
||||
latitude: (target.lat * Math.PI) / 180,
|
||||
height: target.height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
view,
|
||||
);
|
||||
|
||||
/** Ground width of the camera's view rectangle, in km (0 without one). */
|
||||
const viewWidthKm = () =>
|
||||
page.evaluate(() => {
|
||||
const rect = window.__godsEyeView.viewer.camera.computeViewRectangle?.();
|
||||
const rect = window.__godsEyeView.engine.camera.viewRectangle?.();
|
||||
if (!rect) return 0;
|
||||
const lat = (rect.south + rect.north) / 2;
|
||||
return Math.abs(rect.east - rect.west) * 6371 * Math.cos(lat);
|
||||
@@ -279,8 +277,9 @@ async function probe() {
|
||||
const tilesSettled = () =>
|
||||
until(() => {
|
||||
const gev = window.__godsEyeView;
|
||||
if (gev.viewer.scene.globe.show) return gev.viewer.scene.globe.tilesLoaded;
|
||||
return !gev.tileset || gev.tileset.tilesLoaded;
|
||||
if (gev.viewer.scene.globe.show)
|
||||
return window.__godsEyeView.engine.surface.tilesLoaded();
|
||||
return !gev.tileset || window.__godsEyeView.engine.surface.tilesLoaded();
|
||||
});
|
||||
const backOnPhotoreal = () =>
|
||||
until(() => window.__riQa.activeStack() === 'photoreal');
|
||||
@@ -334,7 +333,7 @@ const measure = () =>
|
||||
let layoutBaseline = null;
|
||||
/** Every control's rect matches the baseline within 1 px. */
|
||||
async function checkLayout(label) {
|
||||
await wait(150);
|
||||
await whenUiSettled(page);
|
||||
const now = await measure();
|
||||
if (!layoutBaseline) {
|
||||
layoutBaseline = now;
|
||||
@@ -370,12 +369,13 @@ try {
|
||||
() =>
|
||||
window.__gevRecentImagery &&
|
||||
window.__godsEyeView &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
window.__godsEyeView.engine,
|
||||
null,
|
||||
90_000,
|
||||
);
|
||||
check('the app boots with the Recent Imagery handle', booted);
|
||||
if (!booted) throw new Error('boot timeout');
|
||||
await whenSettled(page);
|
||||
await page.evaluate(installPageHelpers);
|
||||
|
||||
await setView(AUSTIN);
|
||||
@@ -586,10 +586,8 @@ try {
|
||||
changedColour = colour;
|
||||
break;
|
||||
}
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.viewer.scene.requestRender(),
|
||||
);
|
||||
await wait(1000);
|
||||
await page.evaluate(() => window.__godsEyeView.engine.render.request());
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
check(
|
||||
'the rendered frame inside the box changes once the preview drapes',
|
||||
@@ -681,7 +679,7 @@ try {
|
||||
);
|
||||
await checkLayout('pinned');
|
||||
await page.keyboard.press(targetIndex > 0 ? 'ArrowLeft' : 'ArrowRight');
|
||||
await wait(700);
|
||||
await whenUiSettled(page);
|
||||
current = await state();
|
||||
check(
|
||||
'with the image pinned, focus moves without changing the map',
|
||||
@@ -981,7 +979,7 @@ try {
|
||||
let refusalWidth = 0;
|
||||
for (const height of [400_000, 900_000, 1_800_000]) {
|
||||
await setView({ ...AUSTIN, height });
|
||||
await wait(300);
|
||||
await whenUiSettled(page);
|
||||
refusalWidth = await viewWidthKm();
|
||||
if (refusalWidth > 1_100) break;
|
||||
}
|
||||
@@ -1041,10 +1039,9 @@ try {
|
||||
await panelShot('05-refused-panel');
|
||||
// ZOOM IN: straight down to the height whose view is 400 km wide.
|
||||
const fitHeight = await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const canvas = viewer.scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const halfWidth =
|
||||
Math.tan(viewer.camera.frustum.fovy / 2) *
|
||||
Math.tan(window.__godsEyeView.engine.camera.pose().fovy / 2) *
|
||||
(canvas.clientWidth / canvas.clientHeight);
|
||||
return Math.min(400, Math.max(5, 400 / (2 * halfWidth))) * 1000;
|
||||
});
|
||||
@@ -1052,14 +1049,14 @@ try {
|
||||
const zoomed = await until(
|
||||
(target) =>
|
||||
Math.abs(
|
||||
window.__godsEyeView.viewer.camera.positionCartographic.height - target,
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height - target,
|
||||
) <=
|
||||
target * 0.01,
|
||||
fitHeight,
|
||||
ZOOM_TIMEOUT_MS,
|
||||
);
|
||||
const zoomedHeight = await page.evaluate(
|
||||
() => window.__godsEyeView.viewer.camera.positionCartographic.height,
|
||||
() => window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
);
|
||||
check(
|
||||
`ZOOM IN reaches target within ${ZOOM_TIMEOUT_MS / 1_000} s`,
|
||||
@@ -1235,7 +1232,7 @@ try {
|
||||
}, id);
|
||||
const imageryPanel = 'recent-imagery-panel';
|
||||
const collapsedNow = (await togglePanel(imageryPanel)) === false;
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
const headerOnly = await page.evaluate(() => ({
|
||||
panel: document.getElementById('recent-imagery-panel')?.offsetHeight,
|
||||
body: document.getElementById('recent-imagery-panel-body')?.offsetHeight,
|
||||
@@ -1249,7 +1246,7 @@ try {
|
||||
JSON.stringify(headerOnly),
|
||||
);
|
||||
const expandedAgain = await togglePanel(imageryPanel);
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
const bodyBack = await page.evaluate(
|
||||
() => document.getElementById('recent-imagery-panel-body')?.offsetHeight,
|
||||
);
|
||||
@@ -1270,7 +1267,7 @@ try {
|
||||
cctv: document.getElementById('cctv-panel')?.offsetHeight,
|
||||
})),
|
||||
);
|
||||
await wait(100);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
const tail = (key) =>
|
||||
new Set(samples.slice(-HEIGHT_TAIL_SAMPLES).map((s) => s[key]));
|
||||
@@ -1315,7 +1312,7 @@ try {
|
||||
for (const height of [900, 700]) {
|
||||
const at = `(${height} px viewport)`;
|
||||
await page.setViewport({ width: 1280, height });
|
||||
await wait(600);
|
||||
await whenUiSettled(page);
|
||||
check(
|
||||
`the CCTV panel expands from its launcher ${at}`,
|
||||
(await togglePanel('cctv-panel')) === true,
|
||||
@@ -1372,7 +1369,7 @@ try {
|
||||
?.classList.contains('collapsed'),
|
||||
);
|
||||
if (imageryExpanded) await togglePanel(imageryPanel);
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
const onScreen = await page.evaluate(() =>
|
||||
Object.fromEntries(
|
||||
[
|
||||
@@ -1389,7 +1386,7 @@ try {
|
||||
JSON.stringify(onScreen),
|
||||
);
|
||||
await togglePanel(imageryPanel);
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
|
||||
// ── scroll preservation (700 px viewport, body scrollable) ───────────────
|
||||
@@ -1397,7 +1394,7 @@ try {
|
||||
const header = document.querySelector('#ri-details .rail-card-header');
|
||||
if (header?.getAttribute('aria-expanded') !== 'true') header?.click();
|
||||
});
|
||||
await wait(300);
|
||||
await whenUiSettled(page);
|
||||
const bodyScroll = () =>
|
||||
page.evaluate(() => {
|
||||
const body = document.getElementById('recent-imagery-panel-body');
|
||||
@@ -1424,7 +1421,7 @@ try {
|
||||
layer.setToolActive(true);
|
||||
layer.setToolActive(false);
|
||||
});
|
||||
await wait(300);
|
||||
await whenUiSettled(page);
|
||||
const afterNotify = await bodyScroll();
|
||||
check(
|
||||
'the body scroll position survives alpha and state notifications',
|
||||
@@ -1434,7 +1431,7 @@ try {
|
||||
await focusStrip();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await wait(600);
|
||||
await whenUiSettled(page);
|
||||
const afterArrows = await bodyScroll();
|
||||
check(
|
||||
'the body scroll position survives arrow navigation',
|
||||
@@ -1488,7 +1485,7 @@ try {
|
||||
error?.stack || error?.message || String(error),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`shots: ${shots.join(', ')}`);
|
||||
console.log(`RESULT: ${checks} checks, ${failures} failures`);
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
/** Exercise installed Scene controls and real project/playback operations. */
|
||||
import { whenSettled, whenUiSettled } 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';
|
||||
|
||||
const shots = path.resolve('qa-shots/scene-controls');
|
||||
fs.mkdirSync(shots, { recursive: true });
|
||||
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();
|
||||
@@ -34,11 +33,10 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector?._controls &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__godsEyeView?.sceneDirector?._controls,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
window.__qaSceneState = [];
|
||||
window.__godsEyeView.sceneDirector.subscribe((notification) =>
|
||||
@@ -294,6 +292,7 @@ try {
|
||||
);
|
||||
await page.click('#scene-start-btn');
|
||||
await page.waitForFunction(() => window.__godsEyeView.sceneDirector.running);
|
||||
await whenSettled(page);
|
||||
await page.$eval('#scene-stop-btn', (button) => button.click());
|
||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector.running);
|
||||
check(
|
||||
@@ -364,7 +363,7 @@ try {
|
||||
await page.setViewport({ width: 390, height: 844 });
|
||||
// Phones present Scenes in the Layers tab's sheet.
|
||||
await page.click('#mobile-tab-bar [data-mobile-sheet="layers"]');
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await whenUiSettled(page);
|
||||
await page.screenshot({ path: path.join(shots, 'narrow.png') });
|
||||
check(
|
||||
'Scene controls stay inside the narrow viewport',
|
||||
@@ -483,6 +482,6 @@ try {
|
||||
errors.length === 0,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (failures) process.exitCode = 1;
|
||||
@@ -14,13 +14,13 @@
|
||||
* Needs the repository's puppeteer (devDependencies). Exit status 0 when the
|
||||
* drop selects the file, 1 otherwise.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import http from 'node:http';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { createServer } from 'vite';
|
||||
import { VUE_PLUGIN_OPTIONS } from '../build/vueOptions.js';
|
||||
|
||||
@@ -77,7 +77,7 @@ async function main() {
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const url = `http://127.0.0.1:${server.address().port}/`;
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
const browser = await launchBrowser({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1000, height: 800 });
|
||||
@@ -100,7 +100,7 @@ async function main() {
|
||||
}
|
||||
console.log('OK: a file dropped on the visible control is selected');
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
server.close();
|
||||
await vite.close();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -7,17 +7,16 @@
|
||||
* feather 0/11/100/200, layout independence,
|
||||
* share/scene restore, and visible text backing including rotated metadata.
|
||||
*/
|
||||
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';
|
||||
|
||||
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();
|
||||
@@ -214,16 +213,7 @@ async function openApplication(url) {
|
||||
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return (
|
||||
cover?.classList.contains('hidden') &&
|
||||
Number(getComputedStyle(cover).opacity) === 0
|
||||
);
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -668,7 +658,7 @@ try {
|
||||
}
|
||||
check('runtime has no uncaught browser errors', errors.length === 0, errors);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
/** Run against a dev server or built preview with QA_BASE_URL (default :4179). */
|
||||
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({
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH || (await puppeteer.executablePath()),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--use-gl=angle', '--use-angle=swiftshader'],
|
||||
args: [],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
@@ -30,11 +29,7 @@ try {
|
||||
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
// DISPLAY holds most segmented controls; Draw's shape group is in UTILITIES.
|
||||
await page.evaluate(() => {
|
||||
for (const panel of ['pp-toggles', 'utilities-panel'])
|
||||
@@ -347,7 +342,7 @@ try {
|
||||
await page.screenshot({ path: `${shots}/failure.png` }).catch(() => {});
|
||||
throw error;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures.length} failures`);
|
||||
process.exitCode = failures.length ? 1 : 0;
|
||||
@@ -1,17 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: standalone native scenes validate Cesium model lighting.
|
||||
/**
|
||||
* Lit airplane regression: StarrySky must match a black environment baseline.
|
||||
* A white environment is a positive control: it must visibly brighten the
|
||||
* same model. Each case uses a fresh scene and dynamic environment map.
|
||||
* Usage: node scripts/qa-sky-model-lighting.mjs [--url http://localhost:4333/]
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const i = process.argv.indexOf('--url');
|
||||
const base = i > 0 ? process.argv[i + 1] : 'http://localhost:4333/';
|
||||
const url = new URL('/__qa_sky_model__', base);
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||
});
|
||||
@@ -145,5 +146,5 @@ try {
|
||||
'PASS: lit airplane matches black sky; white control is brighter',
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
/** Badge acceptance on dev/preview. Camera state is a disposable controller fixture. */
|
||||
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);
|
||||
@@ -31,11 +32,10 @@ try {
|
||||
{ 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(async () => {
|
||||
const { dataManager, styleManager: manager } = window.__godsEyeView;
|
||||
await dataManager.setEnabled('wind', true);
|
||||
@@ -272,7 +272,7 @@ try {
|
||||
}
|
||||
check('no uncaught browser errors', errors.length === 0, errors);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`${checks} checks; ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
+34
-29
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
/**
|
||||
* Browser QA for the Street Level layer against a running server:
|
||||
@@ -223,6 +226,7 @@ async function boot(page, url) {
|
||||
await page.waitForFunction(() => Boolean(window.__godsEyeView?.dataManager), {
|
||||
timeout: 150_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() =>
|
||||
document.querySelector('.first-run-explore')?.click(),
|
||||
);
|
||||
@@ -338,8 +342,9 @@ async function assertKeyRequired(page) {
|
||||
const findPick = (page, prefix, { box = null, step = 3 } = {}) =>
|
||||
page.evaluate(
|
||||
(want, area, stride) => {
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
const rect = scene.canvas.getBoundingClientRect();
|
||||
const { engine } = window.__godsEyeView;
|
||||
const rect =
|
||||
window.__godsEyeView.engine.host.canvas.getBoundingClientRect();
|
||||
const { x, y, w, h } = area || {
|
||||
x: 0,
|
||||
y: Math.round(rect.height / 2),
|
||||
@@ -348,7 +353,7 @@ const findPick = (page, prefix, { box = null, step = 3 } = {}) =>
|
||||
};
|
||||
for (let py = y; py < y + h; py += stride)
|
||||
for (let px = x; px < x + w; px += stride) {
|
||||
const id = scene.pick({ x: px, y: py })?.id;
|
||||
const id = engine.picker.pick({ x: px, y: py })?.id;
|
||||
if (typeof id === 'string' && id.startsWith(want))
|
||||
return {
|
||||
x: rect.left + px,
|
||||
@@ -378,30 +383,33 @@ async function findPickUntil(
|
||||
for (;;) {
|
||||
const hit = await findPick(page, prefix, { box, step });
|
||||
if (hit || Date.now() >= deadline) return hit;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await whenSettled(page);
|
||||
}
|
||||
}
|
||||
|
||||
/** Park the camera at `at`; a late startup flight can move it. */
|
||||
async function park(page, at = PARK) {
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
const stayed = await page.evaluate(async (at) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
v.camera.cancelFlight?.();
|
||||
const C = v.camera.positionCartographic.constructor;
|
||||
v.camera.setView({
|
||||
destination: v.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
C.fromDegrees(at.lon, at.lat, at.height),
|
||||
),
|
||||
orientation: { heading: 0, pitch: at.pitch, roll: 0 },
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const c = v.camera.positionCartographic;
|
||||
return (
|
||||
Math.abs((c.longitude * 180) / Math.PI - at.lon) < 0.001 &&
|
||||
Math.abs((c.latitude * 180) / Math.PI - at.lat) < 0.001
|
||||
);
|
||||
}, at);
|
||||
const stayed = await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, at) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian(
|
||||
C.fromDegrees(at.lon, at.lat, at.height),
|
||||
),
|
||||
orientation: { heading: 0, pitch: at.pitch, roll: 0 },
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const c = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return (
|
||||
Math.abs((c.longitude * 180) / Math.PI - at.lon) < 0.001 &&
|
||||
Math.abs((c.latitude * 180) / Math.PI - at.lat) < 0.001
|
||||
);
|
||||
},
|
||||
at,
|
||||
);
|
||||
if (stayed) return;
|
||||
}
|
||||
throw new Error(`the camera would not stay parked at ${JSON.stringify(at)}`);
|
||||
@@ -414,14 +422,11 @@ async function main() {
|
||||
const url = urlIndex >= 0 ? args[urlIndex + 1] : 'http://localhost:4173';
|
||||
const fixtures = args.includes('--fixtures');
|
||||
const strict = args.includes('--strict');
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
(await puppeteer.executablePath()),
|
||||
defaultViewport: VIEWPORT,
|
||||
protocolTimeout: 300_000,
|
||||
args: ['--no-sandbox', '--use-gl=angle', '--use-angle=swiftshader'],
|
||||
args: ['--no-sandbox'],
|
||||
});
|
||||
let count = 0;
|
||||
const skips = [];
|
||||
@@ -494,7 +499,7 @@ async function main() {
|
||||
box === 'column'
|
||||
? await page.evaluate(() => {
|
||||
const { height, width } =
|
||||
window.__godsEyeView.viewer.scene.canvas.getBoundingClientRect();
|
||||
window.__godsEyeView.engine.host.canvas.getBoundingClientRect();
|
||||
return { x: Math.round(width * 0.3), y: 0, w: 1, h: height };
|
||||
})
|
||||
: null;
|
||||
@@ -560,7 +565,7 @@ async function main() {
|
||||
'keyless server: the MAPILLARY chip explains its key instead of switching',
|
||||
async () => {
|
||||
await press(page, '#sl-provider-chips [data-chip-id="mapillary"]');
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await whenSettled(page);
|
||||
assert.ok(litAlone(await getUI(page), 'kartaview'));
|
||||
},
|
||||
);
|
||||
@@ -831,7 +836,7 @@ async function main() {
|
||||
if (dir) console.error(`failure evidence saved in ${dir}/`);
|
||||
throw error;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-52
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native terrain-tree counters verify Cesium retry behavior.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, until } from './qa/ready.mjs';
|
||||
/**
|
||||
* Keyless terrain throttling probe.
|
||||
*
|
||||
@@ -21,8 +24,8 @@
|
||||
* Exits 1 when a throttled tile never recovered or a terrain tile is left in
|
||||
* Cesium's FAILED state.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const appUrl = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
const argv = process.argv.slice(2);
|
||||
@@ -46,20 +49,10 @@ const headful = argv.includes('--headful');
|
||||
const TERRAIN_HOST = 'terrain.reearth.land';
|
||||
const TILE_PATTERN = /\/cesium-mesh\/ellipsoid\/\d+\/\d+\/\d+\.terrain/;
|
||||
|
||||
const executablePath =
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
(await puppeteer.executablePath().catch(() => null));
|
||||
if (!executablePath || !fs.existsSync(executablePath)) {
|
||||
throw new Error('Puppeteer Chrome for Testing is unavailable');
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: headful ? false : 'new',
|
||||
executablePath,
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
'--no-sandbox',
|
||||
],
|
||||
});
|
||||
@@ -149,31 +142,17 @@ page.on('console', (message) => {
|
||||
});
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const waitFor = async (predicate, timeoutMs, label) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (await predicate()) return;
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error(`timed out waiting for ${label}`);
|
||||
};
|
||||
|
||||
const setCamera = (lon, lat, height) =>
|
||||
page.evaluate(
|
||||
({ lon, lat, height }) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { lon, lat, height }) => {
|
||||
// The app's own startup flight would otherwise keep steering the camera
|
||||
// through the burst and turn the zoom-out into a zoom-in.
|
||||
viewer.camera.cancelFlight();
|
||||
const cartographic =
|
||||
viewer.camera.positionCartographic.constructor.fromDegrees(
|
||||
lon,
|
||||
lat,
|
||||
height,
|
||||
);
|
||||
viewer.camera.setView({
|
||||
destination:
|
||||
viewer.scene.globe.ellipsoid.cartographicToCartesian(cartographic),
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
const cartographic = qaGeo.cartographic.fromDegrees(lon, lat, height);
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian(cartographic),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.requestRender?.();
|
||||
@@ -183,7 +162,7 @@ const setCamera = (lon, lat, height) =>
|
||||
|
||||
const cameraHeight = () =>
|
||||
page.evaluate(
|
||||
() => window.__godsEyeView.viewer.camera.positionCartographic.height,
|
||||
() => window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
);
|
||||
|
||||
const globeState = () =>
|
||||
@@ -203,7 +182,7 @@ const globeState = () =>
|
||||
return {
|
||||
failed,
|
||||
loaded,
|
||||
tilesLoaded: viewer.scene.globe.tilesLoaded,
|
||||
tilesLoaded: window.__godsEyeView.engine.surface.tilesLoaded(),
|
||||
provider: viewer.terrainProvider?.constructor?.name,
|
||||
};
|
||||
});
|
||||
@@ -212,8 +191,9 @@ const burst = async () => {
|
||||
if (mode === 'zoomOut') {
|
||||
for (let i = 0; i < notches; i += 1) {
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.zoomOut(viewer.camera.positionCartographic.height * 0.6);
|
||||
window.__godsEyeView.engine.camera.zoomOut(
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height * 0.6,
|
||||
);
|
||||
window.__godsEyeView.requestRender?.();
|
||||
});
|
||||
await sleep(40);
|
||||
@@ -230,14 +210,22 @@ const burst = async () => {
|
||||
|
||||
try {
|
||||
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(() => Boolean(window.__godsEyeView?.viewer), {
|
||||
await page.waitForFunction(() => Boolean(window.__godsEyeView?.engine), {
|
||||
timeout: 60000,
|
||||
});
|
||||
await waitFor(() => layerJsonSeen, 60000, 'the Re:Earth layer.json fetch');
|
||||
await waitFor(
|
||||
await whenSettled(page);
|
||||
await until(() => layerJsonSeen, {
|
||||
timeoutMs: 60000,
|
||||
intervalMs: 250,
|
||||
label: 'the Re:Earth layer.json fetch',
|
||||
});
|
||||
await until(
|
||||
async () => (await globeState()).provider === 'CesiumTerrainProvider',
|
||||
30000,
|
||||
'the keyless terrain provider',
|
||||
{
|
||||
timeoutMs: 30000,
|
||||
intervalMs: 250,
|
||||
label: 'the keyless terrain provider',
|
||||
},
|
||||
);
|
||||
|
||||
const summaries = [];
|
||||
@@ -246,16 +234,16 @@ try {
|
||||
// overwrite the first setView on the very next frame.
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
await setCamera(-97.7431, 30.2672, 40000);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
const height = await cameraHeight();
|
||||
if (Math.abs(height - 40000) < 1000) break;
|
||||
}
|
||||
await waitFor(
|
||||
async () => (await globeState()).tilesLoaded,
|
||||
30000,
|
||||
'the 40 km view to settle',
|
||||
).catch(() => {});
|
||||
await sleep(2000);
|
||||
await until(async () => (await globeState()).tilesLoaded, {
|
||||
timeoutMs: 30000,
|
||||
intervalMs: 250,
|
||||
label: 'the 40 km view to settle',
|
||||
}).catch(() => {});
|
||||
await whenSettled(page);
|
||||
const settledHeight = await cameraHeight();
|
||||
if (Math.abs(settledHeight - 40000) > 1000) {
|
||||
throw new Error(
|
||||
@@ -358,5 +346,5 @@ try {
|
||||
);
|
||||
process.exitCode = failures.length ? 1 : 0;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-traffic-baseline.mjs — Traffic Phase 0 causal-chain capture.
|
||||
*
|
||||
@@ -35,7 +37,7 @@
|
||||
* the latency plan.
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
@@ -53,20 +55,7 @@ const HEADFUL = argv.includes('--headful');
|
||||
const TIMEOUT_MS = Math.max(10_000, getNumberOpt('--timeout-ms', 120_000));
|
||||
const DEGRADED_DELAY_MS = Math.max(0, getNumberOpt('--degraded-delay-ms', 2500));
|
||||
|
||||
const CHROME_EXECUTABLE_CANDIDATES = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
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 sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -95,16 +84,15 @@ function debugUrl(rawUrl) {
|
||||
|
||||
/** Position the camera and explicitly raise the boundaries the layer observes. */
|
||||
async function moveCamera(page, view) {
|
||||
return page.evaluate((v) => {
|
||||
return evaluateWithGeo(page, async (qaGeo, v) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const module = gev.dataManager.layers.get('traffic').module;
|
||||
const beforeLastUpdate = module.getStats().lastUpdate;
|
||||
const boundaryTime = performance.now();
|
||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
||||
const radians = Math.PI / 180;
|
||||
try { gev.viewer.camera.cancelFlight(); } catch { /* no active flight */ }
|
||||
gev.viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* no active flight */ }
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: v.lon * radians,
|
||||
latitude: v.lat * radians,
|
||||
height: v.height,
|
||||
@@ -119,8 +107,7 @@ async function moveCamera(page, view) {
|
||||
// explicitly makes the capture boundary deterministic across GPU speeds.
|
||||
// Pairing still flows through camera.changed's schedule-time anchor; the
|
||||
// synthetic moveEnd is emitted only as the same diagnostic used naturally.
|
||||
gev.viewer.camera.changed.raiseEvent();
|
||||
gev.viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
return { beforeLastUpdate, boundaryTime };
|
||||
}, view);
|
||||
}
|
||||
@@ -144,7 +131,7 @@ async function waitForTrafficCapture(page, beforeLastUpdate, boundaryTime) {
|
||||
boundaryTime,
|
||||
);
|
||||
// Let a final full-pass postRender land after `_fetching` flips false.
|
||||
await sleep(150);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
|
||||
/** Return serializable Traffic User Timing measures created after a boundary. */
|
||||
@@ -272,7 +259,7 @@ function rowsForCapture(capture) {
|
||||
/** Read the active WebGL renderer for the real-GPU/SwiftShader warning. */
|
||||
function readRenderer(page) {
|
||||
return page.evaluate(() => {
|
||||
const gl = window.__godsEyeView?.viewer?.scene?.context?._gl;
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
if (!gl) return 'unknown';
|
||||
const extension = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return extension
|
||||
@@ -285,7 +272,7 @@ async function main() {
|
||||
const url = debugUrl(APP_URL);
|
||||
console.log('\nTraffic Phase 0 — causal-chain baseline capture');
|
||||
console.log(` App URL : ${url}`);
|
||||
console.log(` Browser mode : ${HEADFUL ? 'headful (real GPU expected)' : 'headless SwiftShader (relative-only)'}`);
|
||||
console.log(` Browser mode : headless (QA_ANGLE=${process.env.QA_ANGLE || 'metal'})`);
|
||||
console.log(` Degraded delay : ${DEGRADED_DELAY_MS ? `${DEGRADED_DELAY_MS} ms/request` : 'skipped'}`);
|
||||
|
||||
try {
|
||||
@@ -296,19 +283,9 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const executablePath = findChromeExecutable();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(executablePath ? { executablePath } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1440,900',
|
||||
...(HEADFUL ? [] : ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1440,900', ...(HEADFUL ? [] : [])],
|
||||
});
|
||||
|
||||
let activeProxyDelayMs = 0;
|
||||
@@ -343,19 +320,19 @@ async function main() {
|
||||
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
|
||||
// Disable all persisted overlays, park above traffic's activation ceiling,
|
||||
// then enable traffic without accidentally starting an unmeasured load.
|
||||
await page.evaluate(async (highView) => {
|
||||
await evaluateWithGeo(page, async (qaGeo, highView) => {
|
||||
const gev = window.__godsEyeView;
|
||||
await gev.dataManager.restoreEnabledLayerIds([]);
|
||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
||||
const radians = Math.PI / 180;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: highView.lon * radians,
|
||||
latitude: highView.lat * radians,
|
||||
height: 9000,
|
||||
@@ -363,10 +340,9 @@ async function main() {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
await gev.dataManager.setEnabled('traffic', true);
|
||||
gev.viewer.camera.changed.raiseEvent();
|
||||
gev.viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
}, VIEWS.austin);
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
const renderer = await readRenderer(page);
|
||||
const softwareRenderer = /swiftshader|software/i.test(renderer);
|
||||
@@ -386,22 +362,19 @@ async function main() {
|
||||
|
||||
// Clear the last-bounds gate without clearing the module's road cache, then
|
||||
// revisit the byte-identical camera pose for a true client-cache hit.
|
||||
await page.evaluate((view) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
||||
await evaluateWithGeo(page, async (qaGeo, view) => {
|
||||
const radians = Math.PI / 180;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: view.lon * radians,
|
||||
latitude: view.lat * radians,
|
||||
height: 9000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
gev.viewer.camera.changed.raiseEvent();
|
||||
gev.viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
}, VIEWS.austin);
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
captures.push(await captureState(page, 'exact-viewport revisit', VIEWS.austin, (msValue) => {
|
||||
activeProxyDelayMs = msValue;
|
||||
@@ -467,7 +440,7 @@ async function main() {
|
||||
for (const error of consoleErrors.slice(0, 5)) console.log(` ${error}`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenTilesLoaded, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-traffic-jamviz-ab.mjs — A/B screenshot capture for the jam-viz
|
||||
* congestion prototypes (feat/traffic-jam-viz).
|
||||
@@ -18,7 +20,7 @@
|
||||
* Exits non-zero on harness failure (missing live mode, zero dots, etc.).
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -53,50 +55,18 @@ const VIEWS = [
|
||||
];
|
||||
const MODES = ['none', 'density', 'heatline', 'both'];
|
||||
|
||||
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);
|
||||
const findChromeExecutable = () =>
|
||||
CHROME_EXECUTABLE_CANDIDATES.find((c) => { try { return fs.existsSync(c); } catch { return false; } }) || null;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** Same pattern as qa-cctv-v2: wait for the 3D tileset to settle for crisp shots. */
|
||||
function waitForTilesLoaded(page, timeoutMs) {
|
||||
return page.waitForFunction(
|
||||
() => {
|
||||
const prims = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let i = 0; i < prims.length; i++) {
|
||||
const p = prims.get(i);
|
||||
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
|
||||
return p.tilesLoaded === true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: timeoutMs }
|
||||
).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
/** Teleport to a view (cancelling any camera flight first). */
|
||||
function setView(page, v) {
|
||||
return page.evaluate((view) => {
|
||||
const gev = window.__godsEyeView;
|
||||
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
const ell = gev.viewer.scene.globe.ellipsoid;
|
||||
return evaluateWithGeo(page, async (qaGeo, view) => {
|
||||
try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
const d2r = Math.PI / 180;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ell.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: view.lon * d2r, latitude: view.lat * d2r, height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
@@ -155,15 +125,9 @@ async function main() {
|
||||
}
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
|
||||
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=1600,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=1600,900'],
|
||||
});
|
||||
|
||||
const manifest = [];
|
||||
@@ -174,11 +138,12 @@ async function main() {
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => window.__godsEyeView.dataManager.setEnabled('traffic', true));
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
let firstView = true;
|
||||
for (const view of VIEWS) {
|
||||
@@ -203,7 +168,7 @@ async function main() {
|
||||
);
|
||||
await setView(page, view);
|
||||
const stats = await waitForFreshRender(page, since);
|
||||
await waitForTilesLoaded(page, firstView ? 30000 : 15000);
|
||||
await whenTilesLoaded(page, { timeoutMs: firstView ? 30000 : 15000 }).then(() => true).catch(() => false);
|
||||
await sleep(1400); // flow-recolor race + heat-line pulse mid-bright
|
||||
|
||||
const final = await page.evaluate(() => {
|
||||
@@ -249,7 +214,7 @@ async function main() {
|
||||
exitCode = 3;
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`\nShots + manifest.json → ${OUT_DIR}`);
|
||||
process.exit(exitCode);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native primitive inspection independently measures the Cesium road paint.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* Traffic motion acceptance: real-GPU Puppeteer, stable identities, stationary
|
||||
* camera holds and timestamped CDP JPEGs encoded with ffmpeg. No tile mocks.
|
||||
@@ -9,10 +11,11 @@
|
||||
* --journey follows a neighborhood-to-city route with layer toggles, then
|
||||
* checks Capitol/greenbelt/parking/corridor views against a 1.5 s limit.
|
||||
*/
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import puppeteer from 'puppeteer';
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--help')) {
|
||||
console.log(
|
||||
@@ -37,11 +40,10 @@ fs.mkdirSync(root, { recursive: true });
|
||||
const revision = execFileSync('git', ['rev-parse', 'HEAD'], {
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300000,
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--window-size=1280,800',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -84,12 +86,10 @@ try {
|
||||
u.searchParams.set('trafficRoads', mode);
|
||||
u.searchParams.set('trafficDebug', '1');
|
||||
await page.goto(u.href, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
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,
|
||||
);
|
||||
@@ -99,7 +99,6 @@ try {
|
||||
g.styleManager.setDetection({ enabled: false });
|
||||
for (const [id] of g.dataManager.layers)
|
||||
g.dataManager.setEnabled(id, false);
|
||||
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
const identity = await page.evaluate(() => ({
|
||||
title: document.title,
|
||||
@@ -159,10 +158,10 @@ try {
|
||||
clock.style.cssText =
|
||||
'position:fixed;bottom:4px;left:38%;padding:3px 8px;background:#000c;color:#fff;font:12px monospace;z-index:999999;pointer-events:none';
|
||||
document.body.append(clock);
|
||||
viewer.camera.moveEnd.addEventListener(() =>
|
||||
window.__godsEyeView.engine.camera.moveEnd.addEventListener(() =>
|
||||
window.__motion.moveEnds.push(performance.now()),
|
||||
);
|
||||
scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
if (!window.__motion.active) return;
|
||||
discoverCollections();
|
||||
const now = performance.now(),
|
||||
@@ -174,7 +173,9 @@ try {
|
||||
: ` | camera stopped +${((now - window.__motion.holdStart) / 1000).toFixed(1)}s`);
|
||||
const visit = (id, roadId, point) => {
|
||||
if (!point.show) return;
|
||||
const s = scene.cartesianToCanvasCoordinates(point.position);
|
||||
const s = window.__godsEyeView.engine.projector.worldToWindow(
|
||||
point.position,
|
||||
);
|
||||
if (s && s.x >= 0 && s.y >= 0 && s.x <= 1280 && s.y <= 800)
|
||||
points.set(id, { x: s.x, y: s.y, roadId });
|
||||
};
|
||||
@@ -268,21 +269,19 @@ try {
|
||||
});
|
||||
}, baseline);
|
||||
const fly = (lat, lon, height, pitch, duration, heading = 20) =>
|
||||
page.evaluate(
|
||||
async (v) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, v) => {
|
||||
window.__motion.label = 'camera moving';
|
||||
window.__motion.holdStart = null;
|
||||
viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
await new Promise((resolve) =>
|
||||
viewer.camera.flyTo({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
{
|
||||
latitude: (v.lat * Math.PI) / 180,
|
||||
longitude: (v.lon * Math.PI) / 180,
|
||||
height: v.height,
|
||||
},
|
||||
),
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
latitude: (v.lat * Math.PI) / 180,
|
||||
longitude: (v.lon * Math.PI) / 180,
|
||||
height: v.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: (v.heading * Math.PI) / 180,
|
||||
pitch: (v.pitch * Math.PI) / 180,
|
||||
@@ -534,7 +533,7 @@ try {
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'results.json'),
|
||||
JSON.stringify(
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
import { whenSettled, until } from './qa/ready.mjs';
|
||||
// Deterministic city-navigation exercise; road/flow responses are fixtures.
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { trafficFixtureResponse } from './traffic-fixtures.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const url = args[args.indexOf('--url') + 1] || 'http://localhost:4173';
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 120000,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--use-gl=angle',
|
||||
'--use-angle=swiftshader',
|
||||
'--disable-background-timer-throttling',
|
||||
],
|
||||
});
|
||||
@@ -44,15 +42,15 @@ try {
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await sleep(5000);
|
||||
await whenSettled(page);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.evaluate(async (withCctv) => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const C = await import('/node_modules/.vite/deps/cesium.js');
|
||||
for (const id of ['satellites', 'flights', 'military'])
|
||||
await dataManager.setEnabled(id, false);
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.Cartesian3.fromDegrees(-97.744, 30.267, 3200),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
@@ -71,27 +69,28 @@ try {
|
||||
console.log(
|
||||
'Traffic timeout state',
|
||||
await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
return {
|
||||
stats: dataManager.layers.get('traffic').module.getStats(),
|
||||
camera: viewer.camera.positionCartographic,
|
||||
camera: window.__godsEyeView.engine.camera.pose().cartographic,
|
||||
};
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
const C = await import('/node_modules/.vite/deps/cesium.js');
|
||||
viewer.camera.flyTo({
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: C.Cartesian3.fromDegrees(-0.1276, 51.5072, 3200),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
const deadline = Date.now() + 45000;
|
||||
while (londonRequests < 2 && Date.now() < deadline) await sleep(500);
|
||||
await until(() => londonRequests >= 2, {
|
||||
timeoutMs: 45000,
|
||||
label: 'London retry',
|
||||
});
|
||||
assert.ok(
|
||||
londonRequests >= 2,
|
||||
'London must recover its failed request without a toggle',
|
||||
@@ -121,5 +120,5 @@ try {
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
import {
|
||||
moveCamera,
|
||||
createScreencast,
|
||||
@@ -20,12 +21,10 @@ const modes = modeArg ? [modeArg] : ['tomtom', 'osm', 'hybrid'];
|
||||
const out = `qa-shots/oblique-${baseline ? 'before' : 'after'}`;
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
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,
|
||||
protocolTimeout: 180000,
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
@@ -59,12 +58,10 @@ try {
|
||||
const url = new URL(args.find((a) => /^https?:/.test(a)));
|
||||
url.searchParams.set('trafficRoads', mode);
|
||||
await page.goto(url.href, { 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,
|
||||
);
|
||||
@@ -74,18 +71,19 @@ try {
|
||||
for (const [id] of g.dataManager.layers)
|
||||
await g.dataManager.setEnabled(id, false);
|
||||
g.styleManager.setDetection({ enabled: false });
|
||||
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
const snap = () =>
|
||||
page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
let upper = 0,
|
||||
lower = 0;
|
||||
dataManager.layers
|
||||
.get('traffic')
|
||||
.module.visitMotionDots((id, key, p) => {
|
||||
if (!p.show) return;
|
||||
const s = viewer.scene.cartesianToCanvasCoordinates(p.position);
|
||||
const s = window.__godsEyeView.engine.projector.worldToWindow(
|
||||
p.position,
|
||||
);
|
||||
if (s && s.x >= 240 && s.x <= 1040 && s.y >= 0 && s.y <= 800) {
|
||||
if (s.y < 400) upper++;
|
||||
else lower++;
|
||||
@@ -98,7 +96,7 @@ try {
|
||||
};
|
||||
});
|
||||
const settle = async () => {
|
||||
await sleep(700);
|
||||
await whenSettled(page);
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
@@ -110,7 +108,7 @@ try {
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.catch(() => {});
|
||||
await sleep(1200);
|
||||
await whenSettled(page);
|
||||
return snap();
|
||||
};
|
||||
for (const [name, lat, lon, height] of [
|
||||
@@ -186,20 +184,22 @@ try {
|
||||
mode === 'hybrid' ? await createScreencast(page, dir, { fps: 24 }) : null;
|
||||
await installFrameProbe(page);
|
||||
await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
window.__obliqueFrames = [];
|
||||
window.__obliqueMoving = true;
|
||||
viewer.scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
const s = dataManager.layers.get('traffic').module.getStats();
|
||||
let upper = 0,
|
||||
lower = 0,
|
||||
near = 0;
|
||||
const camera = viewer.camera.positionWC;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().position;
|
||||
dataManager.layers
|
||||
.get('traffic')
|
||||
.module.visitMotionDots((id, key, p) => {
|
||||
if (!p.show) return;
|
||||
const q = viewer.scene.cartesianToCanvasCoordinates(p.position);
|
||||
const q = window.__godsEyeView.engine.projector.worldToWindow(
|
||||
p.position,
|
||||
);
|
||||
if (q && q.x >= 240 && q.x <= 1040 && q.y >= 0 && q.y <= 800) {
|
||||
if (q.y < 400) upper++;
|
||||
else lower++;
|
||||
@@ -251,7 +251,7 @@ try {
|
||||
window.__obliqueMoving = false;
|
||||
});
|
||||
const panned = await settle();
|
||||
await sleep(1800);
|
||||
await whenSettled(page);
|
||||
const frames = await page.evaluate(() => window.__obliqueFrames);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('traffic', false),
|
||||
@@ -304,7 +304,7 @@ try {
|
||||
}
|
||||
} finally {
|
||||
fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (
|
||||
!baseline &&
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenTilesLoaded, whenUiSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-traffic-preset-ab.mjs — A/B screenshot capture for preset-aware
|
||||
* traffic dot styling (owner field finding 2026-07-23: NVG/FLIR/CRT
|
||||
@@ -23,7 +25,7 @@
|
||||
* param not adopted by the layer).
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -69,52 +71,20 @@ const STYLES = [
|
||||
{ id: 'retro', name: 'retro', label: 'CRT', profile: 'crt' },
|
||||
];
|
||||
|
||||
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);
|
||||
const findChromeExecutable = () =>
|
||||
CHROME_EXECUTABLE_CANDIDATES.find((c) => { try { return fs.existsSync(c); } catch { return false; } }) || null;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** Same pattern as qa-traffic-jamviz-ab: wait for the 3D tileset to settle. */
|
||||
function waitForTilesLoaded(page, timeoutMs) {
|
||||
return page.waitForFunction(
|
||||
() => {
|
||||
const prims = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let i = 0; i < prims.length; i++) {
|
||||
const p = prims.get(i);
|
||||
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
|
||||
return p.tilesLoaded === true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: timeoutMs }
|
||||
).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
/** Enable traffic + teleport, then poll the layer until settled. */
|
||||
function settleTraffic(page, view, { minCount = 100, timeoutS = 45 } = {}) {
|
||||
return page.evaluate(async (v, minC, tS) => {
|
||||
return evaluateWithGeo(page, async (qaGeo, v, minC, tS) => {
|
||||
const gev = window.__godsEyeView;
|
||||
await gev.dataManager.setEnabled('traffic', true);
|
||||
const mod = gev.dataManager.layers.get('traffic').module;
|
||||
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
const ell = gev.viewer.scene.globe.ellipsoid;
|
||||
try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
const d2r = Math.PI / 180;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ell.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: v.lon * d2r, latitude: v.lat * d2r, height: v.height,
|
||||
}),
|
||||
orientation: {
|
||||
@@ -161,15 +131,9 @@ async function main() {
|
||||
}
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
|
||||
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=1600,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=1600,900'],
|
||||
});
|
||||
|
||||
const manifest = [];
|
||||
@@ -180,10 +144,10 @@ async function main() {
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
|
||||
let firstView = true;
|
||||
for (const view of VIEWS) {
|
||||
@@ -201,7 +165,7 @@ async function main() {
|
||||
exitCode = 1;
|
||||
continue;
|
||||
}
|
||||
await waitForTilesLoaded(page, firstView ? 30000 : 15000);
|
||||
await whenTilesLoaded(page, { timeoutMs: firstView ? 30000 : 15000 }).then(() => true).catch(() => false);
|
||||
firstView = false;
|
||||
|
||||
for (const style of STYLES) {
|
||||
@@ -213,7 +177,7 @@ async function main() {
|
||||
sm.setStyle(s.name, { applyPreset: true });
|
||||
if (sm.stages.thermal) sm.stages.thermal.uniforms.palette = s.ironbow ? 1.0 : 0.0;
|
||||
}, style);
|
||||
await sleep(1800); // 500 ms intensity lerp + settle
|
||||
await whenUiSettled(page); // 500 ms intensity lerp + settle
|
||||
|
||||
// Round 2 legs: dots alone (DETECT off) vs dots + bucket-colored
|
||||
// brackets (DENSE) — presetDots stays at its default 'on'.
|
||||
@@ -221,7 +185,7 @@ async function main() {
|
||||
await page.evaluate((d) => {
|
||||
window.__godsEyeView.styleManager._setDetectionMode(d === 'on' ? 'DENSE' : 'OFF');
|
||||
}, detect);
|
||||
await sleep(1200); // label-arbiter solve + a few rendered frames
|
||||
await whenUiSettled(page); // label-arbiter solve + a few rendered frames
|
||||
|
||||
const final = await page.evaluate(() => {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('traffic').module;
|
||||
@@ -272,14 +236,14 @@ async function main() {
|
||||
sm._setDetectionMode('OFF');
|
||||
if (sm.stages.thermal) sm.stages.thermal.uniforms.palette = 0.0;
|
||||
});
|
||||
await sleep(1000);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Harness error:', e);
|
||||
exitCode = 3;
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`\nShots + manifest.json → ${OUT_DIR}`);
|
||||
process.exit(exitCode);
|
||||
|
||||
+20
-48
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-traffic.mjs — headless proof for the TomTom live-flow traffic layer.
|
||||
*
|
||||
@@ -30,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 { captureQaScreenshot } from './qa-chrome.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -52,26 +54,7 @@ const HEADFUL = argv.includes('--headful');
|
||||
const FIXTURES = argv.includes('--fixtures');
|
||||
const { installTrafficRoutes } = await import('./traffic-fixtures.mjs');
|
||||
|
||||
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) {
|
||||
@@ -79,7 +62,6 @@ function record(name, ok, detail) {
|
||||
const tag = ok === null ? '\x1b[33mINCONCLUSIVE\x1b[0m' : ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
||||
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** Enable traffic + teleport, then poll the layer until settled. */
|
||||
async function settleTraffic(page, view, { minCount = 1, timeoutS = 30 } = {}) {
|
||||
@@ -87,19 +69,18 @@ async function settleTraffic(page, view, { minCount = 1, timeoutS = 30 } = {}) {
|
||||
// changes do not activate a headful window the way a real UI click does.
|
||||
await page.bringToFront();
|
||||
await page.waitForFunction(() => document.visibilityState === 'visible');
|
||||
return page.evaluate(async (v, minC, tS) => {
|
||||
return evaluateWithGeo(page, async (qaGeo, v, minC, tS) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const dm = gev.dataManager;
|
||||
await dm.setEnabled('traffic', true);
|
||||
const mod = dm.layers.get('traffic').module;
|
||||
const beforeUpdate = mod.getStats().lastUpdate;
|
||||
const ell = gev.viewer.scene.globe.ellipsoid;
|
||||
const d2r = Math.PI / 180;
|
||||
// The app's intro flyTo animation clobbers a setView issued mid-flight —
|
||||
// cancel any active tween before teleporting.
|
||||
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight active */ }
|
||||
gev.viewer.camera.setView({
|
||||
destination: ell.cartographicToCartesian({ longitude: v.lon * d2r, latitude: v.lat * d2r, height: v.height }),
|
||||
try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* no flight active */ }
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({ longitude: v.lon * d2r, latitude: v.lat * d2r, height: v.height }),
|
||||
orientation: { heading: (v.heading || 0) * d2r, pitch: (v.pitch ?? -90) * d2r, roll: 0 },
|
||||
});
|
||||
let s = null;
|
||||
@@ -139,17 +120,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',
|
||||
...(!HEADFUL ? ['--use-gl=angle', '--use-angle=swiftshader'] : []),
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--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', ...(!HEADFUL ? [] : []), '--disable-backgrounding-occluded-windows', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1440,900'],
|
||||
});
|
||||
|
||||
let exitCode = 0;
|
||||
@@ -174,10 +147,10 @@ async function main() {
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await sleep(5000);
|
||||
await whenSettled(page);
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// ── (i) LIVE mode: San Antonio — fast Overpass extract, partial TomTom
|
||||
@@ -201,23 +174,22 @@ async function main() {
|
||||
mumbai.flowCoveragePct > 0 && mumbai.tilesFetched > 0,
|
||||
`coverage=${mumbai.flowCoveragePct}% tiles=${mumbai.tilesFetched}`);
|
||||
if (mumbai.mode !== 'live' || !(mumbai.count > 0) || !(colored > 0)) exitCode = 1;
|
||||
await sleep(1200);
|
||||
await whenSettled(page);
|
||||
await captureQaScreenshot(page, { path: path.join(SHOTS_DIR, 'traffic-live-flow.png') }, { headful: HEADFUL });
|
||||
}
|
||||
|
||||
// ── (iv) uncovered-roads param — hide vs sim ─────────────────────────────
|
||||
console.log('\n(iv) uncoveredRoads param — hide vs sim...');
|
||||
if ((mumbai.flowBuckets?.sim || 0) > 0) {
|
||||
const hid = await page.evaluate(async () => {
|
||||
const hid = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const mod = gev.dataManager.layers.get('traffic').module;
|
||||
const before = mod.getStats().lastUpdate;
|
||||
mod.setParams({ uncoveredRoads: 'hide' });
|
||||
const ell = gev.viewer.scene.globe.ellipsoid;
|
||||
const d2r = Math.PI / 180;
|
||||
// Shift far enough to defeat the overlap gate and force a re-render.
|
||||
gev.viewer.camera.setView({
|
||||
destination: ell.cartographicToCartesian({ longitude: -98.51 * d2r, latitude: 29.435 * d2r, height: 2800 }),
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({ longitude: -98.51 * d2r, latitude: 29.435 * d2r, height: 2800 }),
|
||||
orientation: { heading: 0.4, pitch: -1.25, roll: 0 },
|
||||
});
|
||||
// Poll for a NEW render (lastUpdate changes) — the pre-shift stats
|
||||
@@ -289,10 +261,10 @@ async function main() {
|
||||
});
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await sleep(5000);
|
||||
await whenSettled(page);
|
||||
await page.keyboard.press('Escape');
|
||||
let simStats = await settleTraffic(page, { lon: -98.4936, lat: 29.4241, height: 3000 }, { minCount: 100, timeoutS: 45 });
|
||||
if (!simStats || simStats.count === 0) {
|
||||
@@ -316,7 +288,7 @@ async function main() {
|
||||
if (simStats.mode !== 'sim' || !allWhite || keylessFlowReqs.length !== 0 || !keylessHonest) {
|
||||
exitCode = 1;
|
||||
}
|
||||
await sleep(1000);
|
||||
await whenFramesDrawn(page);
|
||||
await captureQaScreenshot(page, { path: path.join(SHOTS_DIR, 'traffic-sim-keyless.png') }, { headful: HEADFUL });
|
||||
}
|
||||
record('BROWSER: no uncaught page exceptions', pageErrors.length === 0, pageErrors.join('; '));
|
||||
@@ -324,7 +296,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,3 +1,4 @@
|
||||
import { whenFramesDrawn } from './qa/ready.mjs';
|
||||
export function transitQaBase(value) {
|
||||
if (!value) throw new Error('Transit QA requires an explicit QA_BASE_URL');
|
||||
const url = new URL(value);
|
||||
@@ -21,12 +22,15 @@ export function boundPageEvaluations(page, defaultTimeoutMs = 30000) {
|
||||
);
|
||||
const label = String(fn).slice(0, 120).replace(/\s+/g, ' ');
|
||||
return evaluate(
|
||||
async (source, values, timeoutMs, label) => {
|
||||
async (source, values, timeoutMs, label, isFunction) => {
|
||||
let timer;
|
||||
try {
|
||||
return await Promise.race([
|
||||
// Puppeteer callbacks are standalone functions, just as in evaluate.
|
||||
Promise.resolve().then(() => (0, eval)(`(${source})`)(...values)),
|
||||
Promise.resolve().then(() => {
|
||||
const result = (0, eval)(`(${source})`);
|
||||
return isFunction ? result(...values) : result;
|
||||
}),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() =>
|
||||
@@ -47,6 +51,7 @@ export function boundPageEvaluations(page, defaultTimeoutMs = 30000) {
|
||||
args,
|
||||
timeoutMs,
|
||||
label,
|
||||
typeof fn === 'function',
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -62,8 +67,8 @@ export async function reportTransitVisibility(page, label) {
|
||||
visible: state._visible.size,
|
||||
moving: state._moving.size,
|
||||
held: state._renderHeld,
|
||||
requestRenderMode: app.viewer.scene.requestRenderMode,
|
||||
renderLoop: app.viewer.useDefaultRenderLoop,
|
||||
requestRenderMode: !app.engine.render.isContinuous(),
|
||||
renderLoop: !app.engine.isDestroyed(),
|
||||
renderErrors: window.__transitRenderErrors || [],
|
||||
bounds: state._viewBounds,
|
||||
hidden: layer._transitVisibilityForTest().filter((e) => !e.shown),
|
||||
@@ -76,13 +81,14 @@ export async function reportTransitVisibility(page, label) {
|
||||
);
|
||||
if (result.renderLoop === false || result.renderErrors.length)
|
||||
throw new Error(
|
||||
`${label}: Cesium render loop stopped or raised an error: ${JSON.stringify(result.renderErrors)}`,
|
||||
`${label}: render loop stopped or raised an error: ${JSON.stringify(result.renderErrors)}`,
|
||||
);
|
||||
await whenFramesDrawn(page);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Intersect primed histories with the new snapshot; disappearance is availability. */
|
||||
export async function restoreRetainedTransit(retained) {
|
||||
export async function restoreRetainedTransit(qaGeo, retained) {
|
||||
if (!retained.length)
|
||||
return {
|
||||
error: 'No multi-fix history observed during the probe',
|
||||
@@ -117,9 +123,8 @@ export async function restoreRetainedTransit(retained) {
|
||||
const entry = state._vehicles.get(
|
||||
`${history.feedId || 'mbta'}:${history.vehicleId}`,
|
||||
);
|
||||
const camera = app.viewer.camera,
|
||||
C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(entry.record.lon, entry.record.lat, 600),
|
||||
),
|
||||
@@ -173,14 +178,12 @@ export async function sampleTransitPixels(page, palette) {
|
||||
);
|
||||
}, 5000);
|
||||
const remove =
|
||||
window.__godsEyeView.viewer.scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest();
|
||||
const scene = app.viewer.scene;
|
||||
const canvas = scene.canvas;
|
||||
const view = scene.camera.viewMatrix;
|
||||
const proj = scene.camera.frustum.projectionMatrix;
|
||||
const { engine } = app;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const mul = (m, v) => [
|
||||
m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3],
|
||||
m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3],
|
||||
@@ -269,9 +272,12 @@ export async function sampleTransitPixels(page, palette) {
|
||||
for (const entry of state._vehicles.values()) {
|
||||
if (!entry.marker || entry.marker.show === false) continue;
|
||||
const p = entry.marker.position;
|
||||
const clip = mul(proj, mul(view, [p.x, p.y, p.z, 1]));
|
||||
const clip = mul(
|
||||
window.__godsEyeView.engine.projector.viewProjection(),
|
||||
[p.x, p.y, p.z, 1],
|
||||
);
|
||||
if (!(clip[3] > 0)) continue;
|
||||
const screen = entry.marker.computeScreenSpacePosition?.(scene) || {
|
||||
const screen = {
|
||||
x: ((clip[0] / clip[3]) * 0.5 + 0.5) * width,
|
||||
y: (1 - ((clip[1] / clip[3]) * 0.5 + 0.5)) * height,
|
||||
};
|
||||
@@ -492,7 +498,7 @@ export async function sampleTransitPixels(page, palette) {
|
||||
// label or sprite must not masquerade as a dim sensor signature.
|
||||
// Check the whole sampled core, not just its centre pick.
|
||||
let coreOwned = true;
|
||||
if (scene.pick) {
|
||||
{
|
||||
const framebuffer = gl.getParameter?.(gl.FRAMEBUFFER_BINDING);
|
||||
for (const core of corePoints) {
|
||||
let u = core.x / width,
|
||||
@@ -519,13 +525,12 @@ export async function sampleTransitPixels(page, palette) {
|
||||
v) *
|
||||
intensity;
|
||||
}
|
||||
const picked = scene.pick(
|
||||
const picked = engine.picker.pick(
|
||||
{ x: u * width, y: (1 - v) * height },
|
||||
1,
|
||||
1,
|
||||
);
|
||||
if (picked?.id !== s.key && picked?.primitive?.id !== s.key)
|
||||
coreOwned = false;
|
||||
if (picked?.id !== s.key) coreOwned = false;
|
||||
}
|
||||
if (gl.bindFramebuffer)
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
@@ -657,7 +662,7 @@ export async function sampleTransitPixels(page, palette) {
|
||||
);
|
||||
resolve(out);
|
||||
});
|
||||
window.__godsEyeView.viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
}, palette);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
import { whenSettled, whenUiSettled } from './qa/ready.mjs';
|
||||
export const BOSTON = {
|
||||
id: 'boston',
|
||||
feedId: 'mbta',
|
||||
@@ -69,7 +69,7 @@ export function chooseLiveCities(now = new Date()) {
|
||||
* @param {Record<string, number>} [params] aria-label → slider value
|
||||
* @returns {Promise<string>} how the style was set
|
||||
*/
|
||||
export async function selectStyle(page, name, params = {}, wait = pause) {
|
||||
export async function selectStyle(page, name, params = {}) {
|
||||
let how = 'button';
|
||||
// The presets live in the bottom control panel, which auto-dismisses when
|
||||
// the pointer leaves it: pin it open the way a user does, once.
|
||||
@@ -86,7 +86,7 @@ export async function selectStyle(page, name, params = {}, wait = pause) {
|
||||
{ explicit: true },
|
||||
);
|
||||
});
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
try {
|
||||
await page.$eval(`.style-btn[data-style="${name}"]`, (el) =>
|
||||
el.scrollIntoView({ block: 'center' }),
|
||||
@@ -118,7 +118,7 @@ export async function selectStyle(page, name, params = {}, wait = pause) {
|
||||
{ timeout: 10000 },
|
||||
name,
|
||||
);
|
||||
await wait(1_800);
|
||||
await whenUiSettled(page);
|
||||
for (const [label, value] of Object.entries(params)) {
|
||||
const selector = `#param-sliders input.param-slider[aria-label="${label}"]`;
|
||||
await page.waitForSelector(selector, { timeout: 10000 });
|
||||
@@ -130,7 +130,7 @@ export async function selectStyle(page, name, params = {}, wait = pause) {
|
||||
},
|
||||
value,
|
||||
);
|
||||
await wait(700);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
return how;
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export async function selectStyle(page, name, params = {}, wait = pause) {
|
||||
* @param {import('puppeteer').Page} page
|
||||
* @returns {Promise<string>} the profile reached
|
||||
*/
|
||||
export async function detectOn(page, wait = pause) {
|
||||
export async function detectOn(page) {
|
||||
// The real button toggles OFF/restore. Density determines its on-profile.
|
||||
const mode = () =>
|
||||
page.evaluate(() => {
|
||||
@@ -153,11 +153,7 @@ export async function detectOn(page, wait = pause) {
|
||||
};
|
||||
});
|
||||
// The loading screen swallows the first click of a fresh page.
|
||||
await page
|
||||
.waitForFunction(() => !document.getElementById('loading-screen'), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.catch(() => {});
|
||||
await whenSettled(page);
|
||||
// The DISPLAY rail starts collapsed on a first run: open it the way a user
|
||||
// does, then press DETECT until it reports on.
|
||||
await page.evaluate(() => {
|
||||
@@ -175,7 +171,7 @@ export async function detectOn(page, wait = pause) {
|
||||
);
|
||||
if (!(await mode()).on) {
|
||||
await page.click('#detection-toggle');
|
||||
await wait(300);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
await page.$eval('#detection-density-slider', (el) => {
|
||||
el.value = '100';
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { hookRenderErrors } from './qa-browserEvidence.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { transitQaBase } from './qa-transit-browser.mjs';
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
@@ -6,18 +9,18 @@ import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
/** Prove parked and moving headings in the real scene, without live feeds. */
|
||||
export async function runTransitHeadingRegression(base, check) {
|
||||
base = transitQaBase(base);
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
executablePath: chromeExecutablePath(),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
...(process.platform === 'darwin' ? ['--use-angle=metal'] : []),
|
||||
...(process.platform === 'darwin' ? [] : []),
|
||||
'--enable-gpu',
|
||||
],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await hookRenderErrors(page);
|
||||
await blockPaidQaMaps(page);
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
await page.goto(
|
||||
@@ -28,7 +31,7 @@ export async function runTransitHeadingRegression(base, check) {
|
||||
() => window.__godsEyeView?.dataManager?.layers?.has('transit'),
|
||||
{ timeout: 90000 },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const app = window.__godsEyeView;
|
||||
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
|
||||
@@ -63,26 +66,25 @@ export async function runTransitHeadingRegression(base, check) {
|
||||
parts.rendering.schedulePlayback(entry);
|
||||
}
|
||||
parts.rendering.syncRenderHold();
|
||||
window.__headingErrors = [];
|
||||
app.viewer.scene.renderError.addEventListener((scene, error) =>
|
||||
window.__headingErrors.push(error.message),
|
||||
);
|
||||
window.__headingErrors = window.__qaRenderErrors;
|
||||
});
|
||||
const pose = async (heading) => {
|
||||
await page.evaluate((heading) => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(-71.0589, 42.3601, 1200)),
|
||||
orientation: { heading, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, heading);
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, heading) => {
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(-71.0589, 42.3601, 1200)),
|
||||
orientation: { heading, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
},
|
||||
heading,
|
||||
);
|
||||
await whenSettled(page);
|
||||
};
|
||||
const sample = () =>
|
||||
page.evaluate(async () => {
|
||||
const app = window.__godsEyeView,
|
||||
scene = app.viewer.scene;
|
||||
const app = window.__godsEyeView;
|
||||
const state = app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitStateForTest();
|
||||
@@ -96,12 +98,18 @@ export async function runTransitHeadingRegression(base, check) {
|
||||
);
|
||||
const project = (p) => {
|
||||
const clip = mul(
|
||||
scene.camera.frustum.projectionMatrix,
|
||||
mul(scene.camera.viewMatrix, [p.x, p.y, p.z, 1]),
|
||||
window.__godsEyeView.engine.projector.viewProjection(),
|
||||
[p.x, p.y, p.z, 1],
|
||||
);
|
||||
return {
|
||||
x: ((clip[0] / clip[3] + 1) * scene.canvas.clientWidth) / 2,
|
||||
y: ((1 - clip[1] / clip[3]) * scene.canvas.clientHeight) / 2,
|
||||
x:
|
||||
((clip[0] / clip[3] + 1) *
|
||||
window.__godsEyeView.engine.host.canvas.clientWidth) /
|
||||
2,
|
||||
y:
|
||||
((1 - clip[1] / clip[3]) *
|
||||
window.__godsEyeView.engine.host.canvas.clientHeight) /
|
||||
2,
|
||||
};
|
||||
};
|
||||
return [0, 1, 2, 3].map((i) => {
|
||||
@@ -139,7 +147,7 @@ export async function runTransitHeadingRegression(base, check) {
|
||||
'unknown parked course remains screen-up',
|
||||
before[2].rotation === 0 && after[2].rotation === 0,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
await whenSettled(page);
|
||||
const moved = (await sample())[3];
|
||||
const dx = moved.x - after[3].x,
|
||||
dy = moved.y - after[3].y;
|
||||
@@ -160,6 +168,6 @@ export async function runTransitHeadingRegression(base, check) {
|
||||
(await page.evaluate(() => window.__headingErrors)).length === 0,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Camera-jump regression against an existing server; never starts a server. */
|
||||
import puppeteer from 'puppeteer';
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { chromeExecutablePath, captureQaScreenshot } from './qa-chrome.mjs';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { reduceTrailPixels } from '../src/layers/transit/qaMetrics.js';
|
||||
@@ -9,11 +11,10 @@ import { transitQaBase } from './qa-transit-browser.mjs';
|
||||
const base = transitQaBase(process.env.QA_BASE_URL);
|
||||
const shots = process.env.QA_SHOTS || 'qa-shots/transit-recovery';
|
||||
await mkdir(shots, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
executablePath: chromeExecutablePath(),
|
||||
args: [
|
||||
...(process.platform === 'darwin' ? ['--use-angle=metal'] : []),
|
||||
...(process.platform === 'darwin' ? [] : []),
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -35,28 +36,16 @@ try {
|
||||
const app = window.__godsEyeView;
|
||||
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
return scene.globe.tilesLoaded && scene.globe.show;
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return !cover || cover.classList.contains('hidden');
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const app = window.__godsEyeView,
|
||||
camera = app.viewer.camera;
|
||||
const C = camera.positionCartographic.constructor,
|
||||
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||
await whenSettled(page);
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const C = qaGeo.cartographic,
|
||||
lat = 30.267 + 90 / 111320;
|
||||
const floor =
|
||||
app.viewer.scene.globe.getHeight(C.fromDegrees(-97.7431, lat)) || 0;
|
||||
camera.setView({
|
||||
window.__godsEyeView.engine.surface.terrainHeight(
|
||||
C.fromDegrees(-97.7431, lat),
|
||||
) || 0;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(
|
||||
-97.7431,
|
||||
@@ -67,29 +56,24 @@ try {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 3, roll: 0 },
|
||||
});
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.viewer.scene.globe.tilesLoaded,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const result = await page.evaluate(async () => {
|
||||
const app = window.__godsEyeView,
|
||||
scene = app.viewer.scene,
|
||||
camera = app.viewer.camera;
|
||||
await whenSettled(page);
|
||||
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||
await whenSettled(page);
|
||||
const result = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest(),
|
||||
parts = layer._transitPartsForTest();
|
||||
const { getRegisteredTransitFeed } =
|
||||
await import('/src/data/transitFeeds.js');
|
||||
const { setRate } = await import('/src/data/contactPlayback.js');
|
||||
const C = camera.positionCartographic.constructor;
|
||||
const C = qaGeo.cartographic;
|
||||
const feed = {
|
||||
...getRegisteredTransitFeed('capmetro-austin'),
|
||||
id: 'qa-camera-recovery',
|
||||
};
|
||||
const aim = (lat, height = 600) =>
|
||||
camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(
|
||||
-97.7431,
|
||||
@@ -161,7 +145,7 @@ try {
|
||||
const preparedAtSelection = !!parts.trails.diagnostics()?.body;
|
||||
parts.height.requestHeight = requestHeight;
|
||||
const floor =
|
||||
scene.globe.getHeight(
|
||||
window.__godsEyeView.engine.surface.terrainHeight(
|
||||
C.fromDegrees(entry.sample.lon, entry.sample.lat),
|
||||
) || 0;
|
||||
aim(entry.sample.lat, floor + 600);
|
||||
@@ -170,7 +154,7 @@ try {
|
||||
while (performance.now() - started < 1400) {
|
||||
trail = parts.trails.diagnostics();
|
||||
if (entry.marker.show && trail?.body?.show && trail.body.ready) break;
|
||||
scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await wait(25);
|
||||
}
|
||||
const status = {
|
||||
@@ -213,8 +197,8 @@ try {
|
||||
const lengths = [0];
|
||||
for (let i = 1; i < points.length; i++)
|
||||
lengths.push(lengths.at(-1) + V.distance(points[i - 1], points[i]));
|
||||
const canvas = scene.canvas,
|
||||
gl = scene.context._gl,
|
||||
const canvas = window.__godsEyeView.engine.host.canvas,
|
||||
gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2'),
|
||||
scale = canvas.width / canvas.clientWidth;
|
||||
const mul = (m, v) =>
|
||||
[0, 1, 2, 3].map(
|
||||
@@ -222,10 +206,12 @@ try {
|
||||
m[i] * v[0] + m[i + 4] * v[1] + m[i + 8] * v[2] + m[i + 12] * v[3],
|
||||
);
|
||||
const project = (p) => {
|
||||
const q = mul(
|
||||
camera.frustum.projectionMatrix,
|
||||
mul(camera.viewMatrix, [p.x, p.y, p.z, 1]),
|
||||
);
|
||||
const q = mul(window.__godsEyeView.engine.projector.viewProjection(), [
|
||||
p.x,
|
||||
p.y,
|
||||
p.z,
|
||||
1,
|
||||
]);
|
||||
return {
|
||||
x: ((q[0] / q[3]) * 0.5 + 0.5) * canvas.clientWidth,
|
||||
y: (0.5 - (q[1] / q[3]) * 0.5) * canvas.clientHeight,
|
||||
@@ -242,7 +228,8 @@ try {
|
||||
(d - lengths[j - 1]) / Math.max(0.001, lengths[j] - lengths[j - 1]),
|
||||
);
|
||||
const cart = C.fromCartesian(p);
|
||||
cart.height = scene.globe.getHeight(cart) || 0;
|
||||
cart.height =
|
||||
window.__godsEyeView.engine.surface.terrainHeight(cart) || 0;
|
||||
return {
|
||||
...project(C.toCartesian(cart)),
|
||||
radius: (Math.SQRT2 * Math.ceil(3 * scale)) / scale,
|
||||
@@ -282,18 +269,20 @@ try {
|
||||
remove();
|
||||
reject(new Error('No recovery frame'));
|
||||
}, 500);
|
||||
const remove = scene.postRender.addEventListener(() => {
|
||||
remove();
|
||||
clearTimeout(timer);
|
||||
resolve(read());
|
||||
});
|
||||
scene.requestRender();
|
||||
const remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
remove();
|
||||
clearTimeout(timer);
|
||||
resolve(read());
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
let visible = true;
|
||||
// Body and head together, as the trail's own visibility switch does.
|
||||
const remove = scene.preRender.addEventListener(() =>
|
||||
trailRenderer.setVisible(visible),
|
||||
);
|
||||
const remove =
|
||||
window.__godsEyeView.engine.frames.preRender.addEventListener(() =>
|
||||
trailRenderer.setVisible(visible),
|
||||
);
|
||||
const captures = [];
|
||||
try {
|
||||
for (const show of [false, true, false, true]) {
|
||||
@@ -316,7 +305,7 @@ try {
|
||||
offAgain,
|
||||
onAgain,
|
||||
tilesReady:
|
||||
scene.globe.tilesLoaded &&
|
||||
window.__godsEyeView.engine.surface.tilesLoaded() &&
|
||||
parts.trails.diagnostics().body === trail.body,
|
||||
elapsedMs: performance.now() - started,
|
||||
// The trail's ground corridors (age bands and head, each with a backing).
|
||||
@@ -350,5 +339,5 @@ try {
|
||||
console.log(JSON.stringify(result));
|
||||
process.exitCode = pass ? 0 : 1;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
+340
-305
@@ -1,3 +1,6 @@
|
||||
// Cesium-only probes: native globe and ground-primitive coverage are the Cesium visual oracle.
|
||||
import { whenTilesLoaded, whenFramesDrawn } from './qa/ready.mjs';
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { captureQaScreenshot } from './qa-chrome.mjs';
|
||||
import { getRegisteredTransitFeed } from '../src/data/transitFeeds.js';
|
||||
|
||||
@@ -89,24 +92,27 @@ export async function runFleetBudgets({
|
||||
const cdp = await page.createCDPSession();
|
||||
await cdp.send('HeapProfiler.enable');
|
||||
for (const count of [800, 3000]) {
|
||||
await page.evaluate(async (count) => {
|
||||
const app = window.__godsEyeView;
|
||||
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
|
||||
const camera = app.viewer.camera,
|
||||
C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(-71.0605, 42.3554, count === 800 ? 600 : 30000),
|
||||
),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
layer._transitPartsForTest().ingestion.abortAllInFlight();
|
||||
layer._transitStateForTest()._activeFeeds.clear();
|
||||
for (const key of layer._transitStateForTest()._vehicles.keys())
|
||||
layer._transitPartsForTest().ingestion.removeVehicle(key);
|
||||
}, count);
|
||||
await wait(8000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, count) => {
|
||||
const app = window.__godsEyeView;
|
||||
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(-71.0605, 42.3554, count === 800 ? 600 : 30000),
|
||||
),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
layer._transitPartsForTest().ingestion.abortAllInFlight();
|
||||
layer._transitStateForTest()._activeFeeds.clear();
|
||||
for (const key of layer._transitStateForTest()._vehicles.keys())
|
||||
layer._transitPartsForTest().ingestion.removeVehicle(key);
|
||||
},
|
||||
count,
|
||||
);
|
||||
await whenTilesLoaded(page);
|
||||
// Camera proximity can finish a live poll during tile warm-up. Remove it
|
||||
// before the heap baseline, or its memory would discount the fixture fleet.
|
||||
await page.evaluate(() => {
|
||||
@@ -121,9 +127,9 @@ export async function runFleetBudgets({
|
||||
for (const key of state._vehicles.keys())
|
||||
parts.ingestion.removeVehicle(key);
|
||||
parts.rendering.syncRenderHold();
|
||||
window.__godsEyeView.viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await wait(500);
|
||||
await whenFramesDrawn(page);
|
||||
await cdp.send('HeapProfiler.collectGarbage');
|
||||
const before = await cdp.send('Runtime.getHeapUsage');
|
||||
const loaded = await page.evaluate(
|
||||
@@ -141,7 +147,7 @@ export async function runFleetBudgets({
|
||||
page,
|
||||
`${count} budget visibility after load`,
|
||||
);
|
||||
await wait(2000);
|
||||
await whenFramesDrawn(page);
|
||||
await cdp.send('HeapProfiler.collectGarbage');
|
||||
// The retained delta is conservative: it includes concurrent page growth.
|
||||
const retained = await cdp.send('Runtime.getHeapUsage');
|
||||
@@ -156,7 +162,7 @@ export async function runFleetBudgets({
|
||||
.get('transit')
|
||||
.module._resetTransitFixtureClockForTest(),
|
||||
);
|
||||
await wait(1500);
|
||||
await whenFramesDrawn(page);
|
||||
await cdp.send('HeapProfiler.startSampling', {
|
||||
samplingInterval: 128,
|
||||
includeObjectsCollectedByMajorGC: true,
|
||||
@@ -315,15 +321,18 @@ export async function runBostonMatrix({
|
||||
await selectStyle(page, 'normal');
|
||||
for (const candidate of group.candidates) {
|
||||
background = { ...candidate, name: group.name };
|
||||
await page.evaluate((b) => {
|
||||
const camera = window.__godsEyeView.viewer.camera,
|
||||
C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(b.lon, b.lat, 600)),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, background);
|
||||
await wait(8000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, b) => {
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(b.lon, b.lat, 600)),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
},
|
||||
background,
|
||||
);
|
||||
await whenTilesLoaded(page);
|
||||
await page.evaluate(
|
||||
({ background, routes }) => {
|
||||
const layer =
|
||||
@@ -336,7 +345,7 @@ export async function runBostonMatrix({
|
||||
},
|
||||
{ background, routes },
|
||||
);
|
||||
await wait(1000);
|
||||
await whenFramesDrawn(page);
|
||||
backgroundResult = reduceBackground(
|
||||
group.name,
|
||||
await sampleRendered(page),
|
||||
@@ -352,7 +361,7 @@ export async function runBostonMatrix({
|
||||
);
|
||||
for (const look of looks) {
|
||||
await selectStyle(page, look.style, look.params);
|
||||
await wait(1200);
|
||||
await whenFramesDrawn(page);
|
||||
await reportTransitVisibility(
|
||||
page,
|
||||
`Boston ${background.name}/${look.name} visibility`,
|
||||
@@ -407,7 +416,7 @@ export async function runBostonMatrix({
|
||||
.selection.selectVehicle(key),
|
||||
target.key,
|
||||
);
|
||||
await wait(500);
|
||||
await whenFramesDrawn(page);
|
||||
await captureQaScreenshot(page, {
|
||||
path: `${shots}/${tag}-matrix-${background.name}-${look.name}-${target.route}-selected.jpg`,
|
||||
type: 'jpeg',
|
||||
@@ -431,7 +440,7 @@ export async function runBostonMatrix({
|
||||
restored.color === target.color,
|
||||
);
|
||||
}
|
||||
await wait(300);
|
||||
await whenFramesDrawn(page);
|
||||
await captureQaScreenshot(page, {
|
||||
path: `${shots}/${tag}-matrix-${background.name}-${look.name}-restored.jpg`,
|
||||
type: 'jpeg',
|
||||
@@ -464,12 +473,11 @@ export async function runBostonLive({
|
||||
sampleRendered,
|
||||
}) {
|
||||
console.log('\n== Boston live oblique streets ==');
|
||||
await page.evaluate(async () => {
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const app = window.__godsEyeView;
|
||||
await app.dataManager.setEnabled('transit', false, { source: 'qa' });
|
||||
const camera = app.viewer.camera,
|
||||
C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(-71.0605, 42.3554, 600)),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 4, roll: 0 },
|
||||
});
|
||||
@@ -478,7 +486,7 @@ export async function runBostonLive({
|
||||
layer._setTransitFixtureFloorsForTest([], 0);
|
||||
await layer.update();
|
||||
});
|
||||
await wait(20000);
|
||||
await whenTilesLoaded(page);
|
||||
await detectOn(page);
|
||||
const evidence = [];
|
||||
for (const view of [
|
||||
@@ -507,27 +515,30 @@ export async function runBostonLive({
|
||||
params: {},
|
||||
},
|
||||
]) {
|
||||
await page.evaluate((view) => {
|
||||
const app = window.__godsEyeView,
|
||||
camera = app.viewer.camera,
|
||||
C = camera.positionCartographic.constructor;
|
||||
app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitPartsForTest()
|
||||
.selection.clearSelection();
|
||||
camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(-71.0605, 42.3554, view.altitude),
|
||||
),
|
||||
orientation: {
|
||||
heading: (view.heading * Math.PI) / 180,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
}, view);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, view) => {
|
||||
const app = window.__godsEyeView,
|
||||
C = qaGeo.cartographic;
|
||||
app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitPartsForTest()
|
||||
.selection.clearSelection();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(-71.0605, 42.3554, view.altitude),
|
||||
),
|
||||
orientation: {
|
||||
heading: (view.heading * Math.PI) / 180,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
view,
|
||||
);
|
||||
await selectStyle(page, view.style, view.params);
|
||||
await wait(8000);
|
||||
await whenTilesLoaded(page);
|
||||
const fleet = await page.evaluate(() => {
|
||||
const state = window.__godsEyeView.dataManager.layers
|
||||
.get('transit')
|
||||
@@ -562,7 +573,7 @@ export async function runBostonLive({
|
||||
let selection = null;
|
||||
if (target) {
|
||||
await page.mouse.click(target.x, target.y);
|
||||
await wait(250);
|
||||
await whenFramesDrawn(page);
|
||||
selection = await page.evaluate((key) => {
|
||||
const state = window.__godsEyeView.dataManager.layers
|
||||
.get('transit')
|
||||
@@ -606,15 +617,15 @@ export async function runTrailVisibility({
|
||||
}) {
|
||||
const name = 'Google 3D selected trail visible at 6 of 8 projected samples';
|
||||
await selectStyle(page, 'normal');
|
||||
await page.evaluate(
|
||||
async ({ altitude, pitch }) => {
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { altitude, pitch }) => {
|
||||
const app = window.__godsEyeView;
|
||||
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
|
||||
if (app.mapStackController.isStackAvailable('photoreal'))
|
||||
await app.mapStackController.setStack('photoreal');
|
||||
const camera = app.viewer.camera,
|
||||
C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(
|
||||
-97.7431,
|
||||
@@ -627,11 +638,11 @@ export async function runTrailVisibility({
|
||||
},
|
||||
{ altitude, pitch },
|
||||
);
|
||||
await wait(8000);
|
||||
const setup = await page.evaluate(
|
||||
async ({ altitude, pitch, feed }) => {
|
||||
const app = window.__godsEyeView,
|
||||
scene = app.viewer.scene;
|
||||
await whenTilesLoaded(page);
|
||||
const setup = await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { altitude, pitch, feed }) => {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest(),
|
||||
parts = layer._transitPartsForTest();
|
||||
@@ -641,11 +652,13 @@ export async function runTrailVisibility({
|
||||
app.mapStackController.getActiveId() === 'photoreal';
|
||||
if (!google)
|
||||
return { google: false, reason: 'visible Google 3D tileset required' };
|
||||
const C = scene.camera.positionCartographic.constructor;
|
||||
const C = qaGeo.cartographic;
|
||||
const centreLat = 30.267 + 90 / 111320;
|
||||
const floor = scene.sampleHeight(C.fromDegrees(-97.7431, centreLat));
|
||||
const floor = window.__godsEyeView.engine.surface.sampleHeight(
|
||||
C.fromDegrees(-97.7431, centreLat),
|
||||
);
|
||||
if (Number.isFinite(floor))
|
||||
scene.camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(
|
||||
C.fromDegrees(
|
||||
-97.7431,
|
||||
@@ -744,7 +757,7 @@ export async function runTrailVisibility({
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const parts = layer._transitPartsForTest(),
|
||||
entry = layer._transitStateForTest()._vehicles.get(key);
|
||||
const gl = scene.context._gl,
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2'),
|
||||
originals = new Map(),
|
||||
wrapped = new Map();
|
||||
let owner = null,
|
||||
@@ -791,29 +804,35 @@ export async function runTrailVisibility({
|
||||
return original.apply(this, args);
|
||||
};
|
||||
}
|
||||
const pre = scene.preRender.addEventListener(() => {
|
||||
headBytes = bodyBytes = 0;
|
||||
const walk = (collection) => {
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const primitive = collection.get(i);
|
||||
const kind = window.__qaTransitLineKinds.get(primitive?.gevGroundSet);
|
||||
if (kind) wrap(primitive, kind);
|
||||
else if (typeof primitive?.get === 'function') walk(primitive);
|
||||
}
|
||||
};
|
||||
walk(scene.primitives);
|
||||
walk(scene.groundPrimitives);
|
||||
});
|
||||
const post = scene.postRender.addEventListener(() => {
|
||||
frames++;
|
||||
headUploadMax = Math.max(headUploadMax, headBytes);
|
||||
bodyUploadMax = Math.max(bodyUploadMax, bodyBytes);
|
||||
});
|
||||
const pre = window.__godsEyeView.engine.frames.preRender.addEventListener(
|
||||
() => {
|
||||
headBytes = bodyBytes = 0;
|
||||
const walk = (collection) => {
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const primitive = collection.get(i);
|
||||
const kind = window.__qaTransitLineKinds.get(
|
||||
primitive?.gevGroundSet,
|
||||
);
|
||||
if (kind) wrap(primitive, kind);
|
||||
else if (typeof primitive?.get === 'function') walk(primitive);
|
||||
}
|
||||
};
|
||||
walk(scene.primitives);
|
||||
walk(scene.groundPrimitives);
|
||||
},
|
||||
);
|
||||
const post = window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
() => {
|
||||
frames++;
|
||||
headUploadMax = Math.max(headUploadMax, headBytes);
|
||||
bodyUploadMax = Math.max(bodyUploadMax, bodyBytes);
|
||||
},
|
||||
);
|
||||
try {
|
||||
// Include a full history upload, even if the first selection already rendered.
|
||||
entry.pathRevision = -1;
|
||||
parts.trails.prepareEntry(entry);
|
||||
scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => setTimeout(resolve, 20000));
|
||||
headCpu.sort((a, b) => a - b);
|
||||
return {
|
||||
@@ -832,221 +851,237 @@ export async function runTrailVisibility({
|
||||
if (!primitive.isDestroyed()) primitive.update = update;
|
||||
}
|
||||
}, setup.key);
|
||||
const result = await page.evaluate(async (setup) => {
|
||||
const app = window.__godsEyeView,
|
||||
scene = app.viewer.scene;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest(),
|
||||
parts = layer._transitPartsForTest();
|
||||
const entry = state._vehicles.get(setup.key);
|
||||
const trail = parts.trails.diagnostics();
|
||||
if (!entry || !trail)
|
||||
return { ...setup, present: 0, reason: 'selection disappeared' };
|
||||
const { setRate } = await import('/src/data/contactPlayback.js');
|
||||
setRate(entry.track, 0, {
|
||||
wallNowMs: Date.now(),
|
||||
monoNowMs: performance.now(),
|
||||
});
|
||||
parts.rendering.sampleIdle(entry);
|
||||
parts.rendering.schedulePlayback(entry);
|
||||
parts.rendering.syncRenderHold();
|
||||
const ready = () =>
|
||||
app.tileset?.show !== false &&
|
||||
app.tileset?.tilesLoaded === true &&
|
||||
(scene.globe?.show === false || scene.globe?.tilesLoaded === true) &&
|
||||
trail.body?.ready === true &&
|
||||
(!trail.headPrimitive || trail.headPrimitive.ready === true);
|
||||
// Require sustained readiness after freezing playback, before projecting
|
||||
// onto the refined surface. Repeated captures still check pixel stability.
|
||||
const tilesReady = await new Promise((resolve) => {
|
||||
const started = performance.now();
|
||||
let readySince = null;
|
||||
const timer = setInterval(() => {
|
||||
const now = performance.now();
|
||||
readySince = ready() ? (readySince ?? now) : null;
|
||||
scene.requestRender();
|
||||
if (
|
||||
(readySince !== null && now - readySince >= 500) ||
|
||||
now - started >= 30000
|
||||
) {
|
||||
clearInterval(timer);
|
||||
resolve(readySince !== null && now - readySince >= 500);
|
||||
const result = await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, setup) => {
|
||||
const app = window.__godsEyeView,
|
||||
scene = app.viewer.scene;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest(),
|
||||
parts = layer._transitPartsForTest();
|
||||
const entry = state._vehicles.get(setup.key);
|
||||
const trail = parts.trails.diagnostics();
|
||||
if (!entry || !trail)
|
||||
return { ...setup, present: 0, reason: 'selection disappeared' };
|
||||
const { setRate } = await import('/src/data/contactPlayback.js');
|
||||
setRate(entry.track, 0, {
|
||||
wallNowMs: Date.now(),
|
||||
monoNowMs: performance.now(),
|
||||
});
|
||||
parts.rendering.sampleIdle(entry);
|
||||
parts.rendering.schedulePlayback(entry);
|
||||
parts.rendering.syncRenderHold();
|
||||
const ready = () =>
|
||||
app.tileset?.show !== false &&
|
||||
window.__godsEyeView.engine.surface.tilesLoaded() === true &&
|
||||
(scene.globe?.show === false ||
|
||||
window.__godsEyeView.engine.surface.tilesLoaded() === true) &&
|
||||
trail.body?.ready === true &&
|
||||
(!trail.headPrimitive || trail.headPrimitive.ready === true);
|
||||
// Require sustained readiness after freezing playback, before projecting
|
||||
// onto the refined surface. Repeated captures still check pixel stability.
|
||||
const tilesReady = await new Promise((resolve) => {
|
||||
const started = performance.now();
|
||||
let readySince = null;
|
||||
const timer = setInterval(() => {
|
||||
const now = performance.now();
|
||||
readySince = ready() ? (readySince ?? now) : null;
|
||||
window.__godsEyeView.engine.render.request();
|
||||
if (
|
||||
(readySince !== null && now - readySince >= 500) ||
|
||||
now - started >= 30000
|
||||
) {
|
||||
clearInterval(timer);
|
||||
resolve(readySince !== null && now - readySince >= 500);
|
||||
}
|
||||
}, 80);
|
||||
});
|
||||
if (!tilesReady)
|
||||
return {
|
||||
...setup,
|
||||
present: 0,
|
||||
reason: 'tiles or trail geometry did not become ready',
|
||||
};
|
||||
const canvas = window.__godsEyeView.engine.host.canvas,
|
||||
gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||||
const scale = canvas.width / canvas.clientWidth;
|
||||
const points = entry.trailSegments
|
||||
.filter((s) => s.toT <= entry.sample.displayT)
|
||||
.flatMap((s) => s.positions);
|
||||
if (trail.head?.show) points.push(...trail.head.positions);
|
||||
if (points.length < 2)
|
||||
return { ...setup, present: 0, reason: 'no completed path' };
|
||||
const C = qaGeo.cartographic;
|
||||
// Marker positions are plain { x, y, z } now (the display's handles), not
|
||||
// Cesium Cartesian3 instances with static helpers.
|
||||
const V = {
|
||||
distance: (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z),
|
||||
lerp: (a, b, t) => ({
|
||||
x: a.x + (b.x - a.x) * t,
|
||||
y: a.y + (b.y - a.y) * t,
|
||||
z: a.z + (b.z - a.z) * t,
|
||||
}),
|
||||
};
|
||||
// What the floor samples leave out: the Cesium primitives the engine
|
||||
// draws the transit mover sets and trail lines with.
|
||||
const drawnByTransit = [];
|
||||
const walk = (collection) => {
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const item = collection.get(i);
|
||||
if (
|
||||
String(item?.gevMoverSet ?? '').startsWith('transit') ||
|
||||
String(item?.gevGroundSet ?? '').startsWith('transit-trail-')
|
||||
)
|
||||
drawnByTransit.push(item);
|
||||
else if (typeof item?.get === 'function' && 'length' in item)
|
||||
walk(item);
|
||||
}
|
||||
}, 80);
|
||||
});
|
||||
if (!tilesReady)
|
||||
};
|
||||
walk(scene.primitives);
|
||||
walk(scene.groundPrimitives);
|
||||
const lengths = [0];
|
||||
for (let i = 1; i < points.length; i++)
|
||||
lengths.push(lengths.at(-1) + V.distance(points[i - 1], points[i]));
|
||||
const mul = (m, v) =>
|
||||
[0, 1, 2, 3].map(
|
||||
(i) =>
|
||||
m[i] * v[0] + m[i + 4] * v[1] + m[i + 8] * v[2] + m[i + 12] * v[3],
|
||||
);
|
||||
const samples = Array.from({ length: 8 }, (_, i) => {
|
||||
const d = (lengths.at(-1) * (i + 1)) / 9;
|
||||
let j = 1;
|
||||
while (j < lengths.length - 1 && lengths[j] < d) j++;
|
||||
const p = V.lerp(
|
||||
points[j - 1],
|
||||
points[j],
|
||||
(d - lengths[j - 1]) / Math.max(0.001, lengths[j] - lengths[j - 1]),
|
||||
);
|
||||
const cart = C.fromCartesian(p);
|
||||
const height = window.__godsEyeView.engine.surface.sampleHeight(
|
||||
cart,
|
||||
drawnByTransit,
|
||||
);
|
||||
if (Number.isFinite(height)) cart.height = height;
|
||||
const surface = C.toCartesian(cart);
|
||||
const q = mul(window.__godsEyeView.engine.projector.viewProjection(), [
|
||||
surface.x,
|
||||
surface.y,
|
||||
surface.z,
|
||||
1,
|
||||
]);
|
||||
return {
|
||||
x: ((q[0] / q[3]) * 0.5 + 0.5) * canvas.clientWidth,
|
||||
y: (0.5 - (q[1] / q[3]) * 0.5) * canvas.clientHeight,
|
||||
inFront: q[3] > 0,
|
||||
radius: (Math.SQRT2 * Math.ceil(3 * scale)) / scale,
|
||||
};
|
||||
});
|
||||
const marker = entry.marker.position;
|
||||
const markerClip = mul(
|
||||
window.__godsEyeView.engine.projector.viewProjection(),
|
||||
[marker.x, marker.y, marker.z, 1],
|
||||
);
|
||||
const sprite = {
|
||||
x: ((markerClip[0] / markerClip[3]) * 0.5 + 0.5) * canvas.clientWidth,
|
||||
y: (0.5 - (markerClip[1] / markerClip[3]) * 0.5) * canvas.clientHeight,
|
||||
// Include the padded, rotated sprite and its selection bracket.
|
||||
radius: Math.hypot(entry.marker.width, entry.marker.height) / 2 + 4,
|
||||
};
|
||||
const read = () =>
|
||||
samples.map(({ x, y, inFront }) => {
|
||||
const r = Math.ceil(3 * scale),
|
||||
px = Math.round(x * scale),
|
||||
py = Math.round(canvas.height - 1 - y * scale);
|
||||
if (
|
||||
!inFront ||
|
||||
px < r ||
|
||||
py < r ||
|
||||
px + r >= canvas.width ||
|
||||
py + r >= canvas.height
|
||||
)
|
||||
return null;
|
||||
const data = new Uint8Array((2 * r + 1) ** 2 * 4);
|
||||
gl.readPixels(
|
||||
px - r,
|
||||
py - r,
|
||||
2 * r + 1,
|
||||
2 * r + 1,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
data,
|
||||
);
|
||||
return data;
|
||||
});
|
||||
const frame = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
remove();
|
||||
reject(new Error('Trail framebuffer did not render within 5 s'));
|
||||
}, 5000);
|
||||
const remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
() => {
|
||||
remove();
|
||||
clearTimeout(timeout);
|
||||
resolve(read());
|
||||
},
|
||||
);
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
const trailRenderer = window.__qaTransitTrail;
|
||||
if (!trailRenderer)
|
||||
return { ...setup, present: 0, reason: 'no trail renderer captured' };
|
||||
let showTrail = false;
|
||||
// Body and head together, as the trail's own visibility switch does.
|
||||
const applyVisibility = () => trailRenderer.setVisible(showTrail);
|
||||
// Override the layer's frame update in both states; selection stays fixed.
|
||||
const removeVisibility =
|
||||
window.__godsEyeView.engine.frames.preRender.addEventListener(
|
||||
applyVisibility,
|
||||
);
|
||||
const captures = [];
|
||||
let captureReady = true;
|
||||
try {
|
||||
for (const visible of [false, true, false, true]) {
|
||||
showTrail = visible;
|
||||
// Let post-processing settle before each measured framebuffer.
|
||||
for (let i = 0; i < 3; i++) await frame();
|
||||
captures.push(await frame());
|
||||
const current = parts.trails.diagnostics();
|
||||
captureReady &&=
|
||||
ready() &&
|
||||
current?.body === trail.body &&
|
||||
current?.headPrimitive === trail.headPrimitive;
|
||||
}
|
||||
} finally {
|
||||
removeVisibility();
|
||||
trailRenderer.setVisible(true);
|
||||
await frame();
|
||||
}
|
||||
const [off, on, offAgain, onAgain] = captures.map((patches) =>
|
||||
patches.map((p) => (p ? Array.from(p) : null)),
|
||||
);
|
||||
return {
|
||||
...setup,
|
||||
present: 0,
|
||||
reason: 'tiles or trail geometry did not become ready',
|
||||
fixes: entry.track.count,
|
||||
selected: state._selectedKey === setup.key,
|
||||
pathMetres: lengths.at(-1),
|
||||
samples,
|
||||
on,
|
||||
off,
|
||||
onAgain,
|
||||
offAgain,
|
||||
sprite,
|
||||
tilesReady: captureReady,
|
||||
elapsedDisplayMs: entry.sample.displayT - setup.startDisplayT,
|
||||
movedMetres: Math.abs(entry.sample.lat - setup.startLat) * 111320,
|
||||
lines: trail.body?.lines ?? 0,
|
||||
headStrategy: trail.headStrategy,
|
||||
headRebuilds: trail.headRebuilds ?? null,
|
||||
};
|
||||
const canvas = scene.canvas,
|
||||
gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||||
const scale = canvas.width / canvas.clientWidth;
|
||||
const points = entry.trailSegments
|
||||
.filter((s) => s.toT <= entry.sample.displayT)
|
||||
.flatMap((s) => s.positions);
|
||||
if (trail.head?.show) points.push(...trail.head.positions);
|
||||
if (points.length < 2)
|
||||
return { ...setup, present: 0, reason: 'no completed path' };
|
||||
const C = scene.camera.positionCartographic.constructor;
|
||||
// Marker positions are plain { x, y, z } now (the display's handles), not
|
||||
// Cesium Cartesian3 instances with static helpers.
|
||||
const V = {
|
||||
distance: (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z),
|
||||
lerp: (a, b, t) => ({
|
||||
x: a.x + (b.x - a.x) * t,
|
||||
y: a.y + (b.y - a.y) * t,
|
||||
z: a.z + (b.z - a.z) * t,
|
||||
}),
|
||||
};
|
||||
// What the floor samples leave out: the Cesium primitives the engine
|
||||
// draws the transit mover sets and trail lines with.
|
||||
const drawnByTransit = [];
|
||||
const walk = (collection) => {
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const item = collection.get(i);
|
||||
if (
|
||||
String(item?.gevMoverSet ?? '').startsWith('transit') ||
|
||||
String(item?.gevGroundSet ?? '').startsWith('transit-trail-')
|
||||
)
|
||||
drawnByTransit.push(item);
|
||||
else if (typeof item?.get === 'function' && 'length' in item)
|
||||
walk(item);
|
||||
}
|
||||
};
|
||||
walk(scene.primitives);
|
||||
walk(scene.groundPrimitives);
|
||||
const lengths = [0];
|
||||
for (let i = 1; i < points.length; i++)
|
||||
lengths.push(lengths.at(-1) + V.distance(points[i - 1], points[i]));
|
||||
const mul = (m, v) =>
|
||||
[0, 1, 2, 3].map(
|
||||
(i) =>
|
||||
m[i] * v[0] + m[i + 4] * v[1] + m[i + 8] * v[2] + m[i + 12] * v[3],
|
||||
);
|
||||
const samples = Array.from({ length: 8 }, (_, i) => {
|
||||
const d = (lengths.at(-1) * (i + 1)) / 9;
|
||||
let j = 1;
|
||||
while (j < lengths.length - 1 && lengths[j] < d) j++;
|
||||
const p = V.lerp(
|
||||
points[j - 1],
|
||||
points[j],
|
||||
(d - lengths[j - 1]) / Math.max(0.001, lengths[j] - lengths[j - 1]),
|
||||
);
|
||||
const cart = C.fromCartesian(p);
|
||||
const height = scene.sampleHeight(cart, drawnByTransit);
|
||||
if (Number.isFinite(height)) cart.height = height;
|
||||
const surface = C.toCartesian(cart);
|
||||
const q = mul(
|
||||
scene.camera.frustum.projectionMatrix,
|
||||
mul(scene.camera.viewMatrix, [surface.x, surface.y, surface.z, 1]),
|
||||
);
|
||||
return {
|
||||
x: ((q[0] / q[3]) * 0.5 + 0.5) * canvas.clientWidth,
|
||||
y: (0.5 - (q[1] / q[3]) * 0.5) * canvas.clientHeight,
|
||||
inFront: q[3] > 0,
|
||||
radius: (Math.SQRT2 * Math.ceil(3 * scale)) / scale,
|
||||
};
|
||||
});
|
||||
const marker = entry.marker.position;
|
||||
const markerClip = mul(
|
||||
scene.camera.frustum.projectionMatrix,
|
||||
mul(scene.camera.viewMatrix, [marker.x, marker.y, marker.z, 1]),
|
||||
);
|
||||
const sprite = {
|
||||
x: ((markerClip[0] / markerClip[3]) * 0.5 + 0.5) * canvas.clientWidth,
|
||||
y: (0.5 - (markerClip[1] / markerClip[3]) * 0.5) * canvas.clientHeight,
|
||||
// Include the padded, rotated sprite and its selection bracket.
|
||||
radius: Math.hypot(entry.marker.width, entry.marker.height) / 2 + 4,
|
||||
};
|
||||
const read = () =>
|
||||
samples.map(({ x, y, inFront }) => {
|
||||
const r = Math.ceil(3 * scale),
|
||||
px = Math.round(x * scale),
|
||||
py = Math.round(canvas.height - 1 - y * scale);
|
||||
if (
|
||||
!inFront ||
|
||||
px < r ||
|
||||
py < r ||
|
||||
px + r >= canvas.width ||
|
||||
py + r >= canvas.height
|
||||
)
|
||||
return null;
|
||||
const data = new Uint8Array((2 * r + 1) ** 2 * 4);
|
||||
gl.readPixels(
|
||||
px - r,
|
||||
py - r,
|
||||
2 * r + 1,
|
||||
2 * r + 1,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
data,
|
||||
);
|
||||
return data;
|
||||
});
|
||||
const frame = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
remove();
|
||||
reject(new Error('Trail framebuffer did not render within 5 s'));
|
||||
}, 5000);
|
||||
const remove = scene.postRender.addEventListener(() => {
|
||||
remove();
|
||||
clearTimeout(timeout);
|
||||
resolve(read());
|
||||
});
|
||||
scene.requestRender();
|
||||
});
|
||||
const trailRenderer = window.__qaTransitTrail;
|
||||
if (!trailRenderer)
|
||||
return { ...setup, present: 0, reason: 'no trail renderer captured' };
|
||||
let showTrail = false;
|
||||
// Body and head together, as the trail's own visibility switch does.
|
||||
const applyVisibility = () => trailRenderer.setVisible(showTrail);
|
||||
// Override the layer's frame update in both states; selection stays fixed.
|
||||
const removeVisibility = scene.preRender.addEventListener(applyVisibility);
|
||||
const captures = [];
|
||||
let captureReady = true;
|
||||
try {
|
||||
for (const visible of [false, true, false, true]) {
|
||||
showTrail = visible;
|
||||
// Let post-processing settle before each measured framebuffer.
|
||||
for (let i = 0; i < 3; i++) await frame();
|
||||
captures.push(await frame());
|
||||
const current = parts.trails.diagnostics();
|
||||
captureReady &&=
|
||||
ready() &&
|
||||
current?.body === trail.body &&
|
||||
current?.headPrimitive === trail.headPrimitive;
|
||||
}
|
||||
} finally {
|
||||
removeVisibility();
|
||||
trailRenderer.setVisible(true);
|
||||
await frame();
|
||||
}
|
||||
const [off, on, offAgain, onAgain] = captures.map((patches) =>
|
||||
patches.map((p) => (p ? Array.from(p) : null)),
|
||||
);
|
||||
return {
|
||||
...setup,
|
||||
fixes: entry.track.count,
|
||||
selected: state._selectedKey === setup.key,
|
||||
pathMetres: lengths.at(-1),
|
||||
samples,
|
||||
on,
|
||||
off,
|
||||
onAgain,
|
||||
offAgain,
|
||||
sprite,
|
||||
tilesReady: captureReady,
|
||||
elapsedDisplayMs: entry.sample.displayT - setup.startDisplayT,
|
||||
movedMetres: Math.abs(entry.sample.lat - setup.startLat) * 111320,
|
||||
lines: trail.body?.lines ?? 0,
|
||||
headStrategy: trail.headStrategy,
|
||||
headRebuilds: trail.headRebuilds ?? null,
|
||||
};
|
||||
}, setup);
|
||||
},
|
||||
setup,
|
||||
);
|
||||
const pixels = reduceTrailPixels(result.on, result.off, result);
|
||||
result.cost = cost;
|
||||
result.altitude = altitude;
|
||||
|
||||
+169
-147
@@ -1,4 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native entity counts audit Cesium resource ownership alongside the contract checks.
|
||||
import { whenSettled, whenUiSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { hookRenderErrors } from './qa-browserEvidence.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { runTransitHeadingRegression } from './qa-transit-heading.mjs';
|
||||
import {
|
||||
boundPageEvaluations,
|
||||
@@ -36,7 +41,6 @@ import {
|
||||
* exercises the same paths.
|
||||
*/
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { chromeExecutablePath, captureQaScreenshot } from './qa-chrome.mjs';
|
||||
import { createTransitHistory } from '../server/providers/transitHistory.js';
|
||||
@@ -167,13 +171,10 @@ async function sampleScreen(page) {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest();
|
||||
const scene = app.viewer.scene;
|
||||
const canvas = scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
// The app exposes no `window.Cesium`, so the projection is done here in
|
||||
// plain numbers: Cesium matrices are column-major arrays of 16 doubles,
|
||||
// and window coordinates are the standard clip -> NDC -> viewport walk.
|
||||
const view = scene.camera.viewMatrix;
|
||||
const proj = scene.camera.frustum.projectionMatrix;
|
||||
const mul = (m, v) => [
|
||||
m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3],
|
||||
m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3],
|
||||
@@ -186,7 +187,12 @@ async function sampleScreen(page) {
|
||||
for (const entry of state._vehicles.values()) {
|
||||
if (!entry.marker || entry.marker.show === false) continue;
|
||||
const p = entry.marker.position;
|
||||
const clip = mul(proj, mul(view, [p.x, p.y, p.z, 1]));
|
||||
const clip = mul(window.__godsEyeView.engine.projector.viewProjection(), [
|
||||
p.x,
|
||||
p.y,
|
||||
p.z,
|
||||
1,
|
||||
]);
|
||||
if (!(clip[3] > 0)) continue;
|
||||
const ndcX = clip[0] / clip[3];
|
||||
const ndcY = clip[1] / clip[3];
|
||||
@@ -207,11 +213,11 @@ async function sampleScreen(page) {
|
||||
}
|
||||
|
||||
async function sampleVisible(page) {
|
||||
return page.evaluate(() => {
|
||||
return evaluateWithGeo(page, async (qaGeo) => {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest();
|
||||
const Cartographic = app.viewer.camera.positionCartographic.constructor;
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
return [...state._vehicles.values()]
|
||||
.filter((entry) => entry.marker && entry.marker.show !== false)
|
||||
.slice(0, 200)
|
||||
@@ -263,13 +269,11 @@ check(
|
||||
historyProbe.clear();
|
||||
await mkdir(SHOTS, { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 150000,
|
||||
executablePath: chromeExecutablePath(),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -278,6 +282,7 @@ const browser = await puppeteer.launch({
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await hookRenderErrors(page);
|
||||
await blockPaidQaMaps(page);
|
||||
// This gate measures visible-page motion. OS minimization must not silently
|
||||
// switch it into the app's intentional hidden-tab polling pause.
|
||||
@@ -320,7 +325,7 @@ try {
|
||||
Boolean(document.querySelector('[data-layer-id="transit"]')),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await wait(6_000);
|
||||
await whenSettled(page);
|
||||
|
||||
// The Data Layers panel starts collapsed; a user opens it before toggling.
|
||||
const expanded = await page.$eval(
|
||||
@@ -331,7 +336,7 @@ try {
|
||||
return false;
|
||||
},
|
||||
);
|
||||
if (!expanded) await wait(800);
|
||||
if (!expanded) await whenUiSettled(page);
|
||||
check(
|
||||
'the Data Layers panel opens from its own header button',
|
||||
await page.$eval(
|
||||
@@ -371,7 +376,7 @@ try {
|
||||
await page.$eval(row, (element) =>
|
||||
element.scrollIntoView({ block: 'center' }),
|
||||
);
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
const before = transitRequests.length;
|
||||
await page.click(`${row} .data-toggle-btn`);
|
||||
await page.waitForFunction(
|
||||
@@ -385,24 +390,24 @@ try {
|
||||
);
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.__transitRenderErrors = [];
|
||||
window.__godsEyeView.viewer.scene.renderError.addEventListener(
|
||||
(scene, error) => window.__transitRenderErrors.push(error.message),
|
||||
);
|
||||
window.__transitRenderErrors = window.__qaRenderErrors;
|
||||
});
|
||||
for (const city of runs('cities') ? CITIES : []) {
|
||||
console.log(`\n== ${city.label} ==`);
|
||||
// City-wide first: enough altitude to see the fleet as a fleet.
|
||||
await page.evaluate((c) => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const Cartographic = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(c.lon, c.lat - 0.03, 3_500),
|
||||
),
|
||||
orientation: { heading: 0, pitch: (-40 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
}, city);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, c) => {
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(c.lon, c.lat - 0.03, 3_500),
|
||||
),
|
||||
orientation: { heading: 0, pitch: (-40 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
},
|
||||
city,
|
||||
);
|
||||
const arrived = await page
|
||||
.waitForFunction(
|
||||
(feedId) =>
|
||||
@@ -428,7 +433,7 @@ try {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await wait(9_000);
|
||||
await whenSettled(page);
|
||||
const fleet = await page.evaluate((feedId) => {
|
||||
const layer =
|
||||
window.__godsEyeView.dataManager.layers.get('transit').module;
|
||||
@@ -451,49 +456,52 @@ try {
|
||||
|
||||
// Street level: 600 m up, 45 degrees down, framed on a real vehicle so the
|
||||
// shot shows what a user sees when they drop in on one.
|
||||
const framed = await page.evaluate((c) => {
|
||||
const app = window.__godsEyeView;
|
||||
const camera = app.viewer.camera;
|
||||
const Cartographic = camera.positionCartographic.constructor;
|
||||
const state = app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitStateForTest();
|
||||
let best = null;
|
||||
for (const entry of state._vehicles.values()) {
|
||||
if (!entry.key.startsWith(`${c.feedId}:`)) continue;
|
||||
const d = Math.hypot(entry.to.lat - c.lat, entry.to.lon - c.lon);
|
||||
if (!best || d < best.d)
|
||||
best = { d, lat: entry.to.lat, lon: entry.to.lon, key: entry.key };
|
||||
}
|
||||
if (!best) return null;
|
||||
// Camera 600 m back along the heading, 600 m up, looking 45 degrees down:
|
||||
// the vehicle lands in the middle of the frame.
|
||||
const heading = (c.heading * Math.PI) / 180;
|
||||
const lat = best.lat - (600 * Math.cos(heading)) / 111_320;
|
||||
const lon =
|
||||
best.lon -
|
||||
(600 * Math.sin(heading)) /
|
||||
(111_320 * Math.cos((best.lat * Math.PI) / 180));
|
||||
camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(lon, lat, 600),
|
||||
),
|
||||
orientation: { heading, pitch: (-45 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
return best;
|
||||
}, city);
|
||||
const framed = await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, c) => {
|
||||
const app = window.__godsEyeView;
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
const state = app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitStateForTest();
|
||||
let best = null;
|
||||
for (const entry of state._vehicles.values()) {
|
||||
if (!entry.key.startsWith(`${c.feedId}:`)) continue;
|
||||
const d = Math.hypot(entry.to.lat - c.lat, entry.to.lon - c.lon);
|
||||
if (!best || d < best.d)
|
||||
best = { d, lat: entry.to.lat, lon: entry.to.lon, key: entry.key };
|
||||
}
|
||||
if (!best) return null;
|
||||
// Camera 600 m back along the heading, 600 m up, looking 45 degrees down:
|
||||
// the vehicle lands in the middle of the frame.
|
||||
const heading = (c.heading * Math.PI) / 180;
|
||||
const lat = best.lat - (600 * Math.cos(heading)) / 111_320;
|
||||
const lon =
|
||||
best.lon -
|
||||
(600 * Math.sin(heading)) /
|
||||
(111_320 * Math.cos((best.lat * Math.PI) / 180));
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(lon, lat, 600),
|
||||
),
|
||||
orientation: { heading, pitch: (-45 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
return best;
|
||||
},
|
||||
city,
|
||||
);
|
||||
check(
|
||||
`${city.label}: a vehicle to fly to`,
|
||||
Boolean(framed),
|
||||
framed?.key || 'none',
|
||||
);
|
||||
await wait(9_000);
|
||||
await whenSettled(page);
|
||||
|
||||
const first = await page.evaluate(() => {
|
||||
const first = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest();
|
||||
const Cartographic = app.viewer.camera.positionCartographic.constructor;
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
return {
|
||||
count: state._vehicles.size,
|
||||
stats: layer.getStats(),
|
||||
@@ -528,18 +536,19 @@ try {
|
||||
const state = app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitStateForTest();
|
||||
const scene = app.viewer.scene;
|
||||
const seen = [];
|
||||
for (const entry of state._vehicles.values()) {
|
||||
const screen = scene.cartesianToCanvasCoordinates(
|
||||
const screen = window.__godsEyeView.engine.projector.worldToWindow(
|
||||
entry.marker.position,
|
||||
);
|
||||
if (
|
||||
screen &&
|
||||
screen.x > 360 &&
|
||||
screen.x < scene.canvas.clientWidth - 120 &&
|
||||
screen.x <
|
||||
window.__godsEyeView.engine.host.canvas.clientWidth - 120 &&
|
||||
screen.y > 120 &&
|
||||
screen.y < scene.canvas.clientHeight - 160
|
||||
screen.y <
|
||||
window.__godsEyeView.engine.host.canvas.clientHeight - 160
|
||||
) {
|
||||
seen.push({
|
||||
key: entry.key,
|
||||
@@ -552,7 +561,7 @@ try {
|
||||
}, attempt);
|
||||
if (!target) break;
|
||||
await page.mouse.click(target.x, target.y);
|
||||
await wait(900);
|
||||
await whenUiSettled(page);
|
||||
selected = await page.evaluate(() => {
|
||||
const app = window.__godsEyeView;
|
||||
const key = app.dataManager.layers
|
||||
@@ -711,7 +720,7 @@ try {
|
||||
});
|
||||
for (const preset of presets) {
|
||||
const how = await selectStyle(page, preset.style, preset.params);
|
||||
await wait(1_200);
|
||||
await whenUiSettled(page);
|
||||
const styling = await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.layers
|
||||
.get('transit')
|
||||
@@ -782,7 +791,7 @@ try {
|
||||
const clickTarget = rendered[0];
|
||||
if (clickTarget) {
|
||||
await page.mouse.click(clickTarget.x, clickTarget.y);
|
||||
await wait(100);
|
||||
await whenUiSettled(page);
|
||||
const picked = await page.evaluate((key) => {
|
||||
const state = window.__godsEyeView.dataManager.layers
|
||||
.get('transit')
|
||||
@@ -816,7 +825,7 @@ try {
|
||||
const target = (await sampleRendered(page))[0];
|
||||
if (target) {
|
||||
await page.mouse.click(target.x, target.y);
|
||||
await wait(800);
|
||||
await whenUiSettled(page);
|
||||
const underFlir = await page.evaluate((key) => {
|
||||
const layer =
|
||||
window.__godsEyeView.dataManager.layers.get('transit').module;
|
||||
@@ -851,7 +860,7 @@ try {
|
||||
JSON.stringify(underFlir),
|
||||
);
|
||||
await page.keyboard.press('Escape');
|
||||
await wait(500);
|
||||
await whenUiSettled(page);
|
||||
} else {
|
||||
check(
|
||||
'clicking a sprite under FLIR selects it and keeps it white-hot',
|
||||
@@ -881,16 +890,18 @@ try {
|
||||
|
||||
// Motion acceptance uses known report-time playback even when live fleets park.
|
||||
await selectStyle(page, 'normal');
|
||||
await page.evaluate((city) => {
|
||||
const app = window.__godsEyeView,
|
||||
camera = app.viewer.camera;
|
||||
const C = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(city.lon, city.lat, 600)),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, city);
|
||||
await wait(2000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, city) => {
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(C.fromDegrees(city.lon, city.lat, 600)),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
},
|
||||
city,
|
||||
);
|
||||
await whenUiSettled(page);
|
||||
await page.evaluate(
|
||||
(city) =>
|
||||
window.__godsEyeView.dataManager.layers
|
||||
@@ -901,11 +912,11 @@ try {
|
||||
await wait(1000);
|
||||
const motionFirst = await sampleVisible(page);
|
||||
await wait(30_000);
|
||||
const second = await page.evaluate(() => {
|
||||
const second = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest();
|
||||
const Cartographic = app.viewer.camera.positionCartographic.constructor;
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
return {
|
||||
count: state._vehicles.size,
|
||||
// The RENDERED primitive position, read back through Cesium — not the
|
||||
@@ -1052,6 +1063,7 @@ try {
|
||||
if (runs('history')) {
|
||||
console.log('\n== retained history reload ==');
|
||||
const recovery = await browser.newPage();
|
||||
await hookRenderErrors(recovery);
|
||||
await blockPaidQaMaps(recovery);
|
||||
boundPageEvaluations(recovery);
|
||||
try {
|
||||
@@ -1106,10 +1118,7 @@ try {
|
||||
{ timeout: 90000 },
|
||||
);
|
||||
await recovery.evaluate(() => {
|
||||
window.__transitRenderErrors = [];
|
||||
window.__godsEyeView.viewer.scene.renderError.addEventListener(
|
||||
(scene, error) => window.__transitRenderErrors.push(error.message),
|
||||
);
|
||||
window.__transitRenderErrors = window.__qaRenderErrors;
|
||||
});
|
||||
const restored = await recovery.evaluate(
|
||||
restoreRetainedTransit,
|
||||
@@ -1124,7 +1133,7 @@ try {
|
||||
.trails.requestDiagnostics().pending,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
await wait(4000);
|
||||
await whenFramesDrawn(recovery);
|
||||
const proof = await recovery.evaluate(
|
||||
({ key, oldest }) => {
|
||||
const layer =
|
||||
@@ -1189,16 +1198,19 @@ try {
|
||||
// every rendered frame is sampled.
|
||||
if (runs('playback')) {
|
||||
console.log('\n== deterministic playback ==');
|
||||
await page.evaluate((c) => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const Cartographic = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(c.lon, c.lat + 0.005, 2400),
|
||||
),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, BOSTON);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, c) => {
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(c.lon, c.lat + 0.005, 2400),
|
||||
),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
},
|
||||
BOSTON,
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.dataManager.layers
|
||||
.get('transit')
|
||||
@@ -1345,14 +1357,14 @@ try {
|
||||
console.log(` scripted fleet before trace: ${await simState()}`);
|
||||
await reportTransitVisibility(page, 'scripted visibility before trace');
|
||||
// Sample every rendered frame for two minutes.
|
||||
const trace = await page.evaluate(
|
||||
(durationMs) =>
|
||||
const trace = await evaluateWithGeo(
|
||||
page,
|
||||
(qaGeo, durationMs) =>
|
||||
new Promise((resolve) => {
|
||||
const app = window.__godsEyeView;
|
||||
const layer = app.dataManager.layers.get('transit').module;
|
||||
const state = layer._transitStateForTest();
|
||||
const Cartographic =
|
||||
app.viewer.camera.positionCartographic.constructor;
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
const keys = [
|
||||
'sim-straight',
|
||||
'sim-corner',
|
||||
@@ -1410,7 +1422,10 @@ try {
|
||||
resolve({ out, words });
|
||||
}
|
||||
};
|
||||
remove = app.viewer.scene.postRender.addEventListener(tick);
|
||||
remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
tick,
|
||||
);
|
||||
}),
|
||||
120_000,
|
||||
);
|
||||
@@ -1552,23 +1567,22 @@ try {
|
||||
|
||||
// ---- Card anchor synchronisation ---------------------------------------
|
||||
console.log('\n== card anchor ==');
|
||||
await page.evaluate(() => {
|
||||
const app = window.__godsEyeView,
|
||||
camera = app.viewer.camera;
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const app = window.__godsEyeView;
|
||||
const entry = app.dataManager.layers
|
||||
.get('transit')
|
||||
.module._transitStateForTest()
|
||||
._vehicles.get('mbta:sim-straight');
|
||||
if (!entry) return;
|
||||
const C = camera.positionCartographic.constructor;
|
||||
const C = qaGeo.cartographic;
|
||||
const at = C.fromCartesian(entry.marker.position);
|
||||
at.height = 600;
|
||||
camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.toCartesian(at),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
});
|
||||
await wait(1500);
|
||||
await whenUiSettled(page);
|
||||
await reportTransitVisibility(page, 'scripted visibility before click');
|
||||
const target = await page.evaluate(() => {
|
||||
const app = window.__godsEyeView;
|
||||
@@ -1576,10 +1590,7 @@ try {
|
||||
const state = layer._transitStateForTest();
|
||||
const entry = state._vehicles.get('mbta:sim-straight');
|
||||
if (!entry?.marker?.show) return null;
|
||||
const scene = app.viewer.scene;
|
||||
const canvas = scene.canvas;
|
||||
const view = scene.camera.viewMatrix;
|
||||
const proj = scene.camera.frustum.projectionMatrix;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const mul = (m, v) => [
|
||||
m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3],
|
||||
m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3],
|
||||
@@ -1587,7 +1598,12 @@ try {
|
||||
m[3] * v[0] + m[7] * v[1] + m[11] * v[2] + m[15] * v[3],
|
||||
];
|
||||
const p = entry.marker.position;
|
||||
const clip = mul(proj, mul(view, [p.x, p.y, p.z, 1]));
|
||||
const clip = mul(window.__godsEyeView.engine.projector.viewProjection(), [
|
||||
p.x,
|
||||
p.y,
|
||||
p.z,
|
||||
1,
|
||||
]);
|
||||
const x = ((clip[0] / clip[3]) * 0.5 + 0.5) * canvas.clientWidth;
|
||||
const y = (1 - ((clip[1] / clip[3]) * 0.5 + 0.5)) * canvas.clientHeight;
|
||||
if (
|
||||
@@ -1615,7 +1631,7 @@ try {
|
||||
JSON.stringify(target),
|
||||
);
|
||||
await page.mouse.click(target.x, target.y);
|
||||
await wait(600);
|
||||
await whenUiSettled(page);
|
||||
anchor = await page.evaluate(
|
||||
async (key, durationMs) => {
|
||||
const overlay = (
|
||||
@@ -1637,8 +1653,7 @@ try {
|
||||
).pointerOwner(),
|
||||
},
|
||||
};
|
||||
const scene = app.viewer.scene;
|
||||
const canvas = scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const mul = (m, v) => [
|
||||
m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3],
|
||||
m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3],
|
||||
@@ -1659,10 +1674,11 @@ try {
|
||||
const entry = state._vehicles.get(key);
|
||||
const rect = overlay.getPaintRect('transit-selected', key);
|
||||
if (entry?.marker) {
|
||||
const view = scene.camera.viewMatrix;
|
||||
const proj = scene.camera.frustum.projectionMatrix;
|
||||
const p = entry.marker.position;
|
||||
const clip = mul(proj, mul(view, [p.x, p.y, p.z, 1]));
|
||||
const clip = mul(
|
||||
window.__godsEyeView.engine.projector.viewProjection(),
|
||||
[p.x, p.y, p.z, 1],
|
||||
);
|
||||
if (
|
||||
entry.marker.show !== false &&
|
||||
rect &&
|
||||
@@ -1698,8 +1714,8 @@ try {
|
||||
if (trail.head.show && trail.head.positions.length) {
|
||||
const project = (p) => {
|
||||
const clip = mul(
|
||||
scene.camera.frustum.projectionMatrix,
|
||||
mul(scene.camera.viewMatrix, [p.x, p.y, p.z, 1]),
|
||||
window.__godsEyeView.engine.projector.viewProjection(),
|
||||
[p.x, p.y, p.z, 1],
|
||||
);
|
||||
return [
|
||||
((clip[0] / clip[3]) * canvas.clientWidth) / 2,
|
||||
@@ -1742,7 +1758,10 @@ try {
|
||||
});
|
||||
};
|
||||
const timer = setTimeout(finish, durationMs + 1000);
|
||||
remove = scene.postRender.addEventListener(tick);
|
||||
remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
tick,
|
||||
);
|
||||
});
|
||||
},
|
||||
target.key,
|
||||
@@ -1816,21 +1835,24 @@ try {
|
||||
afterRemoval.gone && afterRemoval.selected !== 'mbta:sim-straight',
|
||||
JSON.stringify(afterRemoval),
|
||||
);
|
||||
await page.evaluate((c) => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const Cartographic = camera.positionCartographic.constructor;
|
||||
camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(c.lon, c.lat, 600),
|
||||
),
|
||||
orientation: {
|
||||
heading: (c.heading * Math.PI) / 180,
|
||||
pitch: (-45 * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
}, CITIES.at(-1));
|
||||
await wait(4_000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, c) => {
|
||||
const Cartographic = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: Cartographic.toCartesian(
|
||||
Cartographic.fromDegrees(c.lon, c.lat, 600),
|
||||
),
|
||||
orientation: {
|
||||
heading: (c.heading * Math.PI) / 180,
|
||||
pitch: (-45 * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
CITIES.at(-1),
|
||||
);
|
||||
await whenSettled(page);
|
||||
}
|
||||
|
||||
const sceneQA = {
|
||||
@@ -1921,7 +1943,7 @@ try {
|
||||
if (button.getAttribute('aria-expanded') !== 'true') button.click();
|
||||
},
|
||||
);
|
||||
await wait(1_200);
|
||||
await whenFramesDrawn(page);
|
||||
check(
|
||||
'the CONTEXT panel opens from its own header button',
|
||||
await page.$eval(
|
||||
@@ -1937,12 +1959,12 @@ try {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await wait(6_000);
|
||||
await whenFramesDrawn(page);
|
||||
check('transit is on before entering a Context mode', await isOn());
|
||||
|
||||
// CONTACTS
|
||||
await page.click('#global-context-flights-btn');
|
||||
await wait(18_000);
|
||||
await whenFramesDrawn(page);
|
||||
const inContacts = {
|
||||
transit: await isOn(),
|
||||
vehicles: await page.evaluate(
|
||||
@@ -1959,7 +1981,7 @@ try {
|
||||
JSON.stringify(inContacts),
|
||||
);
|
||||
await page.click('#global-context-flights-btn');
|
||||
await wait(12_000);
|
||||
await whenFramesDrawn(page);
|
||||
check('leaving Contacts restores Transit', await isOn());
|
||||
|
||||
// SPACE MISSIONS
|
||||
@@ -1968,9 +1990,9 @@ try {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await wait(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
await page.click('#global-context-missions-btn');
|
||||
await wait(20_000);
|
||||
await whenFramesDrawn(page);
|
||||
const blocked = await page.evaluate(async () => {
|
||||
const settled = await window.__godsEyeView.dataManager.setEnabled(
|
||||
'transit',
|
||||
@@ -1988,7 +2010,7 @@ try {
|
||||
JSON.stringify(blocked),
|
||||
);
|
||||
await page.click('#global-context-missions-btn');
|
||||
await wait(12_000);
|
||||
await whenFramesDrawn(page);
|
||||
check(
|
||||
'leaving Space Missions leaves Transit off, as it was on entry',
|
||||
(await isOn()) === false,
|
||||
@@ -2017,7 +2039,7 @@ try {
|
||||
error.stack || error.message,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(
|
||||
failures
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
/** Exercise the real UI disposal path, including unrelated resource owners. */
|
||||
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();
|
||||
@@ -24,11 +23,10 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.styleManager?._dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__godsEyeView?.styleManager?._dataManager,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
const result = await page.evaluate(async () => {
|
||||
const ui = window.__godsEyeView.styleManager;
|
||||
const counts = {};
|
||||
@@ -287,6 +285,6 @@ try {
|
||||
errors.length === 0,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (failures) process.exitCode = 1;
|
||||
+22
-55
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-vessel-cards.mjs — operator-side visual proof harness for AIS cards in
|
||||
* the shared world-overlay host.
|
||||
@@ -26,10 +28,10 @@
|
||||
* The name and manifest both record the data source; the manifest also records
|
||||
* headless/headful launch mode and the browser's WebGL renderer.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const getOpt = (name, dflt) => {
|
||||
@@ -220,28 +222,7 @@ function syntheticVesselRows(portKey, port) {
|
||||
}));
|
||||
}
|
||||
|
||||
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 { /* fall through to Puppeteer's cache */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`\nAIS Vessel Cards — visual proof harness`);
|
||||
@@ -265,19 +246,9 @@ async function main() {
|
||||
|
||||
fs.mkdirSync(OUT_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-angle=metal',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1600,900',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1600,900'],
|
||||
});
|
||||
|
||||
let failures = 0;
|
||||
@@ -312,10 +283,10 @@ async function main() {
|
||||
if (ENGINE)
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.layers, { timeout: 180000 });
|
||||
else
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 60000 });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, { timeout: 60000 });
|
||||
|
||||
// Frame the port (kill the intro flight first) and enable the layer.
|
||||
const dataEvidence = await page.evaluate(async ({ p, dataMode, fixtureRows, engineNeutral }) => {
|
||||
const dataEvidence = await evaluateWithGeo(page, async (qaGeo, { p, dataMode, fixtureRows, engineNeutral }) => {
|
||||
const gev = window.__godsEyeView;
|
||||
if (engineNeutral) {
|
||||
const geo = await import('/src/geo/index.js');
|
||||
@@ -325,16 +296,14 @@ async function main() {
|
||||
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
|
||||
});
|
||||
} else {
|
||||
const v = gev.viewer;
|
||||
v.camera.cancelFlight?.();
|
||||
v.scene.tweens?.removeAll?.();
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
const carto = {
|
||||
longitude: (p.lon * Math.PI) / 180,
|
||||
latitude: (p.lat * Math.PI) / 180,
|
||||
height: p.height,
|
||||
};
|
||||
v.camera.setView({
|
||||
destination: v.scene.globe.ellipsoid.cartographicToCartesian(carto),
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian(carto),
|
||||
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
|
||||
});
|
||||
}
|
||||
@@ -347,7 +316,7 @@ async function main() {
|
||||
throw new Error(`Synthetic AIS injection failed (${result?.count || 0}/${fixtureRows.length})`);
|
||||
}
|
||||
if (engineNeutral) gev.engine.render.request('qa-vessel-cards');
|
||||
else gev.viewer.scene.requestRender?.();
|
||||
else window.__godsEyeView.engine.render.request?.();
|
||||
return { mode: 'synthetic', injected: result.count };
|
||||
}, { p: port, dataMode: DATA_MODE, fixtureRows: syntheticRows, engineNeutral: Boolean(ENGINE) });
|
||||
console.log(
|
||||
@@ -361,7 +330,7 @@ async function main() {
|
||||
const gev = window.__godsEyeView;
|
||||
const tilesLoaded = engineNeutral
|
||||
? gev.engine.surface.tilesLoaded()
|
||||
: gev.tileset?.tilesLoaded;
|
||||
: window.__godsEyeView.engine.surface.tilesLoaded();
|
||||
const ais = gev.dataManager.getAll().find((l) => l.id === 'ais-live-vessels');
|
||||
const count = ais?.stats?.count ?? 0;
|
||||
return tilesLoaded && count > 0;
|
||||
@@ -372,7 +341,7 @@ async function main() {
|
||||
// Re-assert the port framing: on slow (SwiftShader) runs the app's
|
||||
// intro flight can start AFTER the first cancelFlight and land mid-wait,
|
||||
// dragging the camera back to the boot city before the screenshot.
|
||||
await page.evaluate(async (p, engineNeutral) => {
|
||||
await evaluateWithGeo(page, async (qaGeo, p, engineNeutral) => {
|
||||
const gev = window.__godsEyeView;
|
||||
if (engineNeutral) {
|
||||
const geo = await import('/src/geo/index.js');
|
||||
@@ -382,44 +351,42 @@ async function main() {
|
||||
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
|
||||
});
|
||||
} else {
|
||||
const v = gev.viewer;
|
||||
v.camera.cancelFlight?.();
|
||||
v.scene.tweens?.removeAll?.();
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
const carto = {
|
||||
longitude: (p.lon * Math.PI) / 180,
|
||||
latitude: (p.lat * Math.PI) / 180,
|
||||
height: p.height,
|
||||
};
|
||||
v.camera.setView({
|
||||
destination: v.scene.globe.ellipsoid.cartographicToCartesian(carto),
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian(carto),
|
||||
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
|
||||
});
|
||||
}
|
||||
}, port, Boolean(ENGINE));
|
||||
// A changed camera invalidates the previous settled result. Let the new
|
||||
// view schedule its tiles before checking readiness again.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
await whenFramesDrawn(page);
|
||||
const resettled = await page
|
||||
.waitForFunction((engineNeutral) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const ais = gev.dataManager.getAll().find((l) => l.id === 'ais-live-vessels');
|
||||
const tilesLoaded = engineNeutral
|
||||
? gev.engine.surface.tilesLoaded()
|
||||
: gev.tileset?.tilesLoaded;
|
||||
: window.__godsEyeView.engine.surface.tilesLoaded();
|
||||
return tilesLoaded && (ais?.stats?.count ?? 0) > 0;
|
||||
}, { timeout: 60000, polling: 500 }, Boolean(ENGINE))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
// Let two visibility/declutter cycles (800 ms) and host rendering settle.
|
||||
await new Promise((r) => setTimeout(r, 2500));
|
||||
await whenFramesDrawn(page);
|
||||
const expectedFixtureIds = syntheticRows.map((row) => row.mmsi);
|
||||
const overlayEvidence = await page.evaluate((fixtureIds, engineNeutral) => {
|
||||
const diagnostics = window.__gevWorldOverlay?.getDiagnostics?.();
|
||||
const canvas = window.__godsEyeView?.engine?.host?.canvas;
|
||||
const gl = engineNeutral
|
||||
? canvas?.getContext('webgl2') || canvas?.getContext('webgl')
|
||||
: window.__godsEyeView?.viewer?.scene?.context?._gl;
|
||||
: window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
const debugInfo = gl?.getExtension?.('WEBGL_debug_renderer_info');
|
||||
const seam = window.__godsEyeView?.dataManager?.layers
|
||||
?.get('ais-live-vessels')?.module?.__focusEvidence;
|
||||
@@ -485,7 +452,7 @@ async function main() {
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
console.log('\nRetained PNG+JSON pairs are local QA evidence; each basename records live or synthetic provenance.');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-vessel-datum.mjs — assertion harness for the AIS vessel vertical-datum
|
||||
* pass (docs/superpowers/specs/2026-07-27-vessel-datum-design.md).
|
||||
@@ -25,9 +26,9 @@
|
||||
* --engine cesium|navara puts ?engine= on the URL; without it, the app's
|
||||
* default engine.
|
||||
*/
|
||||
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, dflt) => {
|
||||
@@ -67,28 +68,7 @@ const NEAR_DEG = 1.0;
|
||||
/** Max billboards sampled for the depth-test sweep. */
|
||||
const SAMPLE_CAP = 500;
|
||||
|
||||
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 { /* fall through to Puppeteer's cache */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-page probe on either engine (the QA probe): the chevrons the layer
|
||||
@@ -160,20 +140,9 @@ async function main() {
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
const chromeExecutable = findChromeExecutable();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-gl=angle',
|
||||
'--use-angle=metal',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1600,900',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1600,900'],
|
||||
});
|
||||
|
||||
let failures = 0;
|
||||
@@ -243,20 +212,16 @@ async function main() {
|
||||
// Visual proof — re-assert the framing (the app's intro flight can land
|
||||
// mid-probe on slow runs), wait for the photoreal tileset, screenshot.
|
||||
await page.evaluate(frameEngine, port, false);
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => window.__godsEyeView?.engine?.surface?.tilesLoaded?.(),
|
||||
{ timeout: 60000, polling: 500 },
|
||||
)
|
||||
await whenTilesLoaded(page, { timeoutMs: 60000 })
|
||||
.catch(() => console.log(' (tileset settle timeout — screenshotting anyway)'));
|
||||
await new Promise((r) => setTimeout(r, 2500));
|
||||
await whenSettled(page);
|
||||
const outPath = path.join(OUT_DIR, `vessel-datum-${ENGINE ? `${ENGINE}-` : ''}${key}.png`);
|
||||
await page.screenshot({ path: outPath });
|
||||
console.log(` [SHOT] ${outPath}`);
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
console.log(failures ? `\n\x1b[31m${failures} failure(s)\x1b[0m` : '\n\x1b[32mAll datum checks passed\x1b[0m');
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native pickPosition failure injection exercises the Cesium HUD boundary.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* Regression proof for the view-target prewarm crash.
|
||||
*
|
||||
@@ -18,10 +20,11 @@
|
||||
* `QA_LABEL=before` names the evidence files so a before/after pair can sit
|
||||
* side by side in qa-shots/prewarm/.
|
||||
*/
|
||||
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';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const shotsDir = path.join(repoRoot, 'qa-shots', 'prewarm');
|
||||
@@ -30,22 +33,9 @@ const label = process.env.QA_LABEL || 'after';
|
||||
const headful = process.argv.includes('--headful');
|
||||
fs.mkdirSync(shotsDir, { recursive: true });
|
||||
|
||||
const chromeCandidates = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
].filter(Boolean);
|
||||
const executablePath = chromeCandidates.find((candidate) => fs.existsSync(candidate));
|
||||
const browser = await puppeteer.launch({
|
||||
headless: headful ? false : 'new',
|
||||
...(executablePath ? { executablePath } : {}),
|
||||
// Metal is a macOS-only ANGLE backend; anywhere else it fails WebGL init.
|
||||
args: [
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
'--no-sandbox',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [...(process.platform === 'darwin' ? ['--enable-gpu'] : []), '--no-sandbox'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const failures = [];
|
||||
@@ -91,26 +81,22 @@ let rejections = [];
|
||||
try {
|
||||
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
|
||||
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.viewer, { timeout: 90_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.engine, { timeout: 90_000 });
|
||||
await whenSettled(page);
|
||||
// Boot noise (tile / network warm-up) is not what this harness is about.
|
||||
await new Promise((resolve) => setTimeout(resolve, 4_000));
|
||||
await whenSettled(page);
|
||||
pageErrors.length = 0;
|
||||
consoleErrors.length = 0;
|
||||
await page.evaluate(() => { window.__qaPrewarmRejections.length = 0; });
|
||||
|
||||
const legReports = [];
|
||||
for (const leg of FLIGHT_LEGS) {
|
||||
const report = await page.evaluate(async (spec) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
const report = await evaluateWithGeo(page, async (qaGeo, spec) => {
|
||||
// No window.Cesium global (qa-cctv-v2 / qa-height-datum precedent) —
|
||||
// borrow Cartesian3's statics off a live camera-position instance.
|
||||
const Cartesian3 = viewer.camera.position.constructor;
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
await new Promise((resolve) => {
|
||||
viewer.camera.flyTo({
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: Cartesian3.fromDegrees(spec.lon, spec.lat, spec.height),
|
||||
orientation: { heading: 0, pitch: (spec.pitch * Math.PI) / 180, roll: 0 },
|
||||
duration: 1.2,
|
||||
@@ -123,15 +109,14 @@ try {
|
||||
|
||||
// Record what the center pick actually yields so the root cause is
|
||||
// visible in the evidence dump rather than merely asserted.
|
||||
const scene = viewer.scene;
|
||||
const canvas = scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const center = {
|
||||
x: (canvas.clientWidth || canvas.width) / 2,
|
||||
y: (canvas.clientHeight || canvas.height) / 2,
|
||||
};
|
||||
let picked;
|
||||
try {
|
||||
const raw = scene.pickPosition(center);
|
||||
const raw = window.__godsEyeView.engine.picker.pickPosition(center);
|
||||
picked = raw ? { x: raw.x, y: raw.y, z: raw.z } : null;
|
||||
} catch (error) {
|
||||
picked = { threw: String(error?.message || error) };
|
||||
@@ -151,19 +136,19 @@ try {
|
||||
// `scene.pickPosition` with one that returns each degenerate shape in turn.
|
||||
// Everything downstream — the moveEnd prewarm inside requestIdleCallback and
|
||||
// the HUD summary interval — is the real shipped code.
|
||||
const injection = await page.evaluate(async () => {
|
||||
const injection = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const { viewer, styleManager } = window.__godsEyeView;
|
||||
const Cartesian3 = viewer.camera.position.constructor;
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
const shapes = [
|
||||
// Throws inside Cesium's normalize.
|
||||
{ name: 'nan', value: new Cartesian3(Number.NaN, Number.NaN, Number.NaN) },
|
||||
{ name: 'origin', value: new Cartesian3(0, 0, 0) },
|
||||
{ name: 'infinite', value: new Cartesian3(Number.POSITIVE_INFINITY, 0, 0) },
|
||||
{ name: 'nan', value: Cartesian3.create(Number.NaN, Number.NaN, Number.NaN) },
|
||||
{ name: 'origin', value: Cartesian3.create(0, 0, 0) },
|
||||
{ name: 'infinite', value: Cartesian3.create(Number.POSITIVE_INFINITY, 0, 0) },
|
||||
// Converts SILENTLY to a point 6,378 km underground — the quiet failure a
|
||||
// bare non-zero check lets through, which then reverse-geocodes 0°, 0°.
|
||||
{ name: 'core-interior', value: new Cartesian3(500, 0, 0) },
|
||||
{ name: 'core-interior', value: Cartesian3.create(500, 0, 0) },
|
||||
// Finite, but large enough to overflow the geodetic iteration into NaN.
|
||||
{ name: 'absurd-magnitude', value: new Cartesian3(1e155, 0, 0) },
|
||||
{ name: 'absurd-magnitude', value: Cartesian3.create(1e155, 0, 0) },
|
||||
];
|
||||
const scene = viewer.scene;
|
||||
const originalPick = Object.getOwnPropertyDescriptor(scene, 'pickPosition');
|
||||
@@ -175,16 +160,16 @@ try {
|
||||
|
||||
const hudErrors = [];
|
||||
const nudge = (step) => {
|
||||
const carto = viewer.camera.positionCartographic;
|
||||
viewer.camera.setView({
|
||||
const carto = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: Cartesian3.fromRadians(
|
||||
carto.longitude + 0.0015 * (step + 1),
|
||||
carto.latitude,
|
||||
carto.height,
|
||||
),
|
||||
orientation: { heading: viewer.camera.heading, pitch: viewer.camera.pitch, roll: 0 },
|
||||
orientation: { heading: window.__godsEyeView.engine.camera.pose().heading, pitch: window.__godsEyeView.engine.camera.pose().pitch, roll: 0 },
|
||||
});
|
||||
viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -213,7 +198,7 @@ try {
|
||||
}
|
||||
} finally {
|
||||
if (originalPick) Object.defineProperty(scene, 'pickPosition', originalPick);
|
||||
else delete scene.pickPosition;
|
||||
else delete window.__godsEyeView.engine.picker.pickPosition;
|
||||
}
|
||||
return { hudErrors, shapes: shapes.length, pickCalls };
|
||||
});
|
||||
@@ -348,7 +333,7 @@ try {
|
||||
} catch (error) {
|
||||
check('harness ran to completion', false, String(error?.message || error));
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
console.log(`\n${failures.length ? `FAIL (${failures.length})` : 'PASS'} — evidence in ${path.relative(repoRoot, shotsDir)}/`);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: the native bloom stage verifies the Cesium visual-effects adapter.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Rendered acceptance for style transitions and effect-state ownership. */
|
||||
import { whenSettled, whenTilesLoaded } 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: [
|
||||
'--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,24 +27,21 @@ try {
|
||||
url.searchParams.set('welcome', '0');
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.styleManager?._visualEffects &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__godsEyeView?.styleManager?._visualEffects,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.scene.tweens?.removeAll?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await whenSettled(page);
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (-97.7431 * Math.PI) / 180,
|
||||
latitude: (30.2568 * Math.PI) / 180,
|
||||
height: 1200,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -0.85, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await page.evaluate(
|
||||
() =>
|
||||
@@ -51,26 +49,10 @@ try {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
),
|
||||
);
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let i = 0; i < primitives.length; i++) {
|
||||
const primitive = primitives.get(i);
|
||||
if (
|
||||
typeof primitive.tilesLoaded === 'boolean' &&
|
||||
!primitive.tilesLoaded
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const settled = await whenTilesLoaded(page, { timeoutMs: 60_000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
check('visible tile content settles before visual captures', settled);
|
||||
fs.mkdirSync('qa-shots/visual-effects', { recursive: true });
|
||||
for (const style of [
|
||||
@@ -228,7 +210,7 @@ try {
|
||||
);
|
||||
if (errors.length) console.log(JSON.stringify(errors));
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,15 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/** Browser acceptance for application shortcuts and generated parameter controls. */
|
||||
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: [
|
||||
'--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();
|
||||
@@ -29,16 +28,7 @@ try {
|
||||
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return (
|
||||
cover?.classList.contains('hidden') &&
|
||||
Number(getComputedStyle(cover).opacity) === 0
|
||||
);
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setStyle('normal');
|
||||
@@ -279,7 +269,7 @@ try {
|
||||
);
|
||||
check('runtime has no uncaught browser errors', errors.length === 0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -4,12 +4,12 @@
|
||||
* Run: node --test --test-reporter=spec scripts/qa-voice-auth-focus.mjs
|
||||
* Sign-in responses are fixtures. No credentials or live login are used.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { createServer } from 'node:http';
|
||||
import { after, before, test } from 'node:test';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
const view = await readFile(
|
||||
@@ -63,17 +63,13 @@ before(async () => {
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
origin = `http://127.0.0.1:${server.address().port}`;
|
||||
const chrome = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
browser = await puppeteer.launch({
|
||||
browser = await launchBrowser({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
(existsSync(chrome) ? chrome : await puppeteer.executablePath()),
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser?.close();
|
||||
await closeBrowser(browser);
|
||||
if (server?.listening) await new Promise((resolve) => server.close(resolve));
|
||||
const output = new URL('.gev-logs/voice-auth-focus/', root);
|
||||
await mkdir(output, { recursive: true });
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
* Use a short spoken reply request with three seconds of leading silence.
|
||||
* Saves only usage, transport metadata, and screenshots; never credentials.
|
||||
*/
|
||||
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 { fileURLToPath } from 'node:url';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const url = new URL(process.argv[2] || 'http://127.0.0.1:4189');
|
||||
@@ -27,14 +28,8 @@ url.searchParams.set('welcome', '0');
|
||||
url.searchParams.set('setup', '1');
|
||||
const output = path.join(root, '.gev-logs', 'voice-auth');
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
const executablePath =
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
(fs.existsSync('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome')
|
||||
? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
||||
: puppeteer.executablePath());
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
executablePath,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
@@ -149,12 +144,10 @@ try {
|
||||
};
|
||||
});
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.voiceCommands &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 45_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.voiceCommands, {
|
||||
timeout: 45_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.waitForSelector('[data-cloud-voice-auth-toggle]', {
|
||||
visible: true,
|
||||
});
|
||||
@@ -281,7 +274,7 @@ try {
|
||||
await page
|
||||
?.evaluate(() => window.__godsEyeView?.voiceCommands?.session?.stop())
|
||||
.catch(() => {});
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
fs.writeFileSync(
|
||||
path.join(output, `${auth}-result.json`),
|
||||
`${JSON.stringify(result, null, 2)}\n`,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native pick and tracking probes validate the legacy Cesium voice route.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-voice-routing.mjs — voice-surface QA harness (overnight 2026-07-27).
|
||||
*
|
||||
@@ -36,11 +39,11 @@
|
||||
* routing assertions pin tool NAMES (model wording varies, tool choice must not).
|
||||
*/
|
||||
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import WebSocket from 'ws';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
import { ROUTING_PHRASES } from './voice-bench/phrases.mjs';
|
||||
import { gradeCore, expectLabel } from './voice-bench/grade.mjs';
|
||||
@@ -52,19 +55,6 @@ import {
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const CHROME_CANDIDATES = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
].filter(Boolean);
|
||||
const CHROME_EXECUTABLE = CHROME_CANDIDATES.find((candidate) => {
|
||||
try {
|
||||
return fs.existsSync(candidate);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// ── CLI ─────────────────────────────────────────────────────
|
||||
function getOpt(flag, fallback) {
|
||||
@@ -674,21 +664,9 @@ async function runDeixisRoutingLayer() {
|
||||
|
||||
async function runDeixisLiveLayer() {
|
||||
console.log('\nLAYER 1c — deixis live (in-app text turns)');
|
||||
const browser = await puppeteer.launch({
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(CHROME_EXECUTABLE ? { executablePath: CHROME_EXECUTABLE } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
...(HEADFUL ? [] : ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
'--autoplay-policy=no-user-gesture-required',
|
||||
'--window-size=1500,950',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(HEADFUL ? [] : []), '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream', '--autoplay-policy=no-user-gesture-required', '--window-size=1500,950'],
|
||||
protocolTimeout: 180000,
|
||||
});
|
||||
const appUrl = new URL(APP_URL);
|
||||
@@ -716,11 +694,12 @@ async function runDeixisLiveLayer() {
|
||||
timeout: 60000,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__gevVoiceCommands?.session,
|
||||
() => window.__godsEyeView?.engine && window.__gevVoiceCommands?.session,
|
||||
{ timeout: 120000, polling: 250 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.viewer.camera.cancelFlight(),
|
||||
window.__godsEyeView.engine.camera.cancelFlight(),
|
||||
);
|
||||
await run('fly_to_location', { query: 'Austin, Texas' });
|
||||
await settle(9000);
|
||||
@@ -778,7 +757,7 @@ async function runDeixisLiveLayer() {
|
||||
).find((e) => e.id === p.aircraftId);
|
||||
viewer.scene.render();
|
||||
at = found?.position
|
||||
? viewer.scene.cartesianToCanvasCoordinates(found.position)
|
||||
? window.__godsEyeView.engine.projector.worldToWindow(found.position)
|
||||
: null;
|
||||
}
|
||||
if (at) window.__gevVoiceCommands.pointer.moveTo(at.x, at.y);
|
||||
@@ -865,8 +844,8 @@ async function runDeixisLiveLayer() {
|
||||
}
|
||||
|
||||
const size = await page.evaluate(() => ({
|
||||
w: window.__godsEyeView.viewer.scene.canvas.clientWidth,
|
||||
h: window.__godsEyeView.viewer.scene.canvas.clientHeight,
|
||||
w: window.__godsEyeView.engine.host.canvas.clientWidth,
|
||||
h: window.__godsEyeView.engine.host.canvas.clientHeight,
|
||||
}));
|
||||
const events = await turn('How many flights are around here?', {
|
||||
x: size.w * 0.3,
|
||||
@@ -893,7 +872,7 @@ async function runDeixisLiveLayer() {
|
||||
} catch {
|
||||
/* closing */
|
||||
}
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
console.log(` deixis live turns used: ${turnsUsed}`);
|
||||
}
|
||||
}
|
||||
@@ -986,8 +965,8 @@ function findPickableAircraft(page) {
|
||||
return page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const module = dataManager.layers.get('flights')?.module;
|
||||
const canvas = viewer.scene.canvas;
|
||||
const eye = viewer.camera.positionWC;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const eye = window.__godsEyeView.engine.camera.pose().position;
|
||||
viewer.scene.render();
|
||||
let picks = 0;
|
||||
const candidates = [];
|
||||
@@ -995,7 +974,7 @@ function findPickableAircraft(page) {
|
||||
// Horizon test: far-side contacts still project onto the canvas.
|
||||
const p = entry.position;
|
||||
if (eye.x * p.x + eye.y * p.y + eye.z * p.z < 6356000 ** 2) continue;
|
||||
const at = viewer.scene.cartesianToCanvasCoordinates(entry.position);
|
||||
const at = window.__godsEyeView.engine.projector.worldToWindow(entry.position);
|
||||
if (
|
||||
!at ||
|
||||
at.x < 60 ||
|
||||
@@ -1176,7 +1155,7 @@ async function runDeixisBehavior(page, run, settle) {
|
||||
|
||||
// D2 — bare ground: the chip gives coordinates and "around here" is a radius.
|
||||
const size = await page.evaluate(() => {
|
||||
const canvas = window.__godsEyeView.viewer.scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
return { w: canvas.clientWidth, h: canvas.clientHeight };
|
||||
});
|
||||
let groundTurn = null;
|
||||
@@ -1320,20 +1299,9 @@ async function runBehaviorLayer() {
|
||||
return;
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(CHROME_EXECUTABLE ? { executablePath: CHROME_EXECUTABLE } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
...(HEADFUL ? [] : ['--use-gl=angle', `--use-angle=${process.env.QA_ANGLE || 'swiftshader'}`]),
|
||||
'--mute-audio',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-web-security',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1500,950',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(HEADFUL ? [] : []), '--mute-audio', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1500,950'],
|
||||
protocolTimeout: 180000,
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -1389,14 +1357,15 @@ async function runBehaviorLayer() {
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.viewer &&
|
||||
window.__godsEyeView?.engine &&
|
||||
window.__gevVoiceCommands?.runner &&
|
||||
window.__gevAnnotations,
|
||||
{ timeout: 120000, polling: 250 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
// House rule: the intro flight clobbers teleports issued mid-flight.
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.viewer.camera.cancelFlight(),
|
||||
window.__godsEyeView.engine.camera.cancelFlight(),
|
||||
);
|
||||
|
||||
const run = (name, args) =>
|
||||
@@ -1414,13 +1383,12 @@ async function runBehaviorLayer() {
|
||||
);
|
||||
const camState = () =>
|
||||
page.evaluate(() => {
|
||||
const c = window.__godsEyeView.viewer.camera;
|
||||
const p = c.positionCartographic;
|
||||
const p = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return {
|
||||
lat: (p.latitude * 180) / Math.PI,
|
||||
lon: (p.longitude * 180) / Math.PI,
|
||||
altKm: p.height / 1000,
|
||||
pitchDeg: (c.pitch * 180) / Math.PI,
|
||||
pitchDeg: (window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
|
||||
};
|
||||
});
|
||||
const settle = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
@@ -1465,7 +1433,7 @@ async function runBehaviorLayer() {
|
||||
// final tween frame. Cancel that already-arrived flight so the assertion
|
||||
// measures Radio's camera ownership, not residual navigation motion.
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.viewer.camera.cancelFlight(),
|
||||
window.__godsEyeView.engine.camera.cancelFlight(),
|
||||
);
|
||||
await settle(250);
|
||||
const radioCameraBefore = await camState();
|
||||
@@ -1832,7 +1800,7 @@ async function runBehaviorLayer() {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView.viewer.camera.positionCartographic.height /
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height /
|
||||
1000 >
|
||||
12000,
|
||||
{ timeout: 30_000, polling: 250 },
|
||||
@@ -1853,7 +1821,7 @@ async function runBehaviorLayer() {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView.viewer.camera.positionCartographic.height /
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height /
|
||||
1000 <
|
||||
900,
|
||||
{ timeout: 40_000, polling: 250 },
|
||||
@@ -1923,7 +1891,7 @@ async function runBehaviorLayer() {
|
||||
// (6c) move_camera orbit ONCE: bounded eased ~30° heading advance.
|
||||
const heading = () =>
|
||||
page.evaluate(
|
||||
() => (window.__godsEyeView.viewer.camera.heading * 180) / Math.PI,
|
||||
() => (window.__godsEyeView.engine.camera.pose().heading * 180) / Math.PI,
|
||||
);
|
||||
let h0 = await heading();
|
||||
r = await run('move_camera', { motion: 'orbit', mode: 'once' });
|
||||
@@ -2105,10 +2073,9 @@ async function runBehaviorLayer() {
|
||||
|
||||
// (6e5) tilt at the clamp answers honestly instead of silently no-oping.
|
||||
await page.evaluate(() => {
|
||||
const c = window.__godsEyeView.viewer.camera;
|
||||
c.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
orientation: {
|
||||
heading: c.heading,
|
||||
heading: window.__godsEyeView.engine.camera.pose().heading,
|
||||
pitch: (-5.2 * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
@@ -2395,7 +2362,7 @@ async function runBehaviorLayer() {
|
||||
String(e?.message || e).slice(0, 200),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2425,11 +2392,10 @@ async function runBundledOutlineScenarios({
|
||||
const timings = [];
|
||||
for (const scenario of BUNDLED_OUTLINES) {
|
||||
const label = `behavior: "outline ${scenario.target}" draws the bundled ${scenario.pack} outline`;
|
||||
await page.evaluate(({ lat, lon }) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo, { lat, lon }) => {
|
||||
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: 9000,
|
||||
@@ -2529,11 +2495,10 @@ async function runGeometryScenarios({ page, run, settle, overpassRequests }) {
|
||||
const shot = (name) => page.screenshot({ path: path.join(shotDir, `${name}.png`) }).catch(() => {});
|
||||
const nominatimStart = nominatimCount();
|
||||
const overpassStart = overpassRequests.length;
|
||||
const teleport = (lat, lon, height, pitch = -90) => page.evaluate(({ lat, lon, height, pitch }) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
const teleport = (lat, lon, height, pitch = -90) => evaluateWithGeo(page, async (qaGeo, { lat, lon, height, pitch }) => {
|
||||
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,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (pitch * Math.PI) / 180, roll: 0 },
|
||||
@@ -2729,11 +2694,10 @@ async function runOutlineAcceptance({
|
||||
const overpassStart = overpassRequests.length;
|
||||
if (SHOT_DIR) fs.mkdirSync(path.resolve(ROOT, SHOT_DIR), { recursive: true });
|
||||
|
||||
const teleport = (ask) => page.evaluate(({ lat, lon, height }) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
const teleport = (ask) => evaluateWithGeo(page, async (qaGeo, { lat, lon, height }) => {
|
||||
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,
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
*
|
||||
* Run: node scripts/qa-voice-wav.mjs http://localhost:4189 [--push-to-talk]
|
||||
*/
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const pushToTalk = process.argv.includes('--push-to-talk');
|
||||
@@ -33,20 +34,9 @@ if (fixtureSha256 !== expectedFixtureSha256) {
|
||||
}
|
||||
|
||||
const appOrigin = new URL(appUrl).origin;
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
executablePath: await puppeteer.executablePath(),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--use-fake-device-for-media-stream',
|
||||
`--use-file-for-fake-audio-capture=${wavPath}%noloop`,
|
||||
'--autoplay-policy=no-user-gesture-required',
|
||||
'--window-size=1440,900',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--use-fake-device-for-media-stream', `--use-file-for-fake-audio-capture=${wavPath}%noloop`, '--autoplay-policy=no-user-gesture-required', '--window-size=1440,900'],
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -65,15 +55,14 @@ try {
|
||||
await page.waitForFunction(() => (
|
||||
window.__godsEyeView?.voiceCommands
|
||||
&& document.getElementById('gev-voice-button')
|
||||
&& document.getElementById('loading-screen')?.classList.contains('hidden')
|
||||
), { timeout: 30_000 });
|
||||
await whenSettled(page);
|
||||
|
||||
const readState = () => page.evaluate(() => {
|
||||
const voice = window.__godsEyeView?.voiceCommands;
|
||||
const radio = window.__godsEyeView?.dataManager?.layers?.get('radio')?.module;
|
||||
const radioState = radio?.getUIState?.() || null;
|
||||
const camera = window.__godsEyeView?.viewer?.camera;
|
||||
const cartographic = camera?.positionCartographic;
|
||||
const cartographic = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return {
|
||||
at: Date.now(),
|
||||
voiceStatus: voice?.status || null,
|
||||
@@ -157,5 +146,5 @@ try {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exitCode = result.ok ? 0 : 1;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-weather-journey — drive the Weather layers the way a person would and
|
||||
* keep the evidence: numbered screenshots plus per-step layer diagnostics.
|
||||
@@ -19,9 +21,9 @@
|
||||
* Requires a running dev server. Screenshots and steps.json land in
|
||||
* <out>/<label>/.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const arg = (name, fallback) => {
|
||||
@@ -41,13 +43,8 @@ mkdirSync(outDir, { recursive: true });
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const WEATHER = ['weather-radar', 'weather-satellite', 'weather-lightning'];
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: flag('--headless') ? 'new' : false,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find((p) =>
|
||||
existsSync(p),
|
||||
),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
defaultViewport: null,
|
||||
args: [
|
||||
@@ -95,10 +92,10 @@ try {
|
||||
}
|
||||
});
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, {
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
await sleep(12_000);
|
||||
await whenSettled(page);
|
||||
|
||||
const diagnostics = () =>
|
||||
page.evaluate(
|
||||
@@ -106,9 +103,13 @@ try {
|
||||
const gev = window.__godsEyeView;
|
||||
const dm = gev.dataManager;
|
||||
const out = {
|
||||
basemap: gev.tileset ? 'google-3d' : 'keyless-globe',
|
||||
globeShown: gev.viewer.scene.globe.show,
|
||||
heightM: Math.round(gev.viewer.camera.positionCartographic.height),
|
||||
basemap: gev.engine.maps.photorealActive()
|
||||
? 'google-3d'
|
||||
: 'keyless-globe',
|
||||
imagerySurface: gev.engine.imagery.surface(),
|
||||
heightM: Math.round(
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
),
|
||||
summaryHeight: Math.round(
|
||||
document.querySelector('.weather-summary')?.getBoundingClientRect()
|
||||
.height ?? 0,
|
||||
@@ -151,11 +152,13 @@ try {
|
||||
page.evaluate(
|
||||
(ms) =>
|
||||
new Promise((resolve) => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
let n = 0;
|
||||
const off = scene.postRender.addEventListener(() => {
|
||||
n += 1;
|
||||
});
|
||||
const off =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
() => {
|
||||
n += 1;
|
||||
},
|
||||
);
|
||||
setTimeout(() => {
|
||||
off();
|
||||
resolve(n);
|
||||
@@ -235,13 +238,13 @@ try {
|
||||
heading = 0,
|
||||
duration = 2,
|
||||
) => {
|
||||
await page.evaluate(
|
||||
({ lon, lat, height, pitch, heading, duration }) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { lon, lat, height, pitch, heading, duration }) => {
|
||||
const d2r = Math.PI / 180;
|
||||
v.camera.cancelFlight();
|
||||
v.camera.flyTo({
|
||||
destination: v.scene.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: lon * d2r,
|
||||
latitude: lat * d2r,
|
||||
height,
|
||||
@@ -252,7 +255,7 @@ try {
|
||||
},
|
||||
{ lon, lat, height, pitch, heading, duration },
|
||||
);
|
||||
await sleep(duration * 1000 + 3000);
|
||||
await whenSettled(page);
|
||||
};
|
||||
|
||||
console.log(`\nqa-weather-journey · ${label} · ${url}`);
|
||||
@@ -271,40 +274,40 @@ try {
|
||||
// 2. Wind from its row, then each field, then ECMWF.
|
||||
await clickRow('wind');
|
||||
await waitReady(['wind']);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
await shot('globe-wind-trails', 'Wind on (trails, no field)');
|
||||
await clickChip('wind', 'overlay-speed');
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await shot('globe-wind-speed', 'Wind speed field');
|
||||
await clickChip('wind', 'overlay-temperature');
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await waitReady(['wind']);
|
||||
await shot('globe-wind-temperature', 'Wind + 2 m temperature');
|
||||
await clickChip('wind', 'overlay-pressure');
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await waitReady(['wind']);
|
||||
await shot('globe-wind-pressure', 'Wind + MSL pressure');
|
||||
await clickChip('wind', 'model-ifs');
|
||||
await waitReady(['wind']);
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await shot('globe-wind-ecmwf-pressure', 'ECMWF IFS model');
|
||||
await clickChip('wind', 'model-gfs');
|
||||
await clickChip('wind', 'overlay-speed');
|
||||
await waitReady(['wind']);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
|
||||
// 3. Observed products.
|
||||
for (const id of WEATHER) await clickRow(id);
|
||||
await clickRow('weather-cyclones');
|
||||
await waitReady([...WEATHER]);
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await shot(
|
||||
'globe-all-weather',
|
||||
'Wind speed + radar + regional IR + lightning + cyclones',
|
||||
);
|
||||
await clickChip('weather-satellite', 'clouds');
|
||||
await waitReady(['weather-satellite']);
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await shot('globe-global-infrared', 'Satellite: global infrared mosaic');
|
||||
await clickChip('weather-satellite', 'clouds-regional');
|
||||
await waitReady(['weather-satellite']);
|
||||
@@ -328,21 +331,21 @@ try {
|
||||
return first.textContent.trim();
|
||||
});
|
||||
if (storm) {
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
if (await clickChip('weather-cyclones', 'focus')) {
|
||||
await sleep(6000);
|
||||
await whenSettled(page);
|
||||
await shot('storm-view', `View storm: ${storm}`);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.viewer.camera.zoomIn(400_000),
|
||||
window.__godsEyeView.engine.camera.zoomIn(400_000),
|
||||
);
|
||||
await sleep(4000);
|
||||
await whenSettled(page);
|
||||
await shot('storm-closer', 'Zoomed toward the storm');
|
||||
}
|
||||
} else console.log(' (no active cyclone advisories to follow)');
|
||||
|
||||
// 5. US radar, then regional → city → street over the same spot.
|
||||
await clickChip('weather-radar', 'coverage');
|
||||
await sleep(6000);
|
||||
await whenSettled(page);
|
||||
await shot('us-radar', 'View US radar');
|
||||
await fly(-94.6, 37.5, 600_000);
|
||||
await shot('regional-600km', 'Regional 600 km');
|
||||
@@ -391,7 +394,7 @@ try {
|
||||
await page.mouse.wheel({ deltaY: 240 });
|
||||
await sleep(120);
|
||||
}
|
||||
await sleep(3000);
|
||||
await whenSettled(page);
|
||||
await shot('wheel-out', 'Wheel zoom out ×12');
|
||||
await page.mouse.move(cx - 200, cy);
|
||||
await page.mouse.down();
|
||||
@@ -400,13 +403,13 @@ try {
|
||||
await sleep(30);
|
||||
}
|
||||
await page.mouse.up();
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await shot('drag-pan', 'Drag pan');
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await page.mouse.wheel({ deltaY: -240 });
|
||||
await sleep(120);
|
||||
}
|
||||
await sleep(4000);
|
||||
await whenSettled(page);
|
||||
await shot('wheel-in', 'Wheel zoom in ×10');
|
||||
|
||||
// 8. Back to Earth with everything on.
|
||||
@@ -419,5 +422,5 @@ try {
|
||||
);
|
||||
console.log(`\nWrote ${steps.length} steps to ${outDir}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
+38
-44
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: resolution scale, MSAA and native camera transforms measure Cesium GPU cost.
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* qa-weather-perf — matched-route throughput and idle-work measurement for the
|
||||
* Weather layers (Wind, Rain radar, Satellite clouds, Lightning density).
|
||||
@@ -33,9 +36,9 @@
|
||||
* Requires a running dev server. Screenshots and results.json land in
|
||||
* <out>/<label>/.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const arg = (name, fallback) => {
|
||||
@@ -82,17 +85,9 @@ mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const WEATHER = ['weather-radar', 'weather-satellite', 'weather-lightning'];
|
||||
const AUSTIN = { lon: -97.7431, lat: 30.2672 };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const chromePath =
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find((p) =>
|
||||
existsSync(p),
|
||||
);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: flag('--headless') ? 'new' : false,
|
||||
executablePath: chromePath,
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
defaultViewport: null,
|
||||
args: [
|
||||
@@ -152,15 +147,15 @@ try {
|
||||
failures.clear();
|
||||
};
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, {
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
await sleep(15_000);
|
||||
await whenSettled(page);
|
||||
|
||||
if (scale !== 1)
|
||||
await page.evaluate((scale) => {
|
||||
window.__godsEyeView.viewer.resolutionScale = scale;
|
||||
window.__godsEyeView.viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
}, scale);
|
||||
const env = await page.evaluate(() => {
|
||||
const gev = window.__godsEyeView;
|
||||
@@ -168,7 +163,7 @@ try {
|
||||
return {
|
||||
basemap: gev.tileset ? 'google-3d' : 'keyless-globe',
|
||||
globeShown: scene.globe.show,
|
||||
canvas: `${scene.canvas.width}x${scene.canvas.height}`,
|
||||
canvas: `${window.__godsEyeView.engine.host.canvas.width}x${window.__godsEyeView.engine.host.canvas.height}`,
|
||||
dpr: devicePixelRatio,
|
||||
ua:
|
||||
navigator.userAgent.match(/Chrome\/[\d.]+/)?.[0] || navigator.userAgent,
|
||||
@@ -182,8 +177,7 @@ try {
|
||||
// Park deterministically, disable every layer, and instrument once.
|
||||
await page.evaluate(async () => {
|
||||
const gev = window.__godsEyeView;
|
||||
const v = gev.viewer;
|
||||
v.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
for (const [id, entry] of gev.dataManager.layers)
|
||||
if (entry.enabled) {
|
||||
try {
|
||||
@@ -198,7 +192,7 @@ try {
|
||||
longTaskMs: 0,
|
||||
mutations: 0,
|
||||
});
|
||||
v.scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
P.renders += 1;
|
||||
});
|
||||
try {
|
||||
@@ -224,18 +218,18 @@ try {
|
||||
});
|
||||
|
||||
const pose = (which) =>
|
||||
page.evaluate(
|
||||
({ which, AUSTIN }) => {
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { which, AUSTIN }) => {
|
||||
const C = window.Cesium || null;
|
||||
const v = window.__godsEyeView.viewer;
|
||||
v.camera.cancelFlight();
|
||||
v.camera.lookAtTransform?.(
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.lookAtTransform?.(
|
||||
(C || v.camera.constructor).Matrix4?.IDENTITY ??
|
||||
v.scene.camera.transform,
|
||||
);
|
||||
const ell = v.scene.ellipsoid || v.scene.globe.ellipsoid;
|
||||
const d2r = Math.PI / 180;
|
||||
const destination = ell.cartographicToCartesian({
|
||||
const destination = qaGeo.cartographic.toCartesian({
|
||||
longitude: AUSTIN.lon * d2r,
|
||||
latitude: AUSTIN.lat * d2r,
|
||||
height:
|
||||
@@ -245,7 +239,7 @@ try {
|
||||
? 1_200
|
||||
: 17_368_000,
|
||||
});
|
||||
v.camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination,
|
||||
orientation:
|
||||
which === 'close'
|
||||
@@ -258,13 +252,12 @@ try {
|
||||
|
||||
/** Start a 3 s flight without awaiting it; the caller measures alongside. */
|
||||
const fly = (which) =>
|
||||
page.evaluate(
|
||||
({ which, AUSTIN }) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const ell = v.scene.ellipsoid || v.scene.globe.ellipsoid;
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { which, AUSTIN }) => {
|
||||
const d2r = Math.PI / 180;
|
||||
v.camera.flyTo({
|
||||
destination: ell.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: AUSTIN.lon * d2r,
|
||||
latitude: AUSTIN.lat * d2r,
|
||||
height: which === 'close' ? 1_200 : 17_368_000,
|
||||
@@ -281,13 +274,13 @@ try {
|
||||
|
||||
/** Start an 8 s orbit at 1.8 km range around the Austin anchor (not awaited). */
|
||||
const orbit = () =>
|
||||
page.evaluate(
|
||||
({ AUSTIN }) => {
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { AUSTIN }) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const camera = v.camera;
|
||||
const ell = v.scene.ellipsoid || v.scene.globe.ellipsoid;
|
||||
const d2r = Math.PI / 180;
|
||||
const center = ell.cartographicToCartesian({
|
||||
const center = qaGeo.cartographic.toCartesian({
|
||||
longitude: AUSTIN.lon * d2r,
|
||||
latitude: AUSTIN.lat * d2r,
|
||||
height: 0,
|
||||
@@ -303,9 +296,12 @@ try {
|
||||
const hpr = HPR
|
||||
? new HPR(heading, -45 * d2r, 1800)
|
||||
: { heading, pitch: -45 * d2r, range: 1800 };
|
||||
camera.lookAt(center, hpr);
|
||||
window.__godsEyeView.engine.camera.lookAt(center, hpr);
|
||||
if (elapsed < 8) requestAnimationFrame(step);
|
||||
else camera.lookAtTransform(camera.transform.constructor.IDENTITY);
|
||||
else
|
||||
window.__godsEyeView.engine.camera.lookAtTransform(
|
||||
camera.transform.constructor.IDENTITY,
|
||||
);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
},
|
||||
@@ -317,10 +313,8 @@ try {
|
||||
page.evaluate(
|
||||
({ ms, hold }) =>
|
||||
new Promise((resolve) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const scene = gev.viewer.scene;
|
||||
const P = window.__gevWeatherPerf;
|
||||
const canvas = scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const w0 = canvas.width;
|
||||
const h0 = canvas.height;
|
||||
const r0 = P.renders;
|
||||
@@ -364,7 +358,7 @@ try {
|
||||
rafs += 1;
|
||||
intervals.push(now - last);
|
||||
last = now;
|
||||
if (hold) scene.requestRender();
|
||||
if (hold) window.__godsEyeView.engine.render.request();
|
||||
if (summary && !summary.hidden) {
|
||||
const h =
|
||||
Math.round(summary.getBoundingClientRect().height * 2) / 2;
|
||||
@@ -478,7 +472,7 @@ try {
|
||||
},
|
||||
{ wantPlaying, WEATHER },
|
||||
);
|
||||
await sleep(2_500);
|
||||
await whenFramesDrawn(page);
|
||||
return { ready, readyMs, layers: await diagnostics() };
|
||||
};
|
||||
|
||||
@@ -530,7 +524,7 @@ try {
|
||||
for (const state of ['off', 'history-paused', 'history-playing', 'wind']) {
|
||||
await pose('close');
|
||||
const setup = await applyState(state);
|
||||
await sleep(4_000);
|
||||
await whenFramesDrawn(page);
|
||||
for (let run = 1; run <= 2; run++)
|
||||
record({
|
||||
phase: 'idle',
|
||||
@@ -564,5 +558,5 @@ try {
|
||||
writeFileSync(path.join(outDir, 'summary.md'), lines.join('\n') + '\n');
|
||||
console.log(`\nWrote ${path.join(outDir, 'results.json')} and summary.md`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
+18
-20
@@ -1,3 +1,5 @@
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* Weather history-swap probe: does the scene keep drawing while a draped
|
||||
* weather frame is replaced? Runs against a live dev server, turns on one
|
||||
@@ -8,9 +10,9 @@
|
||||
* node scripts/qa-weather-swap.mjs --url http://localhost:4173 --label google
|
||||
* [--layer weather-satellite] [--product clouds] [--headless]
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const arg = (name, fallback) => {
|
||||
@@ -32,17 +34,11 @@ const product = arg(
|
||||
);
|
||||
const outDir = path.resolve(arg('--out', 'qa-shots/weather-swap'), label);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
// Globe disc in CSS pixels for the 1400×920 viewport at the Earth view.
|
||||
const DISC = { cx: 700, cy: 460, r: 300 };
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: flag('--headless') ? 'new' : false,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find((p) =>
|
||||
existsSync(p),
|
||||
),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
defaultViewport: null,
|
||||
args: [
|
||||
@@ -59,20 +55,20 @@ try {
|
||||
await page.setViewport({ width: 1400, height: 920, deviceScaleFactor: 1 });
|
||||
page.on('pageerror', (e) => console.log(' [pageerror]', e.message));
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, {
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
await sleep(12_000);
|
||||
await page.evaluate(
|
||||
async ({ layerId, product }) => {
|
||||
await whenSettled(page);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { layerId, product }) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const dm = gev.dataManager;
|
||||
for (const [id, e] of dm.layers)
|
||||
if (e.enabled) await dm.setEnabled(id, false, { origin: 'user' });
|
||||
const v = gev.viewer;
|
||||
const d = Math.PI / 180;
|
||||
v.camera.setView({
|
||||
destination: v.scene.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -80 * d,
|
||||
latitude: 25 * d,
|
||||
height: 17_368_000,
|
||||
@@ -96,7 +92,7 @@ try {
|
||||
{ layerId, product },
|
||||
)
|
||||
.catch(() => console.log(' !! first frame not ready'));
|
||||
await sleep(3000);
|
||||
await whenSettled(page);
|
||||
const diag = () =>
|
||||
page.evaluate((layerId) => {
|
||||
const d = window.__godsEyeView.dataManager.layers
|
||||
@@ -126,13 +122,15 @@ try {
|
||||
{ layerId, params },
|
||||
);
|
||||
await step({ step: -1 });
|
||||
// Sample the replacement over six seconds, including frames still loading.
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
await sleep(400);
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await capture(`earlier-${String(i).padStart(2, '0')}`);
|
||||
}
|
||||
await step({ play: true });
|
||||
// History playback is observed over ten seconds.
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
await sleep(500);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
if (i % 2 === 0) await capture(`play-${String(i).padStart(2, '0')}`);
|
||||
}
|
||||
await step({ play: true });
|
||||
@@ -193,5 +191,5 @@ try {
|
||||
`frames=${frames.length} black(>15%)=${blackFrames} max=${(summary.maxBlack * 100).toFixed(1)}% distinctTimes=${distinctTimes}`,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Cesium-only probes: native imagery, primitive and listener counts audit Cesium resource ownership.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* Weather teardown and cross-layer regression probe.
|
||||
*
|
||||
@@ -14,9 +17,9 @@
|
||||
*
|
||||
* node scripts/qa-weather-teardown.mjs --url http://localhost:4173 --label google
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const arg = (name, fallback) => {
|
||||
@@ -42,13 +45,8 @@ const WEATHER = [
|
||||
'weather-cyclones',
|
||||
];
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: flag('--headless') ? 'new' : false,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find((p) =>
|
||||
existsSync(p),
|
||||
),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
defaultViewport: null,
|
||||
args: [
|
||||
@@ -75,19 +73,18 @@ const boot = async () => {
|
||||
if (m.type() === 'error') errors.push(`console: ${m.text()}`.slice(0, 200));
|
||||
});
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, {
|
||||
await page.waitForFunction(() => !!window.__godsEyeView?.engine, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
await sleep(10_000);
|
||||
await page.evaluate(async () => {
|
||||
await whenSettled(page);
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const dm = gev.dataManager;
|
||||
for (const [id, e] of dm.layers)
|
||||
if (e.enabled) await dm.setEnabled(id, false, { origin: 'user' });
|
||||
const v = gev.viewer;
|
||||
const d = Math.PI / 180;
|
||||
v.camera.setView({
|
||||
destination: v.scene.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -95 * d,
|
||||
latitude: 32 * d,
|
||||
height: 4_500_000,
|
||||
@@ -95,7 +92,7 @@ const boot = async () => {
|
||||
orientation: { heading: 0, pitch: -90 * d, roll: 0 },
|
||||
});
|
||||
});
|
||||
await sleep(3000);
|
||||
await whenSettled(page);
|
||||
return { page, errors };
|
||||
};
|
||||
|
||||
@@ -167,12 +164,15 @@ const snapshot = (page) =>
|
||||
dataSources: v.dataSources.length,
|
||||
entities: v.entities.values.length,
|
||||
listeners: {
|
||||
postRender: s.postRender.numberOfListeners,
|
||||
preRender: s.preRender.numberOfListeners,
|
||||
postRender:
|
||||
window.__godsEyeView.engine.frames.postRender.numberOfListeners,
|
||||
preRender:
|
||||
window.__godsEyeView.engine.frames.preRender.numberOfListeners,
|
||||
postUpdate: s.postUpdate.numberOfListeners,
|
||||
moveEnd: v.camera.moveEnd.numberOfListeners,
|
||||
moveStart: v.camera.moveStart.numberOfListeners,
|
||||
changed: v.camera.changed.numberOfListeners,
|
||||
moveEnd: window.__godsEyeView.engine.camera.moveEnd.numberOfListeners,
|
||||
moveStart:
|
||||
window.__godsEyeView.engine.camera.moveStart.numberOfListeners,
|
||||
changed: window.__godsEyeView.engine.camera.changed.numberOfListeners,
|
||||
tick: v.clock.onTick.numberOfListeners,
|
||||
},
|
||||
dom: {
|
||||
@@ -194,7 +194,10 @@ const sample = async (page, seconds) => {
|
||||
window.__qaRenders = 0;
|
||||
window.__qaLong = { count: 0, ms: 0 };
|
||||
const s = window.__godsEyeView.viewer.scene;
|
||||
window.__qaOff = s.postRender.addEventListener(() => window.__qaRenders++);
|
||||
window.__qaOff =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
() => window.__qaRenders++,
|
||||
);
|
||||
window.__qaObs = new PerformanceObserver((list) => {
|
||||
for (const e of list.getEntries()) {
|
||||
window.__qaLong.count++;
|
||||
@@ -203,7 +206,7 @@ const sample = async (page, seconds) => {
|
||||
});
|
||||
window.__qaObs.observe({ entryTypes: ['longtask'] });
|
||||
window.__qaRaf = () => {
|
||||
s.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
window.__qaRafId = requestAnimationFrame(window.__qaRaf);
|
||||
};
|
||||
window.__qaRaf();
|
||||
@@ -313,7 +316,7 @@ try {
|
||||
console.log('weather on', JSON.stringify(weatherOn));
|
||||
results.weatherOnSample = await sample(a.page, 5);
|
||||
for (const id of WEATHER) await setLayer(a.page, id, false);
|
||||
await sleep(4000);
|
||||
await whenFramesDrawn(a.page);
|
||||
const afterOff = await snapshot(a.page);
|
||||
const leak = diff(
|
||||
{ ...baseline, enabled: undefined, weather: undefined },
|
||||
@@ -331,7 +334,7 @@ try {
|
||||
results.afterWeather = [];
|
||||
for (const id of others) {
|
||||
const ok = await setLayer(a.page, id, true);
|
||||
await sleep(9000);
|
||||
await whenFramesDrawn(a.page);
|
||||
const m = await sample(a.page, 5);
|
||||
const snap = await snapshot(a.page);
|
||||
results.afterWeather.push({
|
||||
@@ -354,7 +357,7 @@ try {
|
||||
results.control = [];
|
||||
for (const id of others) {
|
||||
const ok = await setLayer(b.page, id, true);
|
||||
await sleep(9000);
|
||||
await whenFramesDrawn(b.page);
|
||||
const m = await sample(b.page, 5);
|
||||
const snap = await snapshot(b.page);
|
||||
results.control.push({
|
||||
@@ -400,5 +403,5 @@ try {
|
||||
JSON.stringify(results, null, 2),
|
||||
);
|
||||
console.log(`wrote ${path.join(outDir, 'results.json')}`);
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native inertia and render-loop control reproduce the Cesium zoom guard.
|
||||
/** Real-app zoom and tilt regression. Start a dev server, then run this file. */
|
||||
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';
|
||||
|
||||
const origin = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
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',
|
||||
'--enable-unsafe-swiftshader',
|
||||
]),
|
||||
? ['--enable-gpu']
|
||||
: ['--enable-unsafe-swiftshader']),
|
||||
],
|
||||
protocolTimeout: 120_000,
|
||||
});
|
||||
@@ -28,12 +26,12 @@ try {
|
||||
await page.goto(`${origin}/?welcome=0`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.viewer &&
|
||||
window.__godsEyeView?.engine &&
|
||||
window.__godsEyeView.viewer.scene.screenSpaceCameraController
|
||||
.enableInputs &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
.enableInputs,
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
const renderer = await page.evaluate(async () => {
|
||||
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||||
const { maxPitchForHeight } =
|
||||
@@ -41,21 +39,23 @@ try {
|
||||
const { readCameraTargetFrame } =
|
||||
await import('/src/ui/cameraOrientationControls.js');
|
||||
const { viewer, engine } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
viewer.trackedEntity = undefined;
|
||||
// Explicit real renders make each gesture independent of host frame rate.
|
||||
viewer.useDefaultRenderLoop = false;
|
||||
const controller = viewer.scene.screenSpaceCameraController;
|
||||
controller.inertiaZoom = 0;
|
||||
controller.inertiaSpin = 0;
|
||||
const original = viewer.camera.setView.bind(viewer.camera);
|
||||
const original = window.__godsEyeView.engine.camera.setView.bind(
|
||||
viewer.camera,
|
||||
);
|
||||
const qa = {
|
||||
viewer,
|
||||
guardWrites: 0,
|
||||
targetPitch: () => readCameraTargetFrame(engine, viewer)?.pitch,
|
||||
history: [],
|
||||
frame() {
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
viewer.render();
|
||||
qa.history.push({
|
||||
height: viewer.camera._positionCartographic.height,
|
||||
@@ -66,17 +66,17 @@ try {
|
||||
},
|
||||
pose() {
|
||||
return {
|
||||
height: viewer.camera.positionCartographic.height,
|
||||
pitch: viewer.camera.pitch,
|
||||
height: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
pitch: window.__godsEyeView.engine.camera.pose().pitch,
|
||||
maxPitch: maxPitchForHeight(
|
||||
viewer.camera.positionCartographic.height,
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
),
|
||||
guardWrites: qa.guardWrites,
|
||||
};
|
||||
},
|
||||
reset(height = 1_000_000, pitch = -35) {
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.lookAtTransform(C.Matrix4.IDENTITY);
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.lookAtTransform(C.Matrix4.IDENTITY);
|
||||
original({
|
||||
destination: C.Cartesian3.fromDegrees(0, 0, height),
|
||||
orientation: { heading: 0, pitch: C.Math.toRadians(pitch), roll: 0 },
|
||||
@@ -92,7 +92,7 @@ try {
|
||||
return original(options);
|
||||
};
|
||||
window.__zoomGuardQa = qa;
|
||||
const gl = viewer.scene.context._gl;
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
const debug = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return gl.getParameter(debug ? debug.UNMASKED_RENDERER_WEBGL : gl.RENDERER);
|
||||
});
|
||||
@@ -330,5 +330,5 @@ try {
|
||||
assert.deepEqual(errors, [], 'no JavaScript page errors');
|
||||
console.log('[PASS] no JavaScript page errors');
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user