mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Move QA scripts to the shared harness, batch 1 of 4
Each script launches and closes Chrome through scripts/qa/browser.mjs, waits on the application through scripts/qa/ready.mjs instead of the loading screen and fixed pauses, and reads the contract instead of Cesium where the check is not about Cesium itself.
This commit is contained in:
1 parent
731eadc34a
commit
ee697422c9
25 files changed
+587
-881
No files matched your search
@@ -6,12 +6,13 @@
|
|||||||
* native dispatch/disabled/teardown without providers or world calibration writes.
|
* native dispatch/disabled/teardown without providers or world calibration writes.
|
||||||
* Real Scene authoring/playback remains covered by qa-scene-controls.mjs.
|
* 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 fs from 'node:fs';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--use-gl=angle', '--use-angle=swiftshader'],
|
args: [],
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
const errors = [];
|
const errors = [];
|
||||||
@@ -34,11 +35,10 @@ try {
|
|||||||
{ waitUntil: 'domcontentloaded' },
|
{ waitUntil: 'domcontentloaded' },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
() =>
|
() => window.__godsEyeView?.styleManager?._cctvControls,
|
||||||
window.__godsEyeView?.styleManager?._cctvControls &&
|
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
||||||
{ timeout: 60000 },
|
{ timeout: 60000 },
|
||||||
);
|
);
|
||||||
|
await whenSettled(page);
|
||||||
const inventory = await page.evaluate(() => {
|
const inventory = await page.evaluate(() => {
|
||||||
const ids = [
|
const ids = [
|
||||||
'scene-new-btn',
|
'scene-new-btn',
|
||||||
@@ -304,11 +304,10 @@ try {
|
|||||||
localized.searchParams.set('lang', 'ru');
|
localized.searchParams.set('lang', 'ru');
|
||||||
await page.goto(localized.href, { waitUntil: 'domcontentloaded' });
|
await page.goto(localized.href, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
() =>
|
() => window.__godsEyeView?.styleManager?._cctvControls,
|
||||||
window.__godsEyeView?.styleManager?._cctvControls &&
|
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
||||||
{ timeout: 60000 },
|
{ timeout: 60000 },
|
||||||
);
|
);
|
||||||
|
await whenSettled(page);
|
||||||
for (const panel of ['scene-panel', 'cctv-panel']) {
|
for (const panel of ['scene-panel', 'cctv-panel']) {
|
||||||
await page.evaluate((panel) => {
|
await page.evaluate((panel) => {
|
||||||
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
||||||
@@ -329,7 +328,7 @@ try {
|
|||||||
}
|
}
|
||||||
check('no uncaught browser errors', errors.length === 0, errors);
|
check('no uncaught browser errors', errors.length === 0, errors);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
console.log(`RESULT: ${failures} failures`);
|
console.log(`RESULT: ${failures} failures`);
|
||||||
process.exitCode = failures ? 1 : 0;
|
process.exitCode = failures ? 1 : 0;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/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
|
* qa-admin-outlines.mjs — bundled state/province/county outlines, driven like a
|
||||||
* user would drive them.
|
* user would drive them.
|
||||||
@@ -23,11 +24,12 @@
|
|||||||
* timings, frame-time p50/p95 and still-camera frame-difference statistics.
|
* timings, frame-time p50/p95 and still-camera frame-difference statistics.
|
||||||
* Exits nonzero on any failed assertion.
|
* 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 assert from 'node:assert/strict';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
@@ -63,12 +65,11 @@ const result = {
|
|||||||
video: null,
|
video: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: !args.includes('--headful'),
|
headless: true,
|
||||||
protocolTimeout: 300_000,
|
protocolTimeout: 300_000,
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
'--use-gl=angle',
|
|
||||||
'--disable-dev-shm-usage',
|
'--disable-dev-shm-usage',
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-timer-throttling',
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-renderer-backgrounding',
|
||||||
@@ -145,14 +146,10 @@ try {
|
|||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.annotations,
|
() => window.__godsEyeView?.engine && window.__godsEyeView?.annotations,
|
||||||
{ timeout: 90_000, polling: 500 },
|
{ timeout: 90_000, polling: 500 },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await whenSettled(page);
|
||||||
() =>
|
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
||||||
{ timeout: 90_000, polling: 250 },
|
|
||||||
);
|
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
);
|
);
|
||||||
@@ -162,7 +159,7 @@ try {
|
|||||||
{ timeout: 10_000 },
|
{ timeout: 10_000 },
|
||||||
);
|
);
|
||||||
result.renderer = await page.evaluate(() => {
|
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');
|
const debug = gl.getExtension('WEBGL_debug_renderer_info');
|
||||||
return debug
|
return debug
|
||||||
? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)
|
? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)
|
||||||
@@ -177,7 +174,7 @@ try {
|
|||||||
window.__qaOutlines = {
|
window.__qaOutlines = {
|
||||||
fly(view, duration) {
|
fly(view, duration) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
viewer.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
viewer.camera.flyTo({
|
viewer.camera.flyTo({
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
destination: Cesium.Cartesian3.fromDegrees(
|
||||||
view.lon,
|
view.lon,
|
||||||
@@ -264,20 +261,7 @@ try {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const settle = () =>
|
const settle = () =>
|
||||||
page
|
whenTilesLoaded(page, { timeoutMs: 60_000 }).catch(() => {});
|
||||||
.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(() => {});
|
|
||||||
|
|
||||||
async function shot(name) {
|
async function shot(name) {
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
@@ -317,11 +301,14 @@ try {
|
|||||||
// built the mark's primitives (each tick updates them before render).
|
// built the mark's primitives (each tick updates them before render).
|
||||||
const { viewer } = window.__godsEyeView;
|
const { viewer } = window.__godsEyeView;
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
const off = viewer.scene.postRender.addEventListener(() => {
|
const off =
|
||||||
if (!viewer.dataSourceDisplay.ready) return;
|
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||||
off();
|
() => {
|
||||||
resolve();
|
if (!viewer.dataSourceDisplay.ready) return;
|
||||||
});
|
off();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
const drawnMs = performance.now() - started;
|
const drawnMs = performance.now() - started;
|
||||||
const mark = window.__godsEyeView.annotations
|
const mark = window.__godsEyeView.annotations
|
||||||
@@ -343,7 +330,7 @@ try {
|
|||||||
target,
|
target,
|
||||||
flyTo,
|
flyTo,
|
||||||
);
|
);
|
||||||
if (flyTo) await new Promise((r) => setTimeout(r, 2200));
|
if (flyTo) await whenSettled(page);
|
||||||
await settle();
|
await settle();
|
||||||
// Hold still: the drawn mark must not rebuild or grow.
|
// Hold still: the drawn mark must not rebuild or grow.
|
||||||
marker = `still:${name}`;
|
marker = `still:${name}`;
|
||||||
@@ -551,7 +538,7 @@ try {
|
|||||||
.screenshot({ path: path.join(shots, 'admin-outlines-failure.png') })
|
.screenshot({ path: path.join(shots, 'admin-outlines-failure.png') })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recordVideo && frames.length > 1) {
|
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.
|
* 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
|
* Run: node scripts/qa-air-quality.mjs --url http://localhost:4173
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
const getOpt = (name, dflt) => {
|
const getOpt = (name, dflt) => {
|
||||||
@@ -27,19 +29,13 @@ function record(name, ok, detail) {
|
|||||||
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
||||||
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
|
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
|
||||||
}
|
}
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let exitCode = 0;
|
let exitCode = 0;
|
||||||
const consoleErrors = [];
|
const consoleErrors = [];
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: 'new',
|
headless: true,
|
||||||
args: [
|
args: ['--enable-unsafe-swiftshader', '--no-sandbox', '--disable-dev-shm-usage'],
|
||||||
'--use-gl=swiftshader',
|
|
||||||
'--enable-unsafe-swiftshader',
|
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-dev-shm-usage',
|
|
||||||
],
|
|
||||||
defaultViewport: { width: 1400, height: 900 },
|
defaultViewport: { width: 1400, height: 900 },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,10 +48,10 @@ async function main() {
|
|||||||
|
|
||||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||||
{ timeout: 90000 },
|
{ timeout: 90000 },
|
||||||
);
|
);
|
||||||
await sleep(1500);
|
await whenSettled(page);
|
||||||
|
|
||||||
const stats = await page.evaluate(async () => {
|
const stats = await page.evaluate(async () => {
|
||||||
const dm = window.__godsEyeView.dataManager;
|
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
|
// 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,
|
// over Canada before asserting on it — the default view is over Austin,
|
||||||
// where an empty in-view list is correct and proves nothing.
|
// where an empty in-view list is correct and proves nothing.
|
||||||
await page.evaluate(() => {
|
await evaluateWithGeo(page, async (qaGeo) => {
|
||||||
const gev = window.__godsEyeView;
|
|
||||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
|
||||||
const d2r = Math.PI / 180;
|
const d2r = Math.PI / 180;
|
||||||
try {
|
try {
|
||||||
gev.viewer.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
} catch {
|
} catch {
|
||||||
/* no flight in progress */
|
/* no flight in progress */
|
||||||
}
|
}
|
||||||
gev.viewer.camera.setView({
|
window.__godsEyeView.engine.camera.setView({
|
||||||
destination: ellipsoid.cartographicToCartesian({
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
longitude: -100 * d2r,
|
longitude: -100 * d2r,
|
||||||
latitude: 55 * d2r,
|
latitude: 55 * d2r,
|
||||||
height: 4_000_000,
|
height: 4_000_000,
|
||||||
}),
|
}),
|
||||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||||
});
|
});
|
||||||
gev.viewer.scene.requestRender?.();
|
window.__godsEyeView.engine.render.request?.();
|
||||||
});
|
});
|
||||||
await sleep(2500);
|
await whenSettled(page);
|
||||||
|
|
||||||
const controls = await page.evaluate(
|
const controls = await page.evaluate(
|
||||||
() =>
|
() =>
|
||||||
@@ -154,7 +148,7 @@ async function main() {
|
|||||||
console.error('\x1b[31mHarness error:\x1b[0m', e);
|
console.error('\x1b[31mHarness error:\x1b[0m', e);
|
||||||
exitCode = 3;
|
exitCode = 3;
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pass = results.filter((r) => r.ok === true).length;
|
const pass = results.filter((r) => r.ok === true).length;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/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
|
* qa-aircraft-heading.mjs — aircraft display-heading checks
|
||||||
* (path-derived, rate-limited display course; Batch 3 overnight run).
|
* (path-derived, rate-limited display course; Batch 3 overnight run).
|
||||||
@@ -97,9 +98,10 @@
|
|||||||
* Exits non-zero if any assertion fails. Never commits anything.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import { installNavaraHeadingProbe } from './qa-heading-navara.mjs';
|
import { installNavaraHeadingProbe } from './qa-heading-navara.mjs';
|
||||||
import { stubPlanespotters } from './qa-photo-stub.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 = path.resolve('qa-shots/aircraft-heading');
|
||||||
const SHOT_DIR_V2 = path.resolve('qa-shots/heading-v2');
|
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
|
// PASS/FAIL reporting
|
||||||
@@ -157,7 +140,7 @@ function record(name, ok, detail) {
|
|||||||
|
|
||||||
async function observeTrackedHostPaint(page, timeoutMs = 5000) {
|
async function observeTrackedHostPaint(page, timeoutMs = 5000) {
|
||||||
return page.evaluate((boundedTimeoutMs) => new Promise((resolve) => {
|
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 scene = viewer?.scene;
|
||||||
const history = [];
|
const history = [];
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
@@ -455,20 +438,9 @@ async function main() {
|
|||||||
fs.mkdirSync(SHOT_DIR, { recursive: true });
|
fs.mkdirSync(SHOT_DIR, { recursive: true });
|
||||||
fs.mkdirSync(SHOT_DIR_V2, { recursive: true });
|
fs.mkdirSync(SHOT_DIR_V2, { recursive: true });
|
||||||
|
|
||||||
const chromeExecutable = findChromeExecutable();
|
const browser = await launchBrowser({
|
||||||
const browser = await puppeteer.launch({
|
headless: true,
|
||||||
headless: HEADFUL ? false : 'new',
|
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(HEADFUL ? [] : []), '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
|
||||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
|
||||||
args: [
|
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-setuid-sandbox',
|
|
||||||
...(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 consoleErrors = [];
|
const consoleErrors = [];
|
||||||
@@ -588,8 +560,9 @@ async function main() {
|
|||||||
() => window.__godsEyeView?.layers && window.__godsEyeView.dataManager,
|
() => window.__godsEyeView?.layers && window.__godsEyeView.dataManager,
|
||||||
{ timeout: 60000, polling: 200 }
|
{ timeout: 60000, polling: 200 }
|
||||||
);
|
);
|
||||||
|
await whenSettled(page);
|
||||||
await page.evaluate(() => window.__godsEyeView.styleManager.initialRestorePromise);
|
await page.evaluate(() => window.__godsEyeView.styleManager.initialRestorePromise);
|
||||||
await page.waitForFunction(() => document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 180000 });
|
await whenSettled(page);
|
||||||
await page.evaluate(() => window.__godsEyeView.styleManager.orbitController.stop());
|
await page.evaluate(() => window.__godsEyeView.styleManager.orbitController.stop());
|
||||||
console.log(' App globals ready.');
|
console.log(' App globals ready.');
|
||||||
|
|
||||||
@@ -730,7 +703,7 @@ async function main() {
|
|||||||
}, { timeout: 40000, polling: 250 }, TURN.flights[0].icao).then(() => true).catch(() => false);
|
}, { 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');
|
record('flights: tracked 3D model rendered (ready+shown)', flModelUp, flModelUp ? 'model up' : 'model never became ready');
|
||||||
if (!flModelUp) { finish(); return; }
|
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 readoutObservation = await observeTrackedHostPaint(page);
|
||||||
const readoutHost = await page.evaluate(() => ({
|
const readoutHost = await page.evaluate(() => ({
|
||||||
@@ -780,7 +753,7 @@ async function main() {
|
|||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.__godsEyeView.styleManager._setDetectionMode('DENSE');
|
window.__godsEyeView.styleManager._setDetectionMode('DENSE');
|
||||||
});
|
});
|
||||||
await sleep(800);
|
await whenFramesDrawn(page);
|
||||||
const shots = [
|
const shots = [
|
||||||
['flights-far-orbitA.png', async () => { /* default follow framing */ }],
|
['flights-far-orbitA.png', async () => { /* default follow framing */ }],
|
||||||
['flights-far-orbitB.png', () => page.evaluate(() => { (window.__headingView ?? window.__godsEyeView.viewer).camera.rotateRight(1.9); })],
|
['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) {
|
for (const [name, move] of shots) {
|
||||||
try { await move(); } catch (e) { console.log(` (camera move for ${name} failed: ${e.message})`); }
|
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) });
|
await page.screenshot({ path: path.join(SHOT_DIR, name) });
|
||||||
console.log(` saved ${name}`);
|
console.log(` saved ${name}`);
|
||||||
}
|
}
|
||||||
@@ -819,7 +792,7 @@ async function main() {
|
|||||||
}, { timeout: 40000, polling: 250 }, TURN.military[0].hex).then(() => true).catch(() => false);
|
}, { 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');
|
record('military: tracked 3D model rendered (ready+shown)', milModelUp, milModelUp ? 'model up' : 'model never became ready');
|
||||||
if (milModelUp) {
|
if (milModelUp) {
|
||||||
await sleep(1500);
|
await whenFramesDrawn(page);
|
||||||
const milSamples = await page.evaluate(
|
const milSamples = await page.evaluate(
|
||||||
(icao) => window.__sampleModelCourse(icao, 180, 40000),
|
(icao) => window.__sampleModelCourse(icao, 180, 40000),
|
||||||
TURN.military[0].hex,
|
TURN.military[0].hex,
|
||||||
@@ -841,7 +814,7 @@ async function main() {
|
|||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.__godsEyeView.styleManager._setDetectionMode('DENSE');
|
window.__godsEyeView.styleManager._setDetectionMode('DENSE');
|
||||||
});
|
});
|
||||||
await sleep(800);
|
await whenFramesDrawn(page);
|
||||||
for (const [name, move] of [
|
for (const [name, move] of [
|
||||||
['military-orbitA.png', async () => { }],
|
['military-orbitA.png', async () => { }],
|
||||||
['military-orbitB.png', () => page.evaluate(() => { (window.__headingView ?? window.__godsEyeView.viewer).camera.rotateRight(1.6); })],
|
['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})`); }
|
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) });
|
await page.screenshot({ path: path.join(SHOT_DIR, name) });
|
||||||
console.log(` saved ${name}`);
|
console.log(` saved ${name}`);
|
||||||
}
|
}
|
||||||
@@ -892,7 +865,7 @@ async function main() {
|
|||||||
// ---- Phase 3 — hovering helicopter --------------------------------------
|
// ---- Phase 3 — hovering helicopter --------------------------------------
|
||||||
console.log('\nPhase 3 — hovering helicopter (klass=helicopter, drift ~4 m, track flips ±45°)');
|
console.log('\nPhase 3 — hovering helicopter (klass=helicopter, drift ~4 m, track flips ±45°)');
|
||||||
if (await trackFlightsAndWaitModel(TURN.hover.icao, 'hover-heli')) {
|
if (await trackFlightsAndWaitModel(TURN.hover.icao, 'hover-heli')) {
|
||||||
await sleep(1500);
|
await whenFramesDrawn(page);
|
||||||
const heliSamples = await sampleCourse(180, 35000, TURN.hover.icao);
|
const heliSamples = await sampleCourse(180, 35000, TURN.hover.icao);
|
||||||
analyzeHover('hover-heli', heliSamples, {});
|
analyzeHover('hover-heli', heliSamples, {});
|
||||||
for (const [name, waitMs] of [['hover-heli-T0.png', 0], ['hover-heli-T8s.png', 8000]]) {
|
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 --------------
|
// ---- 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)');
|
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')) {
|
if (await trackFlightsAndWaitModel(TURN.slow.icao, 'slow-turn')) {
|
||||||
await sleep(1500);
|
await whenFramesDrawn(page);
|
||||||
const slowSamples = await sampleCourse(180, 40000, TURN.slow.icao);
|
const slowSamples = await sampleCourse(180, 40000, TURN.slow.icao);
|
||||||
analyzeSlowTurn('slow-turn', slowSamples, { scoreProgress: false });
|
analyzeSlowTurn('slow-turn', slowSamples, { scoreProgress: false });
|
||||||
// Exercise the production dt clamp with render gaps above
|
// 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); })],
|
['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})`); }
|
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) });
|
await page.screenshot({ path: path.join(SHOT_DIR_V2, name) });
|
||||||
console.log(` saved ${name}`);
|
console.log(` saved ${name}`);
|
||||||
}
|
}
|
||||||
@@ -1097,7 +1070,7 @@ async function main() {
|
|||||||
|
|
||||||
finish();
|
finish();
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
|
|||||||
+12
-29
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/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.
|
* 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
|
* Writes <out>/result.json, <out>/clip.mp4 (CDP screencast with real frame
|
||||||
* timing) and per-stop screenshots. Needs ffmpeg on PATH.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { installAlprFixture } from './fixtures/layers/alpr.mjs';
|
import { installAlprFixture } from './fixtures/layers/alpr.mjs';
|
||||||
@@ -163,14 +165,12 @@ const STOPS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: !argv.includes('--headful'),
|
headless: true,
|
||||||
protocolTimeout: 300_000,
|
protocolTimeout: 300_000,
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
...(process.platform === 'darwin'
|
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||||
? ['--use-angle=metal', '--enable-gpu']
|
|
||||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-timer-throttling',
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-renderer-backgrounding',
|
||||||
'--disable-backgrounding-occluded-windows',
|
'--disable-backgrounding-occluded-windows',
|
||||||
@@ -233,12 +233,10 @@ try {
|
|||||||
const url = new URL(appUrl);
|
const url = new URL(appUrl);
|
||||||
url.searchParams.set('welcome', '0');
|
url.searchParams.set('welcome', '0');
|
||||||
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||||
() =>
|
timeout: 120_000,
|
||||||
window.__godsEyeView?.dataManager &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 120_000 },
|
|
||||||
);
|
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
);
|
);
|
||||||
@@ -286,22 +284,7 @@ try {
|
|||||||
await recorder.start();
|
await recorder.start();
|
||||||
|
|
||||||
async function waitTiles(maxMs = 20_000) {
|
async function waitTiles(maxMs = 20_000) {
|
||||||
await page
|
await whenTilesLoaded(page, { timeoutMs: maxMs }).catch(() => {});
|
||||||
.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(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [passIndex, mode] of passes.entries()) {
|
for (const [passIndex, mode] of passes.entries()) {
|
||||||
@@ -653,7 +636,7 @@ try {
|
|||||||
path.join(outDir, 'result.json'),
|
path.join(outDir, 'result.json'),
|
||||||
JSON.stringify(result, null, 1),
|
JSON.stringify(result, null, 1),
|
||||||
);
|
);
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
|
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
|
||||||
|
|||||||
+13
-13
@@ -1,4 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/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.
|
* 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/]
|
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/]
|
||||||
* Exit: 0 pass, 1 fail, 2 the anime style was not reached.
|
* Exit: 0 pass, 1 fail, 2 the anime style was not reached.
|
||||||
*/
|
*/
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
const i = process.argv.indexOf('--url');
|
const i = process.argv.indexOf('--url');
|
||||||
const URL_ =
|
const URL_ =
|
||||||
i > 0 && process.argv[i + 1] ? process.argv[i + 1] : 'http://localhost:4333/';
|
i > 0 && process.argv[i + 1] ? process.argv[i + 1] : 'http://localhost:4333/';
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: 'new',
|
headless: true,
|
||||||
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -41,17 +43,15 @@ try {
|
|||||||
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
});
|
});
|
||||||
|
await whenSettled(page);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
() => {
|
() => {
|
||||||
const sky = window.__godsEyeView.viewer.scene.skyBox;
|
const sky = window.__godsEyeView.viewer.scene.skyBox;
|
||||||
return (
|
return sky?._milkyWayCubeMap && sky?._starCommand;
|
||||||
sky?._milkyWayCubeMap &&
|
|
||||||
sky?._starCommand &&
|
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden')
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ timeout: 60000 },
|
{ timeout: 60000 },
|
||||||
);
|
);
|
||||||
|
await whenSettled(page);
|
||||||
const r = await page.evaluate(async () => {
|
const r = await page.evaluate(async () => {
|
||||||
const entry = performance
|
const entry = performance
|
||||||
.getEntriesByType('resource')
|
.getEntriesByType('resource')
|
||||||
@@ -94,22 +94,22 @@ try {
|
|||||||
C.Cartesian3.cross(look, north, new C.Cartesian3()),
|
C.Cartesian3.cross(look, north, new C.Cartesian3()),
|
||||||
new C.Cartesian3(),
|
new C.Cartesian3(),
|
||||||
);
|
);
|
||||||
s.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
s.tweens.removeAll();
|
s.tweens.removeAll();
|
||||||
s.camera.setView({
|
window.__godsEyeView.engine.camera.setView({
|
||||||
destination: C.Cartesian3.multiplyByScalar(look, 3e7, new C.Cartesian3()),
|
destination: C.Cartesian3.multiplyByScalar(look, 3e7, new C.Cartesian3()),
|
||||||
orientation: {
|
orientation: {
|
||||||
direction: look,
|
direction: look,
|
||||||
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
|
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,
|
w = gl.drawingBufferWidth,
|
||||||
h = gl.drawingBufferHeight,
|
h = gl.drawingBufferHeight,
|
||||||
px = new Uint8Array(w * h * 4);
|
px = new Uint8Array(w * h * 4);
|
||||||
const measure = async () => {
|
const measure = async () => {
|
||||||
await new Promise((res) => setTimeout(res, 2500));
|
await new Promise((res) => setTimeout(res, 2500));
|
||||||
s.requestRender();
|
window.__godsEyeView.engine.render.request();
|
||||||
s.render();
|
s.render();
|
||||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
||||||
let banded = 0;
|
let banded = 0;
|
||||||
@@ -156,5 +156,5 @@ try {
|
|||||||
console.error(`UNREACHED: ${error.message}`);
|
console.error(`UNREACHED: ${error.message}`);
|
||||||
process.exitCode = 2;
|
process.exitCode = 2;
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { whenSettled } from './qa/ready.mjs';
|
||||||
/** Prove standalone startup and terminal resource ownership in a real browser. */
|
/** 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 assert from 'node:assert/strict';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const urlIndex = args.indexOf('--url');
|
const urlIndex = args.indexOf('--url');
|
||||||
const url = urlIndex >= 0 ? args[urlIndex + 1] : 'http://localhost:4173';
|
const url = urlIndex >= 0 ? args[urlIndex + 1] : 'http://localhost:4173';
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
executablePath:
|
args: ['--no-sandbox'],
|
||||||
process.env.PUPPETEER_EXECUTABLE_PATH || (await puppeteer.executablePath()),
|
|
||||||
args: ['--no-sandbox', '--use-gl=angle', '--use-angle=swiftshader'],
|
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
@@ -28,6 +27,7 @@ try {
|
|||||||
await page.waitForFunction(() => window.__godsEyeView?.voiceCommands, {
|
await page.waitForFunction(() => window.__godsEyeView?.voiceCommands, {
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
|
await whenSettled(page);
|
||||||
const before = await page.evaluate(async () => {
|
const before = await page.evaluate(async () => {
|
||||||
const entry = [...document.scripts].find((script) =>
|
const entry = [...document.scripts].find((script) =>
|
||||||
/\/src\/main\.js(?:\?|$)/.test(script.src),
|
/\/src\/main\.js(?:\?|$)/.test(script.src),
|
||||||
@@ -52,7 +52,7 @@ try {
|
|||||||
enabled,
|
enabled,
|
||||||
layers: app.dataManager.layers.size,
|
layers: app.dataManager.layers.size,
|
||||||
annotationCount: annotation.drawn,
|
annotationCount: annotation.drawn,
|
||||||
viewerAlive: !app.viewer.isDestroyed(),
|
viewerAlive: !window.__godsEyeView.engine.isDestroyed(),
|
||||||
credits: Boolean(document.querySelector('#cesium-credits')),
|
credits: Boolean(document.querySelector('#cesium-credits')),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -83,7 +83,7 @@ try {
|
|||||||
return {
|
return {
|
||||||
status: application.getState().status,
|
status: application.getState().status,
|
||||||
samePromise,
|
samePromise,
|
||||||
viewerDestroyed: app.viewer.isDestroyed(),
|
viewerDestroyed: app.engine.isDestroyed(),
|
||||||
layers: app.dataManager.layers.size,
|
layers: app.dataManager.layers.size,
|
||||||
annotations: app.annotations.count(),
|
annotations: app.annotations.count(),
|
||||||
governor: app.getRenderGovernorDiagnostics(),
|
governor: app.getRenderGovernorDiagnostics(),
|
||||||
@@ -112,5 +112,5 @@ try {
|
|||||||
'PASS: terminal teardown releases runtime owners without late browser errors',
|
'PASS: terminal teardown releases runtime owners without late browser errors',
|
||||||
);
|
);
|
||||||
} finally {
|
} 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).
|
* 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
|
* 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 { existsSync, mkdirSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, resolve } from 'node:path';
|
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');
|
const SHOT_DIR = resolve(__dirname, '..', 'qa-shots', 'b12');
|
||||||
mkdirSync(SHOT_DIR, { recursive: true });
|
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 passed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -70,10 +60,8 @@ const REQUIRED_CREDIT_SUBSTRINGS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const chromeExecutable = findChromeExecutable();
|
const browser = await launchBrowser({
|
||||||
const browser = await puppeteer.launch({
|
headless: true,
|
||||||
headless: 'new',
|
|
||||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
@@ -122,8 +110,9 @@ async function main() {
|
|||||||
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.viewer.creditDisplay,
|
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.viewer.creditDisplay,
|
||||||
{ timeout: 60000 },
|
{ timeout: 60000 },
|
||||||
);
|
);
|
||||||
|
await whenSettled(page);
|
||||||
// Give Cesium a few frames to render the on-screen credit line (logo + link).
|
// 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 ────────────────────────────
|
// ── (i) H11: static credits registered ────────────────────────────
|
||||||
console.log('H11 — per-layer credits registered in viewer.creditDisplay');
|
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.
|
// Force a render frame so the credit display flushes, then open the lightbox.
|
||||||
const lightboxState = await page.evaluate(async () => {
|
const lightboxState = await page.evaluate(async () => {
|
||||||
const viewer = window.__godsEyeView.viewer;
|
const viewer = window.__godsEyeView.viewer;
|
||||||
viewer.scene.requestRender();
|
window.__godsEyeView.engine.render.request();
|
||||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||||
const cd = viewer.creditDisplay;
|
const cd = viewer.creditDisplay;
|
||||||
cd.showLightbox();
|
cd.showLightbox();
|
||||||
viewer.scene.requestRender();
|
window.__godsEyeView.engine.render.request();
|
||||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||||
const box = document.querySelector('.cesium-credit-lightbox');
|
const box = document.querySelector('.cesium-credit-lightbox');
|
||||||
const list = box?.querySelector(':scope > ul');
|
const list = box?.querySelector(':scope > ul');
|
||||||
@@ -263,7 +252,7 @@ async function main() {
|
|||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const viewer = window.__godsEyeView.viewer;
|
const viewer = window.__godsEyeView.viewer;
|
||||||
viewer.resize();
|
viewer.resize();
|
||||||
viewer.scene.requestRender();
|
window.__godsEyeView.engine.render.request();
|
||||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||||
});
|
});
|
||||||
const mobileLightbox = await page.evaluate(() => {
|
const mobileLightbox = await page.evaluate(() => {
|
||||||
@@ -311,7 +300,7 @@ async function main() {
|
|||||||
check('found datacenter + cable layer ids', !!dcId && !!cableId, `dc=${dcId} cable=${cableId}`);
|
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 (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);
|
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(() =>
|
const afterEnableHtml = await page.evaluate(() =>
|
||||||
(window.__godsEyeView.viewer.creditDisplay._staticCredits || []).map((c) => c.html),
|
(window.__godsEyeView.viewer.creditDisplay._staticCredits || []).map((c) => c.html),
|
||||||
);
|
);
|
||||||
@@ -348,7 +337,7 @@ async function main() {
|
|||||||
// ── (iii) clean-view: credit line STILL visible ───────────────────
|
// ── (iii) clean-view: credit line STILL visible ───────────────────
|
||||||
console.log('\nH10 — clean-view keeps the credit line visible');
|
console.log('\nH10 — clean-view keeps the credit line visible');
|
||||||
await page.evaluate(() => document.body.classList.add('ui-clean-view'));
|
await page.evaluate(() => document.body.classList.add('ui-clean-view'));
|
||||||
await new Promise((r) => setTimeout(r, 400));
|
await whenUiSettled(page);
|
||||||
const cleanVis = await creditVisibility();
|
const cleanVis = await creditVisibility();
|
||||||
await page.screenshot({ path: resolve(SHOT_DIR, 'clean-view.png') });
|
await page.screenshot({ path: resolve(SHOT_DIR, 'clean-view.png') });
|
||||||
check(
|
check(
|
||||||
@@ -366,7 +355,7 @@ async function main() {
|
|||||||
// ── (iv) recording-mode: credit line STILL visible ────────────────
|
// ── (iv) recording-mode: credit line STILL visible ────────────────
|
||||||
console.log('\nH10 — recording-mode keeps the credit line visible');
|
console.log('\nH10 — recording-mode keeps the credit line visible');
|
||||||
await page.evaluate(() => document.body.classList.add('recording-mode'));
|
await page.evaluate(() => document.body.classList.add('recording-mode'));
|
||||||
await new Promise((r) => setTimeout(r, 400));
|
await whenUiSettled(page);
|
||||||
const recVis = await creditVisibility();
|
const recVis = await creditVisibility();
|
||||||
await page.screenshot({ path: resolve(SHOT_DIR, 'recording-mode.png') });
|
await page.screenshot({ path: resolve(SHOT_DIR, 'recording-mode.png') });
|
||||||
check(
|
check(
|
||||||
@@ -377,7 +366,7 @@ async function main() {
|
|||||||
await page.evaluate(() => document.body.classList.remove('recording-mode'));
|
await page.evaluate(() => document.body.classList.remove('recording-mode'));
|
||||||
|
|
||||||
// baseline (normal) screenshot for comparison
|
// baseline (normal) screenshot for comparison
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
await whenUiSettled(page);
|
||||||
await page.screenshot({ path: resolve(SHOT_DIR, 'normal.png') });
|
await page.screenshot({ path: resolve(SHOT_DIR, 'normal.png') });
|
||||||
|
|
||||||
check(
|
check(
|
||||||
@@ -391,7 +380,7 @@ async function main() {
|
|||||||
failedResponses.slice(0, 3).join(' | ') || 'clean',
|
failedResponses.slice(0, 3).join(' | ') || 'clean',
|
||||||
);
|
);
|
||||||
|
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
|
|
||||||
console.log('\n────────────────────────────────────────────────────────────');
|
console.log('\n────────────────────────────────────────────────────────────');
|
||||||
console.log(` RESULT: ${passed} passed, ${failed} failed`);
|
console.log(` RESULT: ${passed} passed, ${failed} failed`);
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ export function hookRenderErrors(page) {
|
|||||||
return page.evaluateOnNewDocument(() => {
|
return page.evaluateOnNewDocument(() => {
|
||||||
window.__qaRenderErrors = [];
|
window.__qaRenderErrors = [];
|
||||||
const hook = () => {
|
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;
|
const scene = window.__godsEyeView?.viewer?.scene;
|
||||||
if (!scene?.renderError) return false;
|
if (!scene?.renderError) return false;
|
||||||
scene.renderError.addEventListener((_, error) =>
|
scene.renderError.addEventListener((_, error) =>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/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.
|
* 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
|
* Requires a running dev server. Headless; rAF throttling disabled so the
|
||||||
* frame clock is honest (hidden-pane gotcha).
|
* frame clock is honest (hidden-pane gotcha).
|
||||||
*/
|
*/
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
const argv = process.argv;
|
const argv = process.argv;
|
||||||
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4214';
|
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)}`);
|
console.log(` [MEAS] ${name} — ${JSON.stringify(detail)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: 'new',
|
headless: true,
|
||||||
protocolTimeout: 300_000,
|
protocolTimeout: 300_000,
|
||||||
args: [
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-timer-throttling'],
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-setuid-sandbox',
|
|
||||||
'--window-size=1440,900',
|
|
||||||
'--disable-backgrounding-occluded-windows',
|
|
||||||
'--disable-renderer-backgrounding',
|
|
||||||
'--disable-background-timer-throttling',
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setViewport({ width: 1440, height: 860 });
|
await page.setViewport({ width: 1440, height: 860 });
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
|
await page.waitForFunction(() => !!window.__godsEyeView?.engine, { timeout: 90_000 });
|
||||||
await new Promise((r) => setTimeout(r, 12_000)); // boot flyTo + deferred init
|
await whenSettled(page); // boot flyTo + deferred init
|
||||||
|
|
||||||
// Park mid-Atlantic (many cables + both coasts' landings in range) and
|
// Park mid-Atlantic (many cables + both coasts' landings in range) and
|
||||||
// disable every layer so the cables layer is measured in isolation.
|
// 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 gev = window.__godsEyeView;
|
||||||
const v = gev.viewer;
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
v.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.setView({
|
||||||
const ell = v.scene.globe.ellipsoid;
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
v.camera.setView({
|
|
||||||
destination: ell.cartographicToCartesian({
|
|
||||||
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
|
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
|
||||||
}),
|
}),
|
||||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
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 */ } }
|
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 ─────────────────────────────────────
|
// ── b. toggle-ON → labels visible ─────────────────────────────────────
|
||||||
if (!control) {
|
if (!control) {
|
||||||
@@ -96,14 +90,14 @@ try {
|
|||||||
};
|
};
|
||||||
while (performance.now() < deadline) {
|
while (performance.now() < deadline) {
|
||||||
if (labelsUp()) return { ms: Math.round(performance.now() - t0), timedOut: false };
|
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));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
return { ms: 60_000, timedOut: true };
|
return { ms: 60_000, timedOut: true };
|
||||||
}, LAYER_ID, legacy);
|
}, LAYER_ID, legacy);
|
||||||
report('toggle-ON -> labels visible (ms)', labelLatency);
|
report('toggle-ON -> labels visible (ms)', labelLatency);
|
||||||
check('labels became visible after enable', labelLatency.timedOut === false, 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 ────────────────────────────────
|
// ── c. drawn-set / label / host counts ────────────────────────────────
|
||||||
@@ -190,11 +184,10 @@ try {
|
|||||||
: 'scene.render cost, cables ON, 10s orbit', frameCost);
|
: 'scene.render cost, cables ON, 10s orbit', frameCost);
|
||||||
|
|
||||||
// ── d. parked idle honesty (labels must not force continuous render) ──
|
// ── 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 idle = await page.evaluate(() => new Promise((resolve) => {
|
||||||
const scene = window.__godsEyeView.viewer.scene;
|
|
||||||
let renders = 0;
|
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);
|
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
|
||||||
}));
|
}));
|
||||||
report(control
|
report(control
|
||||||
@@ -224,7 +217,7 @@ try {
|
|||||||
const cycle = await page.evaluate(async (layerId) => {
|
const cycle = await page.evaluate(async (layerId) => {
|
||||||
const gev = window.__godsEyeView;
|
const gev = window.__godsEyeView;
|
||||||
await gev.dataManager.setEnabled(layerId, false, { origin: 'user' });
|
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));
|
await new Promise((r) => setTimeout(r, 1_200));
|
||||||
const offDiag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
const offDiag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
||||||
const offEntries = offDiag.entriesBySource?.[layerId] || 0;
|
const offEntries = offDiag.entriesBySource?.[layerId] || 0;
|
||||||
@@ -236,7 +229,7 @@ try {
|
|||||||
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
||||||
onPainted = diag.paintedBySource?.[layerId] || 0;
|
onPainted = diag.paintedBySource?.[layerId] || 0;
|
||||||
if (onPainted > 0) break;
|
if (onPainted > 0) break;
|
||||||
gev.viewer.scene.requestRender?.();
|
window.__godsEyeView.engine.render.request?.();
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
return { offEntries, offPainted, onPainted };
|
return { offEntries, offPainted, onPainted };
|
||||||
@@ -245,7 +238,7 @@ try {
|
|||||||
check('re-enable restores painted labels', cycle.onPainted > 0, cycle);
|
check('re-enable restores painted labels', cycle.onPainted > 0, cycle);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const passed = results.filter((r) => r.pass).length;
|
const passed = results.filter((r) => r.pass).length;
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/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
|
* qa-cables-render-probe — replicates the owner-side perf probe methodology
|
||||||
* for the cables layer (relative numbers; SwiftShader when run headless):
|
* 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]
|
* Usage: node scripts/qa-cables-render-probe.mjs [--url http://localhost:4216]
|
||||||
*/
|
*/
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
const argv = process.argv;
|
const argv = process.argv;
|
||||||
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4216';
|
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4216';
|
||||||
const LAYER_ID = 'telegeography-submarine-cables';
|
const LAYER_ID = 'telegeography-submarine-cables';
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: 'new',
|
headless: true,
|
||||||
protocolTimeout: 300_000,
|
protocolTimeout: 300_000,
|
||||||
args: [
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-timer-throttling'],
|
||||||
'--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900',
|
|
||||||
'--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding',
|
|
||||||
'--disable-background-timer-throttling',
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setViewport({ width: 1440, height: 860 });
|
await page.setViewport({ width: 1440, height: 860 });
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
|
await page.waitForFunction(() => !!window.__godsEyeView?.engine, { timeout: 90_000 });
|
||||||
await new Promise((r) => setTimeout(r, 12_000));
|
await whenSettled(page);
|
||||||
|
|
||||||
await page.evaluate(async () => {
|
await evaluateWithGeo(page, async (qaGeo) => {
|
||||||
const gev = window.__godsEyeView;
|
const gev = window.__godsEyeView;
|
||||||
gev.viewer.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
const ell = gev.viewer.scene.globe.ellipsoid;
|
window.__godsEyeView.engine.camera.setView({
|
||||||
gev.viewer.camera.setView({
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
destination: ell.cartographicToCartesian({
|
|
||||||
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
|
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
|
||||||
}),
|
}),
|
||||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
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 */ } }
|
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) => ({
|
const stats = (values) => ({
|
||||||
mean: +(values.reduce((s, v) => s + v, 0) / Math.max(1, values.length)).toFixed(2),
|
mean: +(values.reduce((s, v) => s + v, 0) / Math.max(1, values.length)).toFixed(2),
|
||||||
@@ -60,7 +58,7 @@ try {
|
|||||||
const gev = window.__godsEyeView;
|
const gev = window.__godsEyeView;
|
||||||
await gev.dataManager.setEnabled(id, on, { origin: 'user' });
|
await gev.dataManager.setEnabled(id, on, { origin: 'user' });
|
||||||
}, LAYER_ID, enabled);
|
}, 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.
|
// 1. PARKED timed renders.
|
||||||
const parked = await page.evaluate(() => {
|
const parked = await page.evaluate(() => {
|
||||||
@@ -77,18 +75,17 @@ try {
|
|||||||
// 2. PAN-STOP cycles: move, raise moveEnd, wait 650 ms, 20 timed renders.
|
// 2. PAN-STOP cycles: move, raise moveEnd, wait 650 ms, 20 timed renders.
|
||||||
const cycles = [];
|
const cycles = [];
|
||||||
for (let cycle = 0; cycle < 3; cycle++) {
|
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 v = window.__godsEyeView.viewer;
|
||||||
const ell = v.scene.globe.ellipsoid;
|
window.__godsEyeView.engine.camera.setView({
|
||||||
v.camera.setView({
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
destination: ell.cartographicToCartesian({
|
|
||||||
longitude: (-40 + step * 3) * Math.PI / 180,
|
longitude: (-40 + step * 3) * Math.PI / 180,
|
||||||
latitude: 35 * Math.PI / 180,
|
latitude: 35 * Math.PI / 180,
|
||||||
height: 4_500_000,
|
height: 4_500_000,
|
||||||
}),
|
}),
|
||||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||||
});
|
});
|
||||||
v.camera.moveEnd.raiseEvent();
|
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 650));
|
await new Promise((resolve) => setTimeout(resolve, 650));
|
||||||
const durations = [];
|
const durations = [];
|
||||||
for (let i = 0; i < 20; i++) {
|
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(`\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`);
|
console.log(`pan-stop worst: ON ${on.worst}ms vs OFF ${off.worst}ms`);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
+15
-20
@@ -1,10 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/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
|
* qa-cables-shot — capture the cables layer at two fixed cameras for
|
||||||
* before/after visual-identity comparison. Writes to gitignored qa-shots/.
|
* before/after visual-identity comparison. Writes to gitignored qa-shots/.
|
||||||
* Usage: node scripts/qa-cables-shot.mjs [--url http://localhost:4214] [--tag before]
|
* 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 { mkdirSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
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';
|
const LAYER_ID = 'telegeography-submarine-cables';
|
||||||
mkdirSync(new URL('../qa-shots', import.meta.url), { recursive: true });
|
mkdirSync(new URL('../qa-shots', import.meta.url), { recursive: true });
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: 'new',
|
headless: true,
|
||||||
protocolTimeout: 300_000,
|
protocolTimeout: 300_000,
|
||||||
args: [
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-timer-throttling'],
|
||||||
'--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900',
|
|
||||||
'--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding',
|
|
||||||
'--disable-background-timer-throttling',
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setViewport({ width: 1440, height: 860 });
|
await page.setViewport({ width: 1440, height: 860 });
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
|
await page.waitForFunction(() => !!window.__godsEyeView?.engine, { timeout: 90_000 });
|
||||||
await new Promise((r) => setTimeout(r, 12_000));
|
await whenSettled(page);
|
||||||
await page.evaluate(async (layerId) => {
|
await page.evaluate(async (layerId) => {
|
||||||
const gev = window.__godsEyeView;
|
const gev = window.__godsEyeView;
|
||||||
gev.viewer.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
for (const [id, entry] of gev.dataManager.layers) {
|
for (const [id, entry] of gev.dataManager.layers) {
|
||||||
if (entry.enabled && id !== layerId) {
|
if (entry.enabled && id !== layerId) {
|
||||||
try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* shot only */ }
|
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 },
|
{ name: 'ny-coast', lon: -73.5, lat: 40.4, height: 500_000 },
|
||||||
];
|
];
|
||||||
for (const view of views) {
|
for (const view of views) {
|
||||||
await page.evaluate((v) => {
|
await evaluateWithGeo(page, async (qaGeo, v) => {
|
||||||
const viewer = window.__godsEyeView.viewer;
|
window.__godsEyeView.engine.camera.setView({
|
||||||
const ell = viewer.scene.globe.ellipsoid;
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
viewer.camera.setView({
|
|
||||||
destination: ell.cartographicToCartesian({
|
|
||||||
longitude: v.lon * Math.PI / 180, latitude: v.lat * Math.PI / 180, height: v.height,
|
longitude: v.lon * Math.PI / 180, latitude: v.lat * Math.PI / 180, height: v.height,
|
||||||
}),
|
}),
|
||||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||||
@@ -57,15 +53,14 @@ try {
|
|||||||
}, view);
|
}, view);
|
||||||
// Let the sweep + labels settle with frames flowing.
|
// Let the sweep + labels settle with frames flowing.
|
||||||
await page.evaluate(() => new Promise((resolve) => {
|
await page.evaluate(() => new Promise((resolve) => {
|
||||||
const v = window.__godsEyeView.viewer;
|
|
||||||
let ticks = 0;
|
let ticks = 0;
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
v.scene.requestRender?.();
|
window.__godsEyeView.engine.render.request?.();
|
||||||
if (++ticks < 240) requestAnimationFrame(tick); else resolve();
|
if (++ticks < 240) requestAnimationFrame(tick); else resolve();
|
||||||
};
|
};
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
}));
|
}));
|
||||||
await new Promise((r) => setTimeout(r, 1_000));
|
await whenSettled(page);
|
||||||
const path = fileURLToPath(
|
const path = fileURLToPath(
|
||||||
new URL(`../qa-shots/cables-${tag}-${view.name}.png`, import.meta.url),
|
new URL(`../qa-shots/cables-${tag}-${view.name}.png`, import.meta.url),
|
||||||
);
|
);
|
||||||
@@ -73,5 +68,5 @@ try {
|
|||||||
console.log(`saved ${path}`);
|
console.log(`saved ${path}`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/** Camera-panel browser acceptance with controlled sources, images and terrain. */
|
/** 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 fs from 'node:fs';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
const origin = process.env.QA_BASE_URL || 'http://localhost:4173';
|
const origin = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||||
const selectAllModifier = process.platform === 'darwin' ? 'Meta' : 'Control';
|
const selectAllModifier = process.platform === 'darwin' ? 'Meta' : 'Control';
|
||||||
const sources = [
|
const sources = [
|
||||||
@@ -31,13 +32,11 @@ const sources = [
|
|||||||
rangeM: 300,
|
rangeM: 300,
|
||||||
mountHeightM: 8,
|
mountHeightM: 8,
|
||||||
}));
|
}));
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
...(process.platform === 'darwin'
|
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||||
? ['--use-angle=metal', '--enable-gpu']
|
|
||||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
@@ -108,10 +107,10 @@ try {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
() =>
|
() =>
|
||||||
window.__godsEyeView?.styleManager?._cctvControls &&
|
window.__godsEyeView?.styleManager?._cctvControls &&
|
||||||
window.__godsEyeView?.dataManager?.layers?.has('cctv') &&
|
window.__godsEyeView?.dataManager?.layers?.has('cctv'),
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
||||||
{ timeout: 60000 },
|
{ timeout: 60000 },
|
||||||
);
|
);
|
||||||
|
await whenSettled(page);
|
||||||
await page.$eval('[data-collapse-target="cctv-panel"]', (button) =>
|
await page.$eval('[data-collapse-target="cctv-panel"]', (button) =>
|
||||||
button.click(),
|
button.click(),
|
||||||
);
|
);
|
||||||
@@ -357,22 +356,23 @@ try {
|
|||||||
fs.mkdirSync('qa-shots/camera-controls', { recursive: true });
|
fs.mkdirSync('qa-shots/camera-controls', { recursive: true });
|
||||||
await page.screenshot({ path: 'qa-shots/camera-controls/desktop.png' });
|
await page.screenshot({ path: 'qa-shots/camera-controls/desktop.png' });
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const camera = window.__godsEyeView.viewer.camera;
|
window.__godsEyeView.engine.camera.setView({
|
||||||
camera.setView({
|
destination: window.__godsEyeView.engine.camera.pose().position,
|
||||||
orientation: {
|
orientation: {
|
||||||
heading: camera.heading + Math.PI / 3,
|
heading:
|
||||||
|
window.__godsEyeView.engine.camera.pose().heading + Math.PI / 3,
|
||||||
pitch: -0.6,
|
pitch: -0.6,
|
||||||
roll: 0,
|
roll: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
await whenUiSettled(page);
|
||||||
await page.screenshot({ path: 'qa-shots/camera-controls/angle.png' });
|
await page.screenshot({ path: 'qa-shots/camera-controls/angle.png' });
|
||||||
await page.setViewport({ width: 390, height: 844 });
|
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.
|
// Phones present CCTV in the Cameras tab's sheet.
|
||||||
await page.click('#mobile-tab-bar [data-mobile-sheet="cameras"]');
|
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' });
|
await page.screenshot({ path: 'qa-shots/camera-controls/narrow.png' });
|
||||||
check(
|
check(
|
||||||
'narrow camera panel remains within the viewport',
|
'narrow camera panel remains within the viewport',
|
||||||
@@ -411,6 +411,6 @@ try {
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await Promise.all([...delayedFrames].map((respond) => respond()));
|
await Promise.all([...delayedFrames].map((respond) => respond()));
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
if (failures) process.exitCode = 1;
|
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
|
* qa-cctv-v2.mjs — CCTV v2 subsystem proof harness
|
||||||
*
|
*
|
||||||
@@ -76,7 +79,7 @@
|
|||||||
* Exit 0 = all asserts passed. Non-zero = a hard failure.
|
* 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 { watchBrowserPage, redact } from './qa-browserEvidence.mjs';
|
||||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
import { installCctvFixture } from './fixtures/layers/cctv.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;
|
const results = evidence.checks;
|
||||||
function record(name, ok, detail) {
|
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.
|
* The predicate must return a boolean: its result crosses CDP by value.
|
||||||
* @returns {Promise<boolean>} True if the predicate held within the budget.
|
* @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
|
// 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
|
// by page.mouse.move(), before the LEFT_DOWN under test even runs. Position the
|
||||||
@@ -266,22 +220,21 @@ function installRenderFramesHelper(page) {
|
|||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
window.__qaRenderFrames = (count, capMs) =>
|
window.__qaRenderFrames = (count, capMs) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
const scene = window.__godsEyeView.viewer.scene;
|
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
const finish = () => {
|
const finish = () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
remove();
|
remove();
|
||||||
resolve(frames);
|
resolve(frames);
|
||||||
};
|
};
|
||||||
const remove = scene.postRender.addEventListener(() => {
|
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||||
if (++frames >= count) {
|
if (++frames >= count) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scene.requestRender();
|
window.__godsEyeView.engine.render.request();
|
||||||
});
|
});
|
||||||
const timer = setTimeout(finish, capMs);
|
const timer = setTimeout(finish, capMs);
|
||||||
scene.requestRender();
|
window.__godsEyeView.engine.render.request();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -462,23 +415,6 @@ const BEARING_SHIFT_SRC = `
|
|||||||
* @param {number} [timeoutMs=15000]
|
* @param {number} [timeoutMs=15000]
|
||||||
* @returns {Promise<boolean>} True if tilesLoaded was observed within the timeout.
|
* @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
|
* Searches for a calibration patch (pitchDeg/fovDeg/rangeScale offsets) that
|
||||||
@@ -542,24 +478,13 @@ async function main() {
|
|||||||
|
|
||||||
fs.mkdirSync(SHOTS_DIR, { recursive: true });
|
fs.mkdirSync(SHOTS_DIR, { recursive: true });
|
||||||
|
|
||||||
const chromeExecutable = findChromeExecutable();
|
const browser = await launchBrowser({
|
||||||
const browser = await puppeteer.launch({
|
|
||||||
headless: true,
|
headless: true,
|
||||||
protocolTimeout: PROTOCOL_TIMEOUT_MS,
|
protocolTimeout: PROTOCOL_TIMEOUT_MS,
|
||||||
// Talk CDP over a pipe, so no debugging port stays open on localhost for
|
// Talk CDP over a pipe, so no debugging port stays open on localhost for
|
||||||
// the length of a headful run.
|
// the length of a headful run.
|
||||||
pipe: true,
|
pipe: true,
|
||||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-gpu', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
|
||||||
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',
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const consoleErrors = evidence.browserErrors;
|
const consoleErrors = evidence.browserErrors;
|
||||||
@@ -596,7 +521,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
console.log('Loading app...');
|
console.log('Loading app...');
|
||||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||||
const appReady = await waitForPredicate(
|
const appReady = await pollPage(
|
||||||
page,
|
page,
|
||||||
() =>
|
() =>
|
||||||
Boolean(
|
Boolean(
|
||||||
@@ -638,7 +563,7 @@ async function main() {
|
|||||||
if (!keyless) throw new Error('Expected a keyless globe before CCTV QA');
|
if (!keyless) throw new Error('Expected a keyless globe before CCTV QA');
|
||||||
}
|
}
|
||||||
// Let the initial fly-to Austin and first tiles settle.
|
// 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
|
// Install raycast counters BEFORE the layer is enabled (§9.1-amended: the
|
||||||
@@ -653,7 +578,7 @@ async function main() {
|
|||||||
if (
|
if (
|
||||||
!scene ||
|
!scene ||
|
||||||
typeof scene.pickFromRay !== 'function' ||
|
typeof scene.pickFromRay !== 'function' ||
|
||||||
typeof scene.sampleHeight !== 'function'
|
typeof window.__godsEyeView.engine.surface.sampleHeight !== 'function'
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -663,7 +588,7 @@ async function main() {
|
|||||||
window.__qaCounters.pickFromRay += 1;
|
window.__qaCounters.pickFromRay += 1;
|
||||||
return origPick(...args);
|
return origPick(...args);
|
||||||
};
|
};
|
||||||
const origSample = scene.sampleHeight.bind(scene);
|
const origSample = window.__godsEyeView.engine.surface.sampleHeight.bind(scene);
|
||||||
scene.sampleHeight = (...args) => {
|
scene.sampleHeight = (...args) => {
|
||||||
window.__qaCounters.sampleHeight += 1;
|
window.__qaCounters.sampleHeight += 1;
|
||||||
return origSample(...args);
|
return origSample(...args);
|
||||||
@@ -719,7 +644,7 @@ async function main() {
|
|||||||
console.log(
|
console.log(
|
||||||
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
|
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
|
||||||
);
|
);
|
||||||
const drained = await waitForPredicate(
|
const drained = await pollPage(
|
||||||
page,
|
page,
|
||||||
() => {
|
() => {
|
||||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
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;
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
mod.focusNearest({ durationSec: 0.1 });
|
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 cAfterActivate = await readCounters();
|
||||||
const activeId = await page.evaluate(
|
const activeId = await page.evaluate(
|
||||||
() =>
|
() =>
|
||||||
@@ -793,7 +718,7 @@ async function main() {
|
|||||||
.get('cctv')
|
.get('cctv')
|
||||||
.module.setParams({ selectedCameraId: id });
|
.module.setParams({ selectedCameraId: id });
|
||||||
}, activeId);
|
}, activeId);
|
||||||
await sleep(400);
|
await whenFramesDrawn(page);
|
||||||
const cAfterReselect = await readCounters();
|
const cAfterReselect = await readCounters();
|
||||||
const geomAfterReselect = await page.evaluate(`(() => {
|
const geomAfterReselect = await page.evaluate(`(() => {
|
||||||
${SERIALIZE_GEOM_SRC}
|
${SERIALIZE_GEOM_SRC}
|
||||||
@@ -816,7 +741,7 @@ async function main() {
|
|||||||
// latency of the completion pass (update interval is 10s).
|
// latency of the completion pass (update interval is 10s).
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
console.log('Waiting for tiles + the one-shot completion pass...');
|
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;
|
let sampleFloorOk = false;
|
||||||
if (tilesSeenLoaded && !terrainGlobe) {
|
if (tilesSeenLoaded && !terrainGlobe) {
|
||||||
sampleFloorOk = await page
|
sampleFloorOk = await page
|
||||||
@@ -866,10 +791,10 @@ async function main() {
|
|||||||
terrain.ok && heights?.length === 2 && heights.every(Number.isFinite);
|
terrain.ok && heights?.length === 2 && heights.every(Number.isFinite);
|
||||||
const mountResolved =
|
const mountResolved =
|
||||||
terrainReady &&
|
terrainReady &&
|
||||||
(await waitForPredicate(
|
(await pollPage(
|
||||||
page,
|
page,
|
||||||
(expected, base) => {
|
await withGeo((qaGeo, expected, base) => {
|
||||||
const { viewer, dataManager, engine } = window.__godsEyeView;
|
const { dataManager, engine } = window.__godsEyeView;
|
||||||
const module = dataManager.layers.get('cctv').module;
|
const module = dataManager.layers.get('cctv').module;
|
||||||
const camera = module.getUIState().activeCamera;
|
const camera = module.getUIState().activeCamera;
|
||||||
// The corner ray is an engine line; its first point (the mount)
|
// 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 mount = drawn ? ray?.polyline?.positions?.[0] : undefined;
|
||||||
const height =
|
const height =
|
||||||
mount &&
|
mount &&
|
||||||
viewer.scene.globe.ellipsoid.cartesianToCartographic(mount)
|
qaGeo.cartographic.fromCartesian(mount)
|
||||||
.height;
|
.height;
|
||||||
return (
|
return (
|
||||||
Number.isFinite(camera.groundPriorM) &&
|
Number.isFinite(camera.groundPriorM) &&
|
||||||
@@ -895,8 +820,8 @@ async function main() {
|
|||||||
) &&
|
) &&
|
||||||
window.__qaCounters.sampleHeight === base
|
window.__qaCounters.sampleHeight === base
|
||||||
);
|
);
|
||||||
},
|
}, heights, c0.sampleHeight),
|
||||||
{ timeoutMs: 30000, args: [heights, c0.sampleHeight] },
|
{ timeoutMs: 30000 },
|
||||||
));
|
));
|
||||||
record(
|
record(
|
||||||
'terrain-globe completion uses a real DEM height at the rendered mount without mesh samples',
|
'terrain-globe completion uses a real DEM height at the rendered mount without mesh samples',
|
||||||
@@ -905,10 +830,10 @@ async function main() {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('Waiting for tiles + the one-shot completion pass...');
|
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;
|
let sampleFloorOk = false;
|
||||||
if (tilesSeenLoaded) {
|
if (tilesSeenLoaded) {
|
||||||
sampleFloorOk = await waitForPredicate(
|
sampleFloorOk = await pollPage(
|
||||||
page,
|
page,
|
||||||
(base) => window.__qaCounters.sampleHeight - base >= 1,
|
(base) => window.__qaCounters.sampleHeight - base >= 1,
|
||||||
{ timeoutMs: 30000, args: [c0.sampleHeight] },
|
{ timeoutMs: 30000, args: [c0.sampleHeight] },
|
||||||
@@ -993,7 +918,7 @@ async function main() {
|
|||||||
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
|
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
|
||||||
});
|
});
|
||||||
}, activeId);
|
}, activeId);
|
||||||
await sleep(500);
|
await whenFramesDrawn(page);
|
||||||
const cAfterCal = await readCounters();
|
const cAfterCal = await readCounters();
|
||||||
const sampleDeltaCal = cAfterCal.sampleHeight - cBeforeCal.sampleHeight;
|
const sampleDeltaCal = cAfterCal.sampleHeight - cBeforeCal.sampleHeight;
|
||||||
const pickDeltaCal = cAfterCal.pickFromRay - cBeforeCal.pickFromRay;
|
const pickDeltaCal = cAfterCal.pickFromRay - cBeforeCal.pickFromRay;
|
||||||
@@ -1016,7 +941,7 @@ async function main() {
|
|||||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
||||||
}, activeId);
|
}, activeId);
|
||||||
await sleep(300);
|
await whenFramesDrawn(page);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Group 2: geometry contract
|
// Group 2: geometry contract
|
||||||
@@ -1098,7 +1023,7 @@ async function main() {
|
|||||||
? JSON.stringify(geomInfo.plane.dimensions)
|
? JSON.stringify(geomInfo.plane.dimensions)
|
||||||
: null;
|
: null;
|
||||||
for (let attempt = 0; attempt < 3 && !patchGeometryApplied; attempt++) {
|
for (let attempt = 0; attempt < 3 && !patchGeometryApplied; attempt++) {
|
||||||
await waitForTilesLoaded(page, 15000);
|
await whenTilesLoaded(page, { timeoutMs: 15000 }).then(() => true).catch(() => false);
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
({ camId, patch }) => {
|
({ camId, patch }) => {
|
||||||
const mod =
|
const mod =
|
||||||
@@ -1107,7 +1032,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
{ camId: activeId, patch: safePatch },
|
{ camId: activeId, patch: safePatch },
|
||||||
);
|
);
|
||||||
await sleep(400);
|
await whenFramesDrawn(page);
|
||||||
geomForCorners = await page.evaluate(
|
geomForCorners = await page.evaluate(
|
||||||
`(function(camId){ ${SERIALIZE_GEOM_SRC} return serializeGeom(camId); })(${JSON.stringify(activeId)})`,
|
`(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;
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
||||||
}, activeId);
|
}, activeId);
|
||||||
await sleep(300);
|
await whenFramesDrawn(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Byte-stable over 30s idle: serialize + compare after 30s. Nothing
|
// Byte-stable over 30s idle: serialize + compare after 30s. Nothing
|
||||||
@@ -1283,7 +1208,7 @@ async function main() {
|
|||||||
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
|
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
|
||||||
});
|
});
|
||||||
}, activeId);
|
}, activeId);
|
||||||
await sleep(300);
|
await whenFramesDrawn(page);
|
||||||
|
|
||||||
// Save-gated persistence (v3 design §3e): a PATCH edits the live pose
|
// Save-gated persistence (v3 design §3e): a PATCH edits the live pose
|
||||||
// only — the store must stay untouched and the camera must read as
|
// 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;
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
mod.setParams({ calibration: { cameraId: camId, save: true } });
|
mod.setParams({ calibration: { cameraId: camId, save: true } });
|
||||||
}, activeId);
|
}, activeId);
|
||||||
await sleep(200);
|
await whenFramesDrawn(page);
|
||||||
|
|
||||||
const storeEntry = await page.evaluate((camId) => {
|
const storeEntry = await page.evaluate((camId) => {
|
||||||
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
|
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
|
||||||
@@ -1371,7 +1296,7 @@ async function main() {
|
|||||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
mod.setParams({ calibration: { cameraId: camId, reset: true } });
|
||||||
}, activeId);
|
}, activeId);
|
||||||
await sleep(300);
|
await whenFramesDrawn(page);
|
||||||
|
|
||||||
const storeAfterReset = await page.evaluate((camId) => {
|
const storeAfterReset = await page.evaluate((camId) => {
|
||||||
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
|
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
|
||||||
@@ -1483,7 +1408,7 @@ async function main() {
|
|||||||
const gev = window.__godsEyeView;
|
const gev = window.__godsEyeView;
|
||||||
const viewer = gev.viewer;
|
const viewer = gev.viewer;
|
||||||
const scene = viewer.scene;
|
const scene = viewer.scene;
|
||||||
const canvas = scene.canvas;
|
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const { hitTest } = (
|
const { hitTest } = (
|
||||||
await import('/src/overlays/worldOverlay.js')
|
await import('/src/overlays/worldOverlay.js')
|
||||||
@@ -1535,28 +1460,28 @@ async function main() {
|
|||||||
const mod = gev.dataManager.layers.get('cctv').module;
|
const mod = gev.dataManager.layers.get('cctv').module;
|
||||||
const snapshotPose = () => ({
|
const snapshotPose = () => ({
|
||||||
position: [
|
position: [
|
||||||
viewer.camera.positionWC.x,
|
window.__godsEyeView.engine.camera.pose().position.x,
|
||||||
viewer.camera.positionWC.y,
|
window.__godsEyeView.engine.camera.pose().position.y,
|
||||||
viewer.camera.positionWC.z,
|
window.__godsEyeView.engine.camera.pose().position.z,
|
||||||
],
|
],
|
||||||
direction: [
|
direction: [
|
||||||
viewer.camera.directionWC.x,
|
window.__godsEyeView.engine.camera.pose().direction.x,
|
||||||
viewer.camera.directionWC.y,
|
window.__godsEyeView.engine.camera.pose().direction.y,
|
||||||
viewer.camera.directionWC.z,
|
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: [
|
right: [
|
||||||
viewer.camera.rightWC.x,
|
window.__godsEyeView.engine.camera.pose().right.x,
|
||||||
viewer.camera.rightWC.y,
|
window.__godsEyeView.engine.camera.pose().right.y,
|
||||||
viewer.camera.rightWC.z,
|
window.__godsEyeView.engine.camera.pose().right.z,
|
||||||
],
|
],
|
||||||
transform: Array.from(
|
transform: Array.from(
|
||||||
{ length: 16 },
|
{ length: 16 },
|
||||||
(_, i) => viewer.camera.transform[i],
|
(_, i) => viewer.camera.transform[i],
|
||||||
),
|
),
|
||||||
heading: viewer.camera.heading,
|
heading: window.__godsEyeView.engine.camera.pose().heading,
|
||||||
pitch: viewer.camera.pitch,
|
pitch: window.__godsEyeView.engine.camera.pose().pitch,
|
||||||
roll: viewer.camera.roll,
|
roll: window.__godsEyeView.engine.camera.pose().roll,
|
||||||
trackedId: viewer.trackedEntity?.id ?? null,
|
trackedId: viewer.trackedEntity?.id ?? null,
|
||||||
});
|
});
|
||||||
const cameraPoseMatches = (a, b, epsilon = 1e-5) => {
|
const cameraPoseMatches = (a, b, epsilon = 1e-5) => {
|
||||||
@@ -1626,7 +1551,7 @@ async function main() {
|
|||||||
.module.setParams({ calibrationMode: true });
|
.module.setParams({ calibrationMode: true });
|
||||||
});
|
});
|
||||||
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
||||||
await sleep(250);
|
await whenFramesDrawn(page);
|
||||||
const adjustClick = await page.evaluate(() => {
|
const adjustClick = await page.evaluate(() => {
|
||||||
const evidence = window.__qaCctvClickEvidence;
|
const evidence = window.__qaCctvClickEvidence;
|
||||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
@@ -1673,7 +1598,7 @@ async function main() {
|
|||||||
distance,
|
distance,
|
||||||
new ray.origin.constructor(),
|
new ray.origin.constructor(),
|
||||||
);
|
);
|
||||||
const screen = scene.cartesianToCanvasCoordinates(candidate);
|
const screen = window.__godsEyeView.engine.projector.worldToWindow(candidate);
|
||||||
if (
|
if (
|
||||||
screen &&
|
screen &&
|
||||||
Math.hypot(screen.x - canvasPoint.x, screen.y - canvasPoint.y) < 2
|
Math.hypot(screen.x - canvasPoint.x, screen.y - canvasPoint.y) < 2
|
||||||
@@ -1695,7 +1620,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
window.__qaCctvSiblingOwner = owner;
|
window.__qaCctvSiblingOwner = owner;
|
||||||
const framesRendered = await window.__qaRenderFrames(3, 10000);
|
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 picked = scene.pick(canvasPoint);
|
||||||
const pickedId = picked?.id?.id ?? picked?.id ?? null;
|
const pickedId = picked?.id?.id ?? picked?.id ?? null;
|
||||||
window.__qaCctvClickEvidence.siblingBaselineTransitions =
|
window.__qaCctvClickEvidence.siblingBaselineTransitions =
|
||||||
@@ -1735,7 +1660,7 @@ async function main() {
|
|||||||
);
|
);
|
||||||
if (siblingOwnsPick) {
|
if (siblingOwnsPick) {
|
||||||
await page.mouse.click(siblingTarget.x, siblingTarget.y);
|
await page.mouse.click(siblingTarget.x, siblingTarget.y);
|
||||||
await sleep(250);
|
await whenFramesDrawn(page);
|
||||||
}
|
}
|
||||||
const siblingClick = await page.evaluate(() => {
|
const siblingClick = await page.evaluate(() => {
|
||||||
const evidence = window.__qaCctvClickEvidence;
|
const evidence = window.__qaCctvClickEvidence;
|
||||||
@@ -1769,7 +1694,7 @@ async function main() {
|
|||||||
|
|
||||||
if (emptyClickPoint) {
|
if (emptyClickPoint) {
|
||||||
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
||||||
await sleep(250);
|
await whenFramesDrawn(page);
|
||||||
}
|
}
|
||||||
const firstEmptyClick = await page.evaluate(() => {
|
const firstEmptyClick = await page.evaluate(() => {
|
||||||
const evidence = window.__qaCctvClickEvidence;
|
const evidence = window.__qaCctvClickEvidence;
|
||||||
@@ -1810,7 +1735,7 @@ async function main() {
|
|||||||
|
|
||||||
if (emptyClickPoint) {
|
if (emptyClickPoint) {
|
||||||
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
|
||||||
await sleep(250);
|
await whenFramesDrawn(page);
|
||||||
}
|
}
|
||||||
const repeatEmptyClick = await page.evaluate(() => {
|
const repeatEmptyClick = await page.evaluate(() => {
|
||||||
const evidence = window.__qaCctvClickEvidence;
|
const evidence = window.__qaCctvClickEvidence;
|
||||||
@@ -1894,7 +1819,7 @@ async function main() {
|
|||||||
mod.setParams({ coverageMode: 'viewshed' });
|
mod.setParams({ coverageMode: 'viewshed' });
|
||||||
return mod.getUIState().coverageMode;
|
return mod.getUIState().coverageMode;
|
||||||
});
|
});
|
||||||
await sleep(400);
|
await whenFramesDrawn(page);
|
||||||
record(
|
record(
|
||||||
'coverageMode setParams round-trip (viewshed)',
|
'coverageMode setParams round-trip (viewshed)',
|
||||||
modeAfterSet === 'viewshed',
|
modeAfterSet === 'viewshed',
|
||||||
@@ -1918,7 +1843,7 @@ async function main() {
|
|||||||
const on = mod.getUIState().coverageMode;
|
const on = mod.getUIState().coverageMode;
|
||||||
return { off, on };
|
return { off, on };
|
||||||
});
|
});
|
||||||
await sleep(200);
|
await whenFramesDrawn(page);
|
||||||
const volumesInOnMode = await countVolumes();
|
const volumesInOnMode = await countVolumes();
|
||||||
record(
|
record(
|
||||||
'boolean showCoverage compat (false→off, true→on)',
|
'boolean showCoverage compat (false→off, true→on)',
|
||||||
@@ -1938,7 +1863,7 @@ async function main() {
|
|||||||
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
mod.setParams({ coverageMode: 'viewshed' });
|
mod.setParams({ coverageMode: 'viewshed' });
|
||||||
});
|
});
|
||||||
await sleep(400);
|
await whenFramesDrawn(page);
|
||||||
// NEXT starts an asynchronous footprint-height lookup. Its completion is
|
// NEXT starts an asynchronous footprint-height lookup. Its completion is
|
||||||
// allowed to update geometry; the unchanged 8s idle window starts after it.
|
// allowed to update geometry; the unchanged 8s idle window starts after it.
|
||||||
const terrainDeadline = Date.now() + 35000;
|
const terrainDeadline = Date.now() + 35000;
|
||||||
@@ -1998,7 +1923,7 @@ async function main() {
|
|||||||
// Frame that actual camera before requiring an on-screen physical drag.
|
// Frame that actual camera before requiring an on-screen physical drag.
|
||||||
await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('cctv')
|
await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('cctv')
|
||||||
.module.setParams({ focusSelected: true, focusDurationSec: 0.2 }));
|
.module.setParams({ focusSelected: true, focusDurationSec: 0.2 }));
|
||||||
await sleep(800);
|
await whenFramesDrawn(page);
|
||||||
const GIZMO_PARTS = [
|
const GIZMO_PARTS = [
|
||||||
'ring-heading',
|
'ring-heading',
|
||||||
'ring-pitch',
|
'ring-pitch',
|
||||||
@@ -2113,7 +2038,7 @@ async function main() {
|
|||||||
grab.x + eastDrag.ux * i * 12,
|
grab.x + eastDrag.ux * i * 12,
|
||||||
grab.y + eastDrag.uy * i * 12,
|
grab.y + eastDrag.uy * i * 12,
|
||||||
);
|
);
|
||||||
await sleep(40);
|
await whenFramesDrawn(page);
|
||||||
transientDragStates.push(
|
transientDragStates.push(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const mod =
|
const mod =
|
||||||
@@ -2132,7 +2057,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
const candidateCountersBeforeRelease = await readCounters();
|
const candidateCountersBeforeRelease = await readCounters();
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
await sleep(400);
|
await whenFramesDrawn(page);
|
||||||
const candidateEast = await page.evaluate(() => {
|
const candidateEast = await page.evaluate(() => {
|
||||||
const mod =
|
const mod =
|
||||||
window.__godsEyeView.dataManager.layers.get('cctv').module;
|
window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
@@ -2328,7 +2253,7 @@ async function main() {
|
|||||||
// Screenshots supplement this state/geometry oracle. Positive rendered
|
// Screenshots supplement this state/geometry oracle. Positive rendered
|
||||||
// media pixels are a separate mandatory pixel gate.
|
// media pixels are a separate mandatory pixel gate.
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
await sleep(1000);
|
await whenFramesDrawn(page);
|
||||||
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
|
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
|
||||||
await saveReviewShot(page, shot, 'visual review only, not a gate');
|
await saveReviewShot(page, shot, 'visual review only, not a gate');
|
||||||
|
|
||||||
@@ -2344,7 +2269,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await fixture?.dispose();
|
await fixture?.dispose();
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
evidence.pass =
|
evidence.pass =
|
||||||
results.length > 0 &&
|
results.length > 0 &&
|
||||||
results.every((r) => r.ok === true) &&
|
results.every((r) => r.ok === true) &&
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||||
/**
|
/**
|
||||||
* Rendered proof for backdrop-selective callout plates at GROUND LEVEL.
|
* 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 --tag before
|
||||||
* node scripts/qa-cockpit-plates.mjs --url http://localhost:4268 --teeth
|
* 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
|
* which is deterministic but is NOT real-GPU evidence; the banner says which
|
||||||
* backend actually ran and the run records it alongside the results.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
|
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
const getOpt = (name, fallback) => {
|
const getOpt = (name, fallback) => {
|
||||||
@@ -64,7 +66,7 @@ const APP_ORIGIN = new URL(APP_URL).origin;
|
|||||||
const TAG = getOpt('--tag', 'after');
|
const TAG = getOpt('--tag', 'after');
|
||||||
const HEADFUL = argv.includes('--headful');
|
const HEADFUL = argv.includes('--headful');
|
||||||
const TEETH = argv.includes('--teeth');
|
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');
|
const SHOT_DIR = path.resolve('qa-shots/cockpitplates');
|
||||||
|
|
||||||
/** Mirrors `SKY_PLATE_SCALE` in src/overlays/worldOverlayTokens.js. */
|
/** 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 = [];
|
const results = [];
|
||||||
function record(name, ok, detail) {
|
function record(name, ok, detail) {
|
||||||
@@ -213,25 +207,9 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(SHOT_DIR, { recursive: true });
|
fs.mkdirSync(SHOT_DIR, { recursive: true });
|
||||||
const executablePath = CHROME_CANDIDATES.find((candidate) => {
|
const browser = await launchBrowser({
|
||||||
try { return fs.existsSync(candidate); } catch { return false; }
|
headless: true,
|
||||||
});
|
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(SWIFTSHADER ? [] : ['--enable-gpu', '--ignore-gpu-blocklist']), '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800'],
|
||||||
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 consoleErrors = [];
|
const consoleErrors = [];
|
||||||
@@ -276,10 +254,7 @@ async function main() {
|
|||||||
|
|
||||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 90_000 });
|
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 90_000 });
|
||||||
await page.waitForFunction(
|
await whenSettled(page);
|
||||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
||||||
{ timeout: 90_000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
renderer = await page.evaluate(() => {
|
renderer = await page.evaluate(() => {
|
||||||
const gl = document.createElement('canvas').getContext('webgl2')
|
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
|
// Inject the field. The layers keep their real pipelines; only the source
|
||||||
// of observations is replaced, exactly as scripts/qa-labels.mjs does.
|
// of observations is replaced, exactly as scripts/qa-labels.mjs does.
|
||||||
await page.evaluate((payload) => {
|
await evaluateWithGeo(page, async (qaGeo, payload) => {
|
||||||
const { viewer, dataManager, styleManager } = window.__godsEyeView;
|
const { dataManager, styleManager } = window.__godsEyeView;
|
||||||
viewer.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
const Cartesian3 = viewer.camera.position.constructor;
|
const Cartesian3 = qaGeo.cartesian3;
|
||||||
window.__PLATE_QA_FIELD = payload.contacts.map((contact) => ({
|
window.__PLATE_QA_FIELD = payload.contacts.map((contact) => ({
|
||||||
sourceId: contact.id.toLowerCase(),
|
sourceId: contact.id.toLowerCase(),
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
@@ -330,17 +305,16 @@ async function main() {
|
|||||||
|
|
||||||
for (const pose of POSES) {
|
for (const pose of POSES) {
|
||||||
console.log(`\n — pose "${pose.name}" (${pose.note})`);
|
console.log(`\n — pose "${pose.name}" (${pose.note})`);
|
||||||
await page.evaluate((payload) => {
|
await evaluateWithGeo(page, async (qaGeo, payload) => {
|
||||||
const { viewer } = window.__godsEyeView;
|
const Cartesian3 = qaGeo.cartesian3;
|
||||||
const Cartesian3 = viewer.camera.position.constructor;
|
window.__godsEyeView.engine.camera.setView({
|
||||||
viewer.camera.setView({
|
|
||||||
destination: Cartesian3.fromDegrees(payload.lon, payload.lat, payload.height),
|
destination: Cartesian3.fromDegrees(payload.lon, payload.lat, payload.height),
|
||||||
orientation: { heading: 0, pitch: payload.pitchDeg * Math.PI / 180, roll: 0 },
|
orientation: { heading: 0, pitch: payload.pitchDeg * Math.PI / 180, roll: 0 },
|
||||||
});
|
});
|
||||||
}, { lon: JFK_LON, lat: JFK_LAT, height: pose.height, pitchDeg: pose.pitchDeg });
|
}, { 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.
|
// 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(() => {
|
await page.evaluate(() => {
|
||||||
window.__PLATE_TAP.events = [];
|
window.__PLATE_TAP.events = [];
|
||||||
window.__PLATE_TAP.on = true;
|
window.__PLATE_TAP.on = true;
|
||||||
@@ -356,10 +330,9 @@ async function main() {
|
|||||||
// OUTPUT has to ask for its own renders. Production is unaffected: real
|
// OUTPUT has to ask for its own renders. Production is unaffected: real
|
||||||
// Cockpit holds, and a parked map keeps showing the last correct frame
|
// Cockpit holds, and a parked map keeps showing the last correct frame
|
||||||
// precisely because nothing changed.
|
// precisely because nothing changed.
|
||||||
const scene = window.__godsEyeView.viewer.scene;
|
|
||||||
const pump = () => {
|
const pump = () => {
|
||||||
if (!window.__PLATE_TAP.on) return;
|
if (!window.__PLATE_TAP.on) return;
|
||||||
scene.requestRender();
|
window.__godsEyeView.engine.render.request();
|
||||||
requestAnimationFrame(pump);
|
requestAnimationFrame(pump);
|
||||||
};
|
};
|
||||||
pump();
|
pump();
|
||||||
@@ -522,7 +495,7 @@ async function main() {
|
|||||||
consoleErrors.slice(0, 3).join(' | ') || 'clean',
|
consoleErrors.slice(0, 3).join(' | ') || 'clean',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env node
|
#!/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. */
|
/** 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
import { stubPlanespotters } from './qa-photo-stub.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');
|
const headful = process.argv.includes('--headful');
|
||||||
fs.mkdirSync(shotsDir, { recursive: true });
|
fs.mkdirSync(shotsDir, { recursive: true });
|
||||||
|
|
||||||
const chromeCandidates = [
|
const browser = await launchBrowser({
|
||||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
headless: true,
|
||||||
await puppeteer.executablePath().catch(() => null),
|
args: [...(process.platform === 'darwin' ? ['--enable-gpu'] : []), '--no-sandbox'],
|
||||||
'/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 page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await blockPaidQaMaps(page);
|
await blockPaidQaMaps(page);
|
||||||
@@ -171,10 +160,7 @@ try {
|
|||||||
page.on('request', routeQaRequest);
|
page.on('request', routeQaRequest);
|
||||||
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
||||||
await page.waitForFunction(
|
await whenSettled(page);
|
||||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
||||||
{ timeout: 60_000 },
|
|
||||||
);
|
|
||||||
await page.waitForFunction(() => (
|
await page.waitForFunction(() => (
|
||||||
typeof window.__gevQaRegisterLayer === 'function'
|
typeof window.__gevQaRegisterLayer === 'function'
|
||||||
&& typeof window.__gevQaUnregisterLayer === 'function'
|
&& typeof window.__gevQaUnregisterLayer === 'function'
|
||||||
@@ -675,6 +661,7 @@ try {
|
|||||||
() => Boolean(window.__godsEyeView.viewer.trackedEntity?.position),
|
() => Boolean(window.__godsEyeView.viewer.trackedEntity?.position),
|
||||||
{ timeout: 10_000 },
|
{ timeout: 10_000 },
|
||||||
);
|
);
|
||||||
|
await whenSettled(page);
|
||||||
const preselectedContactAdoption = await page.evaluate(async () => {
|
const preselectedContactAdoption = await page.evaluate(async () => {
|
||||||
const { styleManager, dataManager, viewer } = window.__godsEyeView;
|
const { styleManager, dataManager, viewer } = window.__godsEyeView;
|
||||||
const flights = dataManager.layers.get('flights')?.module;
|
const flights = dataManager.layers.get('flights')?.module;
|
||||||
@@ -767,7 +754,7 @@ try {
|
|||||||
} : null,
|
} : null,
|
||||||
navigation: snapshot?.navigation || null,
|
navigation: snapshot?.navigation || null,
|
||||||
afterId: trackedInfo?.icao24 || 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,
|
contextVisible: !document.getElementById('military-awareness-panel')?.hidden,
|
||||||
entryAvailableAfterSettlement: Boolean(entry && !entry.hidden),
|
entryAvailableAfterSettlement: Boolean(entry && !entry.hidden),
|
||||||
blockerStillRegistered: dataManager.layers.has(blockerId),
|
blockerStillRegistered: dataManager.layers.has(blockerId),
|
||||||
@@ -854,8 +841,8 @@ try {
|
|||||||
const readinessUrl = new URL(appUrl);
|
const readinessUrl = new URL(appUrl);
|
||||||
readinessUrl.searchParams.set('welcome', '0');
|
readinessUrl.searchParams.set('welcome', '0');
|
||||||
await readinessPage.goto(readinessUrl.href, { waitUntil: 'domcontentloaded' });
|
await readinessPage.goto(readinessUrl.href, { waitUntil: 'domcontentloaded' });
|
||||||
await readinessPage.waitForFunction(() => window.__godsEyeView?.dataManager
|
await readinessPage.waitForFunction(() => window.__godsEyeView?.dataManager, { timeout: 60000 });
|
||||||
&& document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60000 });
|
await whenSettled(readinessPage);
|
||||||
await readinessPage.evaluate(async () => {
|
await readinessPage.evaluate(async () => {
|
||||||
const manager = window.__godsEyeView.dataManager;
|
const manager = window.__godsEyeView.dataManager;
|
||||||
await manager.setEnabled('flights', true, { origin: 'user' });
|
await manager.setEnabled('flights', true, { origin: 'user' });
|
||||||
@@ -1100,7 +1087,7 @@ try {
|
|||||||
const awareness = dataManager.layers.get('military-awareness')?.module;
|
const awareness = dataManager.layers.get('military-awareness')?.module;
|
||||||
const before = awareness?.getContextSnapshot?.()?.subject || null;
|
const before = awareness?.getContextSnapshot?.()?.subject || null;
|
||||||
const entityBefore = viewer.trackedEntity;
|
const entityBefore = viewer.trackedEntity;
|
||||||
viewer.camera.zoomOut(1_500_000);
|
window.__godsEyeView.engine.camera.zoomOut(1_500_000);
|
||||||
const cameraRange = () => Math.hypot(
|
const cameraRange = () => Math.hypot(
|
||||||
viewer.camera.position.x,
|
viewer.camera.position.x,
|
||||||
viewer.camera.position.y,
|
viewer.camera.position.y,
|
||||||
@@ -1465,7 +1452,7 @@ try {
|
|||||||
remove();
|
remove();
|
||||||
reject(new Error('Cockpit camera ownership check did not render a frame'));
|
reject(new Error('Cockpit camera ownership check did not render a frame'));
|
||||||
}, 5000);
|
}, 5000);
|
||||||
const remove = viewer.scene.postRender.addEventListener(() => {
|
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||||
remove();
|
remove();
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
resolve();
|
resolve();
|
||||||
@@ -1473,10 +1460,10 @@ try {
|
|||||||
});
|
});
|
||||||
// Let revoked listeners retire before comparing ownership across exit.
|
// Let revoked listeners retire before comparing ownership across exit.
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
const listenersBeforeExit = viewer.scene.preUpdate.numberOfListeners;
|
const listenersBeforeExit = window.__godsEyeView.engine.frames.preUpdate.numberOfListeners;
|
||||||
const exited = styleManager.cockpitView.exit() === true;
|
const exited = styleManager.cockpitView.exit() === true;
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
const listenersAfterExit = viewer.scene.preUpdate.numberOfListeners;
|
const listenersAfterExit = window.__godsEyeView.engine.frames.preUpdate.numberOfListeners;
|
||||||
const refocused = awareness?.focusCurrent?.() === true;
|
const refocused = awareness?.focusCurrent?.() === true;
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
return {
|
return {
|
||||||
@@ -1485,7 +1472,7 @@ try {
|
|||||||
sameEntity: viewer.trackedEntity === entity,
|
sameEntity: viewer.trackedEntity === entity,
|
||||||
listenersBeforeExit,
|
listenersBeforeExit,
|
||||||
listenersAfterExit,
|
listenersAfterExit,
|
||||||
listenersAfterFocus: viewer.scene.preUpdate.numberOfListeners,
|
listenersAfterFocus: window.__godsEyeView.engine.frames.preUpdate.numberOfListeners,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
check(
|
check(
|
||||||
@@ -2088,7 +2075,7 @@ try {
|
|||||||
JSON.stringify(cyberStyleStates));
|
JSON.stringify(cyberStyleStates));
|
||||||
for (const height of [987, 720, 640, 900]) {
|
for (const height of [987, 720, 640, 900]) {
|
||||||
await page.setViewport({ width: 1440, height, deviceScaleFactor: 1 });
|
await page.setViewport({ width: 1440, height, deviceScaleFactor: 1 });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
await whenUiSettled(page);
|
||||||
const geometry = await page.evaluate(() => {
|
const geometry = await page.evaluate(() => {
|
||||||
const signal = document.getElementById('cockpit-signal-stream');
|
const signal = document.getElementById('cockpit-signal-stream');
|
||||||
const utility = document.getElementById('cockpit-utility-controls');
|
const utility = document.getElementById('cockpit-utility-controls');
|
||||||
@@ -2379,7 +2366,7 @@ try {
|
|||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const viewer = window.__godsEyeView.viewer;
|
const viewer = window.__godsEyeView.viewer;
|
||||||
if (!viewer) return false;
|
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;
|
return Math.abs(height - 18_000_000) < 150_000;
|
||||||
}, { timeout: 6_000 });
|
}, { timeout: 6_000 });
|
||||||
const resetState = await page.evaluate(() => {
|
const resetState = await page.evaluate(() => {
|
||||||
@@ -2387,7 +2374,7 @@ try {
|
|||||||
const qa = window.__qaCockpitReset;
|
const qa = window.__qaCockpitReset;
|
||||||
const awareness = gev.dataManager.layers.get('military-awareness')?.module;
|
const awareness = gev.dataManager.layers.get('military-awareness')?.module;
|
||||||
const subjectId = awareness?.getContextSnapshot?.()?.subject?.id || null;
|
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;
|
gev.styleManager._locationNavigation.resetToGlobeView = qa.original;
|
||||||
delete window.__qaCockpitReset;
|
delete window.__qaCockpitReset;
|
||||||
return {
|
return {
|
||||||
@@ -2440,7 +2427,7 @@ try {
|
|||||||
else hud.style.removeProperty('--cockpit-utility-top');
|
else hud.style.removeProperty('--cockpit-utility-top');
|
||||||
if (!prior.cockpit) document.body.classList.remove('cockpit-mode');
|
if (!prior.cockpit) document.body.classList.remove('cockpit-mode');
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`RESULT: ${failures.length ? 'NOT_READY' : 'READY'} (${failures.length} failures)`);
|
console.log(`RESULT: ${failures.length ? 'NOT_READY' : 'READY'} (${failures.length} failures)`);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||||
/**
|
/**
|
||||||
* qa-context-arrivals.mjs — search arrivals above the surface, and Contacts
|
* qa-context-arrivals.mjs — search arrivals above the surface, and Contacts
|
||||||
* listing mapped installations around a moving tracked aircraft.
|
* listing mapped installations around a moving tracked aircraft.
|
||||||
@@ -25,9 +26,10 @@
|
|||||||
* Writes <out>/result.json, screenshots and <out>/clip.mp4 (CDP screencast
|
* Writes <out>/result.json, screenshots and <out>/clip.mp4 (CDP screencast
|
||||||
* with real frame timing; needs ffmpeg).
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import { stubPlanespotters } from './qa-photo-stub.mjs';
|
import { stubPlanespotters } from './qa-photo-stub.mjs';
|
||||||
import { createScreencast, moveCamera } from './qa-journey-recorder.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}` : ''}`);
|
if (!ok) result.failures.push(`${name}${detail ? ` — ${detail}` : ''}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: !argv.includes('--headful'),
|
headless: true,
|
||||||
protocolTimeout: 300_000,
|
protocolTimeout: 300_000,
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
...(process.platform === 'darwin'
|
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||||
? ['--use-angle=metal', '--enable-gpu']
|
|
||||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-timer-throttling',
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-renderer-backgrounding',
|
||||||
'--disable-backgrounding-occluded-windows',
|
'--disable-backgrounding-occluded-windows',
|
||||||
@@ -171,12 +171,10 @@ async function openPage({ aircraft = false } = {}) {
|
|||||||
const url = new URL(appUrl);
|
const url = new URL(appUrl);
|
||||||
url.searchParams.set('welcome', '0');
|
url.searchParams.set('welcome', '0');
|
||||||
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||||
() =>
|
timeout: 120_000,
|
||||||
window.__godsEyeView?.dataManager &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 120_000 },
|
|
||||||
);
|
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
);
|
);
|
||||||
@@ -185,31 +183,16 @@ async function openPage({ aircraft = false } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function waitTiles(page, maxMs = 30_000) {
|
async function waitTiles(page, maxMs = 30_000) {
|
||||||
await page
|
await whenTilesLoaded(page, { timeoutMs: maxMs }).catch(() => {});
|
||||||
.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(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Camera height above the rendered surface under it and under the view centre. */
|
/** Camera height above the rendered surface under it and under the view centre. */
|
||||||
function clearance(page) {
|
function clearance(page) {
|
||||||
return page.evaluate(() => {
|
return evaluateWithGeo(page, async (qaGeo) => {
|
||||||
const { viewer } = window.__godsEyeView;
|
const carto = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||||
const { scene, camera } = viewer;
|
|
||||||
const carto = camera.positionCartographic.clone();
|
|
||||||
const sample = (c) => {
|
const sample = (c) => {
|
||||||
try {
|
try {
|
||||||
const h = scene.sampleHeight(c);
|
const h = window.__godsEyeView.engine.surface.sampleHeight(c);
|
||||||
return Number.isFinite(h) ? h : null;
|
return Number.isFinite(h) ? h : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -217,12 +200,12 @@ function clearance(page) {
|
|||||||
};
|
};
|
||||||
const under = sample(carto);
|
const under = sample(carto);
|
||||||
let centre = null;
|
let centre = null;
|
||||||
const canvas = scene.canvas;
|
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||||
const hit = scene.pickPosition?.({
|
const hit = window.__godsEyeView.engine.picker.pickPosition?.({
|
||||||
x: canvas.clientWidth / 2,
|
x: canvas.clientWidth / 2,
|
||||||
y: canvas.clientHeight / 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 centreSurface = centre ? sample(centre) : null;
|
||||||
const surface = Math.max(under ?? -Infinity, centreSurface ?? -Infinity);
|
const surface = Math.max(under ?? -Infinity, centreSurface ?? -Infinity);
|
||||||
return {
|
return {
|
||||||
@@ -234,7 +217,9 @@ function clearance(page) {
|
|||||||
clearanceM: Number.isFinite(surface)
|
clearanceM: Number.isFinite(surface)
|
||||||
? Math.round(carto.height - surface)
|
? Math.round(carto.height - surface)
|
||||||
: null,
|
: 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.type('#location-search', query, { delay: 40 });
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
// Wait for the flight to land: the camera still for 1.5 s.
|
await whenSettled(page);
|
||||||
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 waitTiles(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 arrived = await clearance(page);
|
||||||
const slug = query.replace(/\W+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
const slug = query.replace(/\W+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
||||||
await page.screenshot({ path: path.join(dir, `arrival-${slug}.png`) });
|
await page.screenshot({ path: path.join(dir, `arrival-${slug}.png`) });
|
||||||
// Look around: a gentle orbit, then check the eye still clears the ground.
|
// Look around: a gentle orbit, then check the eye still clears the ground.
|
||||||
const view = await page.evaluate(() => {
|
const view = await page.evaluate(() => {
|
||||||
const c = window.__godsEyeView.viewer.camera;
|
|
||||||
return {
|
return {
|
||||||
lat: (c.positionCartographic.latitude * 180) / Math.PI,
|
lat:
|
||||||
lon: (c.positionCartographic.longitude * 180) / Math.PI,
|
(window.__godsEyeView.engine.camera.pose().cartographic.latitude *
|
||||||
height: c.positionCartographic.height,
|
180) /
|
||||||
heading: (c.heading * 180) / Math.PI,
|
Math.PI,
|
||||||
pitch: (c.pitch * 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 moveCamera(page, { ...view, heading: view.heading + 25 }, 2.5);
|
||||||
await sleep(1500);
|
await whenFramesDrawn(page);
|
||||||
const lookedAround = await clearance(page);
|
const lookedAround = await clearance(page);
|
||||||
result.arrivals.push({ query, arrived, lookedAround });
|
result.arrivals.push({ query, arrived, lookedAround });
|
||||||
check(
|
check(
|
||||||
@@ -323,7 +293,7 @@ async function contacts() {
|
|||||||
{ lat: 30.35, lon: -97.76, height: 900_000, pitch: -90 },
|
{ lat: 30.35, lon: -97.76, height: 900_000, pitch: -90 },
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
await sleep(3000);
|
await whenFramesDrawn(page);
|
||||||
// In-page probe: row text and installation entity churn every 250 ms.
|
// In-page probe: row text and installation entity churn every 250 ms.
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const samples = [];
|
const samples = [];
|
||||||
@@ -368,7 +338,7 @@ async function contacts() {
|
|||||||
entities: sites.length,
|
entities: sites.length,
|
||||||
created,
|
created,
|
||||||
tracked:
|
tracked:
|
||||||
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ?? null,
|
window.__godsEyeView.engine.camera.following()?.label?.id ?? null,
|
||||||
cockpit: document.body.classList.contains('cockpit-mode'),
|
cockpit: document.body.classList.contains('cockpit-mode'),
|
||||||
});
|
});
|
||||||
if (samples.length > 5000) samples.splice(0, 1000);
|
if (samples.length > 5000) samples.splice(0, 1000);
|
||||||
@@ -383,7 +353,7 @@ async function contacts() {
|
|||||||
await page
|
await page
|
||||||
.waitForFunction(
|
.waitForFunction(
|
||||||
() =>
|
() =>
|
||||||
window.__godsEyeView.viewer.trackedEntity?.gevTrackedId ===
|
window.__godsEyeView.engine.camera.following()?.label?.id ===
|
||||||
'flights:aaa051',
|
'flights:aaa051',
|
||||||
{ timeout: 60_000 },
|
{ timeout: 60_000 },
|
||||||
)
|
)
|
||||||
@@ -478,7 +448,7 @@ async function contacts() {
|
|||||||
await page.evaluate(() =>
|
await page.evaluate(() =>
|
||||||
window.__godsEyeView.styleManager.cockpitView.exit?.(),
|
window.__godsEyeView.styleManager.cockpitView.exit?.(),
|
||||||
);
|
);
|
||||||
await sleep(2000);
|
await whenFramesDrawn(page);
|
||||||
|
|
||||||
// SEARCH NEARBY SITES, as the user presses it.
|
// SEARCH NEARBY SITES, as the user presses it.
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
@@ -504,7 +474,7 @@ async function contacts() {
|
|||||||
const toast = await page.evaluate(() =>
|
const toast = await page.evaluate(() =>
|
||||||
document.getElementById('toast').textContent.trim(),
|
document.getElementById('toast').textContent.trim(),
|
||||||
);
|
);
|
||||||
await sleep(1500);
|
await whenFramesDrawn(page);
|
||||||
const afterSearch = (
|
const afterSearch = (
|
||||||
await readSamples((await page.evaluate(() => performance.now())) - 1000)
|
await readSamples((await page.evaluate(() => performance.now())) - 1000)
|
||||||
).at(-1);
|
).at(-1);
|
||||||
@@ -519,13 +489,13 @@ async function contacts() {
|
|||||||
await page.evaluate(() =>
|
await page.evaluate(() =>
|
||||||
document.getElementById('global-context-flights-btn').click(),
|
document.getElementById('global-context-flights-btn').click(),
|
||||||
);
|
);
|
||||||
await sleep(2000);
|
await whenFramesDrawn(page);
|
||||||
await page.evaluate(() =>
|
await page.evaluate(() =>
|
||||||
window.__godsEyeView.dataManager.layers
|
window.__godsEyeView.dataManager.layers
|
||||||
.get('flights')
|
.get('flights')
|
||||||
.module.trackById('aaa052'),
|
.module.trackById('aaa052'),
|
||||||
);
|
);
|
||||||
await sleep(3000);
|
await whenFramesDrawn(page);
|
||||||
const t1 = await page.evaluate(() => performance.now());
|
const t1 = await page.evaluate(() => performance.now());
|
||||||
await page.evaluate(() =>
|
await page.evaluate(() =>
|
||||||
document.getElementById('global-context-flights-btn').click(),
|
document.getElementById('global-context-flights-btn').click(),
|
||||||
@@ -554,7 +524,7 @@ async function contacts() {
|
|||||||
}
|
}
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
}
|
}
|
||||||
await sleep(2000);
|
await whenFramesDrawn(page);
|
||||||
await page.screenshot({ path: path.join(dir, 'contacts-fort-cavazos.png') });
|
await page.screenshot({ path: path.join(dir, 'contacts-fort-cavazos.png') });
|
||||||
result.contacts.cavazos = cavazos;
|
result.contacts.cavazos = cavazos;
|
||||||
check(
|
check(
|
||||||
@@ -600,7 +570,7 @@ try {
|
|||||||
path.join(outDir, 'result.json'),
|
path.join(outDir, 'result.json'),
|
||||||
JSON.stringify(result, null, 1),
|
JSON.stringify(result, null, 1),
|
||||||
);
|
);
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
|
`${result.failures.length ? 'FAIL' : 'PASS'} — ${path.join(outDir, 'result.json')}`,
|
||||||
|
|||||||
+44
-43
@@ -1,4 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/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,
|
* Browser proof of the Directions layer: real chip clicks, real globe clicks,
|
||||||
* a real route from the real proxy, a real camera flight.
|
* 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]
|
* Usage: node scripts/qa-directions.mjs [--url http://localhost:4173]
|
||||||
* [--shots qa-shots/directions] [--label keyed] [--skip denver]
|
* [--shots qa-shots/directions] [--label keyed] [--skip denver]
|
||||||
*/
|
*/
|
||||||
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
@@ -34,9 +37,8 @@ const check = (name, passed, detail = '') => {
|
|||||||
if (!passed) failures += 1;
|
if (!passed) failures += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: process.env.QA_HEADFUL === '1' ? false : true,
|
headless: true,
|
||||||
executablePath: chromeExecutablePath(),
|
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-timer-throttling',
|
||||||
@@ -62,12 +64,12 @@ const shot = async (name) => {
|
|||||||
|
|
||||||
/** Put the camera over a place, instantly, and let the scene settle. */
|
/** Put the camera over a place, instantly, and let the scene settle. */
|
||||||
async function lookAt(lat, lon, height) {
|
async function lookAt(lat, lon, height) {
|
||||||
await page.evaluate(
|
await evaluateWithGeo(
|
||||||
(la, lo, h) => {
|
page,
|
||||||
const viewer = window.__godsEyeView.viewer;
|
async (qaGeo, la, lo, h) => {
|
||||||
const Cartesian3 = viewer.camera.positionWC.constructor;
|
const Cartesian3 = qaGeo.cartesian3;
|
||||||
viewer.camera.cancelFlight();
|
window.__godsEyeView.engine.camera.cancelFlight();
|
||||||
viewer.camera.setView({
|
window.__godsEyeView.engine.camera.setView({
|
||||||
destination: Cartesian3.fromDegrees(lo, la, h),
|
destination: Cartesian3.fromDegrees(lo, la, h),
|
||||||
orientation: { heading: 0, pitch: -Math.PI / 2.2, roll: 0 },
|
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. */
|
/** Canvas position of a lon/lat on the rendered surface, or null. */
|
||||||
const screenAt = (lat, lon) =>
|
const screenAt = (lat, lon) =>
|
||||||
page.evaluate(
|
evaluateWithGeo(
|
||||||
(la, lo) => {
|
page,
|
||||||
const viewer = window.__godsEyeView.viewer;
|
async (qaGeo, la, lo) => {
|
||||||
const scene = viewer.scene;
|
const Cartesian3 = qaGeo.cartesian3;
|
||||||
const Cartesian3 = viewer.camera.positionWC.constructor;
|
const Cartographic = qaGeo.cartographic;
|
||||||
const Cartographic = viewer.camera.positionCartographic.constructor;
|
|
||||||
const carto = Cartographic.fromDegrees(lo, la);
|
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;
|
if (!Number.isFinite(ground)) ground = 0;
|
||||||
const world = Cartesian3.fromDegrees(lo, la, ground);
|
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;
|
if (!win || !Number.isFinite(win.x)) return null;
|
||||||
const x = Math.round(win.x);
|
const x = Math.round(win.x);
|
||||||
const y = Math.round(win.y);
|
const y = Math.round(win.y);
|
||||||
@@ -150,7 +151,7 @@ const clickChip = async (chipId) => {
|
|||||||
button.click();
|
button.click();
|
||||||
return true;
|
return true;
|
||||||
}, chipId);
|
}, chipId);
|
||||||
await sleep(150);
|
await whenUiSettled(page);
|
||||||
return clicked;
|
return clicked;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ const waitForMode = (word) =>
|
|||||||
|
|
||||||
const cameraPose = () =>
|
const cameraPose = () =>
|
||||||
page.evaluate(() => {
|
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 };
|
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);
|
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);
|
const atA = await screenAt(a.lat, a.lon);
|
||||||
if (!atA) return false;
|
if (!atA) return false;
|
||||||
await page.mouse.click(atA.x, atA.y);
|
await page.mouse.click(atA.x, atA.y);
|
||||||
await sleep(700);
|
await whenUiSettled(page);
|
||||||
await clickChip('set-b');
|
await clickChip('set-b');
|
||||||
const atB = await screenAt(b.lat, b.lon);
|
const atB = await screenAt(b.lat, b.lon);
|
||||||
if (!atB) return false;
|
if (!atB) return false;
|
||||||
@@ -207,6 +208,7 @@ try {
|
|||||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||||
timeout: 90000,
|
timeout: 90000,
|
||||||
});
|
});
|
||||||
|
await whenSettled(page);
|
||||||
// Two module reads the assertions need. Both are the app's OWN module
|
// Two module reads the assertions need. Both are the app's OWN module
|
||||||
// instances (Vite serves one copy), so this observes rather than simulates.
|
// instances (Vite serves one copy), so this observes rather than simulates.
|
||||||
await page.evaluate(async () => {
|
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. ────────────────
|
// ── 1. Open the UTILITIES panel, then toggle the row on. ────────────────
|
||||||
const opened = await page.evaluate(() => {
|
const opened = await page.evaluate(() => {
|
||||||
@@ -328,7 +330,7 @@ try {
|
|||||||
.getElementById('utilities-panel')
|
.getElementById('utilities-panel')
|
||||||
.classList.contains('collapsed');
|
.classList.contains('collapsed');
|
||||||
});
|
});
|
||||||
await sleep(700);
|
await whenUiSettled(page);
|
||||||
check('the UTILITIES panel opens from its own header control', opened);
|
check('the UTILITIES panel opens from its own header control', opened);
|
||||||
const toggled = await page.evaluate(() => {
|
const toggled = await page.evaluate(() => {
|
||||||
const row = document.querySelector(
|
const row = document.querySelector(
|
||||||
@@ -340,7 +342,7 @@ try {
|
|||||||
button.click();
|
button.click();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
await sleep(1200);
|
await whenUiSettled(page);
|
||||||
check(
|
check(
|
||||||
'the Directions row is in the Data Layers panel and toggles on',
|
'the Directions row is in the Data Layers panel and toggles on',
|
||||||
toggled,
|
toggled,
|
||||||
@@ -388,13 +390,13 @@ try {
|
|||||||
);
|
);
|
||||||
close?.click();
|
close?.click();
|
||||||
});
|
});
|
||||||
await sleep(500);
|
await whenUiSettled(page);
|
||||||
|
|
||||||
// ── 2. Place A on the Ferry Building, B in Golden Gate Park. ─────────────
|
// ── 2. Place A on the Ferry Building, B in Golden Gate Park. ─────────────
|
||||||
const FERRY = { lat: 37.7955, lon: -122.3937 };
|
const FERRY = { lat: 37.7955, lon: -122.3937 };
|
||||||
const GGPARK = { lat: 37.7694, lon: -122.4862 };
|
const GGPARK = { lat: 37.7694, lon: -122.4862 };
|
||||||
await lookAt(37.782, -122.44, 16000);
|
await lookAt(37.782, -122.44, 16000);
|
||||||
await sleep(5000);
|
await whenSettled(page);
|
||||||
|
|
||||||
await clickChip('set-a');
|
await clickChip('set-a');
|
||||||
state = await layerState();
|
state = await layerState();
|
||||||
@@ -411,7 +413,7 @@ try {
|
|||||||
let at = await screenAt(FERRY.lat, FERRY.lon);
|
let at = await screenAt(FERRY.lat, FERRY.lon);
|
||||||
check('the Ferry Building is on screen', !!at, JSON.stringify(at));
|
check('the Ferry Building is on screen', !!at, JSON.stringify(at));
|
||||||
await page.mouse.click(at.x, at.y);
|
await page.mouse.click(at.x, at.y);
|
||||||
await sleep(800);
|
await whenUiSettled(page);
|
||||||
state = await layerState();
|
state = await layerState();
|
||||||
check(
|
check(
|
||||||
'placing A releases the pointer claim',
|
'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.
|
// used to place B and deselect whatever was under it in the same gesture.
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.__gevQaLateHandler = [];
|
window.__gevQaLateHandler = [];
|
||||||
const canvas = window.__godsEyeView.viewer.scene.canvas;
|
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||||
canvas.addEventListener('pointerup', () => {
|
canvas.addEventListener('pointerup', () => {
|
||||||
window.__gevQaLateHandler.push(window.__gevQa.pointerOwner());
|
window.__gevQaLateHandler.push(window.__gevQa.pointerOwner());
|
||||||
});
|
});
|
||||||
@@ -443,7 +445,7 @@ try {
|
|||||||
`saw ${JSON.stringify(lateOwners)}`,
|
`saw ${JSON.stringify(lateOwners)}`,
|
||||||
);
|
);
|
||||||
await waitForRoute();
|
await waitForRoute();
|
||||||
await sleep(2000);
|
await whenSettled(page);
|
||||||
state = await layerState();
|
state = await layerState();
|
||||||
check(
|
check(
|
||||||
'once that click is over, the pointer claim is returned',
|
'once that click is over, the pointer claim is returned',
|
||||||
@@ -488,7 +490,7 @@ try {
|
|||||||
items[2]?.click();
|
items[2]?.click();
|
||||||
return { active, tabIndex: items[2]?.tabIndex };
|
return { active, tabIndex: items[2]?.tabIndex };
|
||||||
});
|
});
|
||||||
await sleep(600);
|
await whenUiSettled(page);
|
||||||
const afterListClick = await layerState();
|
const afterListClick = await layerState();
|
||||||
check(
|
check(
|
||||||
'a list row takes keyboard focus and selects its maneuver',
|
'a list row takes keyboard focus and selects its maneuver',
|
||||||
@@ -525,7 +527,7 @@ try {
|
|||||||
// ── 4. Swap endpoints. ──────────────────────────────────────────────────
|
// ── 4. Swap endpoints. ──────────────────────────────────────────────────
|
||||||
const beforeSwap = await layerState();
|
const beforeSwap = await layerState();
|
||||||
await clickChip('swap');
|
await clickChip('swap');
|
||||||
await sleep(3500);
|
await waitForRoute();
|
||||||
const afterSwap = await layerState();
|
const afterSwap = await layerState();
|
||||||
check(
|
check(
|
||||||
'SWAP reverses the route',
|
'SWAP reverses the route',
|
||||||
@@ -534,7 +536,7 @@ try {
|
|||||||
`"${beforeSwap.listText[0]}" -> "${afterSwap.listText[0]}"`,
|
`"${beforeSwap.listText[0]}" -> "${afterSwap.listText[0]}"`,
|
||||||
);
|
);
|
||||||
await clickChip('swap');
|
await clickChip('swap');
|
||||||
await sleep(3500);
|
await waitForRoute();
|
||||||
|
|
||||||
// ── 5. Three rapid reroutes leave exactly one route. ─────────────────────
|
// ── 5. Three rapid reroutes leave exactly one route. ─────────────────────
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
@@ -543,7 +545,7 @@ try {
|
|||||||
manager.setLayerParams('directions', { mode: 'bike' }, { origin: 'user' });
|
manager.setLayerParams('directions', { mode: 'bike' }, { origin: 'user' });
|
||||||
manager.setLayerParams('directions', { mode: 'car' }, { origin: 'user' });
|
manager.setLayerParams('directions', { mode: 'car' }, { origin: 'user' });
|
||||||
});
|
});
|
||||||
await sleep(6000);
|
await waitForRoute();
|
||||||
const rapid = await layerState();
|
const rapid = await layerState();
|
||||||
check(
|
check(
|
||||||
'three rapid reroutes leave exactly one route',
|
'three rapid reroutes leave exactly one route',
|
||||||
@@ -656,7 +658,7 @@ try {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
await waitForRoute();
|
await waitForRoute();
|
||||||
await sleep(1200);
|
await whenUiSettled(page);
|
||||||
}
|
}
|
||||||
await cdp.send('Fetch.disable').catch(() => {});
|
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
|
// 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.
|
// first rather than keep flying a route that no longer exists.
|
||||||
await setMode('foot');
|
await setMode('foot');
|
||||||
await sleep(1200);
|
await whenUiSettled(page);
|
||||||
const afterReroute = await layerState();
|
const afterReroute = await layerState();
|
||||||
check(
|
check(
|
||||||
'changing profile mid-flight lands the flight it replaced',
|
'changing profile mid-flight lands the flight it replaced',
|
||||||
@@ -696,7 +698,7 @@ try {
|
|||||||
await waitForMode('Walk');
|
await waitForMode('Walk');
|
||||||
await setMode('car');
|
await setMode('car');
|
||||||
await waitForMode('Drive');
|
await waitForMode('Drive');
|
||||||
await sleep(800);
|
await whenUiSettled(page);
|
||||||
check('FLY is offered again after the reroute', await clickChip('fly'));
|
check('FLY is offered again after the reroute', await clickChip('fly'));
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
|
|
||||||
@@ -768,7 +770,7 @@ try {
|
|||||||
// ── 9. Denver: maneuver dots sit on the ground, not at sea level. ────────
|
// ── 9. Denver: maneuver dots sit on the ground, not at sea level. ────────
|
||||||
if (!SKIP.has('denver')) {
|
if (!SKIP.has('denver')) {
|
||||||
await lookAt(39.744, -104.999, 7000);
|
await lookAt(39.744, -104.999, 7000);
|
||||||
await sleep(6000);
|
await whenSettled(page);
|
||||||
const placed = await placeEndpoints(
|
const placed = await placeEndpoints(
|
||||||
{ lat: 39.7392, lon: -104.9903 },
|
{ lat: 39.7392, lon: -104.9903 },
|
||||||
{ lat: 39.7487, lon: -105.0077 },
|
{ lat: 39.7487, lon: -105.0077 },
|
||||||
@@ -777,10 +779,9 @@ try {
|
|||||||
if (placed) {
|
if (placed) {
|
||||||
await waitForRoute();
|
await waitForRoute();
|
||||||
// Let the shared ground-floor cells warm and the dots re-anchor.
|
// Let the shared ground-floor cells warm and the dots re-anchor.
|
||||||
await sleep(9000);
|
await whenTilesLoaded(page);
|
||||||
const denver = await page.evaluate(() => {
|
const denver = await evaluateWithGeo(page, async (qaGeo) => {
|
||||||
const viewer = window.__godsEyeView.viewer;
|
const Cartographic = qaGeo.cartographic;
|
||||||
const Cartographic = viewer.camera.positionCartographic.constructor;
|
|
||||||
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
|
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
|
||||||
const heights = stepsShown.map(
|
const heights = stepsShown.map(
|
||||||
(position) => Cartographic.fromCartesian(position).height,
|
(position) => Cartographic.fromCartesian(position).height,
|
||||||
@@ -817,7 +818,7 @@ try {
|
|||||||
origin: 'user',
|
origin: 'user',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await sleep(1500);
|
await whenSettled(page);
|
||||||
const off = await page.evaluate(() => {
|
const off = await page.evaluate(() => {
|
||||||
const drawn = window.__gevQaDirections.drawn();
|
const drawn = window.__gevQaDirections.drawn();
|
||||||
const row = document.querySelector(
|
const row = document.querySelector(
|
||||||
@@ -877,7 +878,7 @@ try {
|
|||||||
check('harness completed', false, error?.stack || String(error));
|
check('harness completed', false, error?.stack || String(error));
|
||||||
await shot(`99-${LABEL}-failure.jpg`).catch(() => {});
|
await shot(`99-${LABEL}-failure.jpg`).catch(() => {});
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(SHOTS, `qa-directions-${LABEL}.log`),
|
path.join(SHOTS, `qa-directions-${LABEL}.log`),
|
||||||
`${lines.join('\n')}\n${failures} failure(s)\n`,
|
`${lines.join('\n')}\n${failures} failure(s)\n`,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/** Verify imported camera directions through real rendering and camera authority handoffs. */
|
/** 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
const output = path.resolve('qa-shots/director-camera');
|
const output = path.resolve('qa-shots/director-camera');
|
||||||
fs.mkdirSync(output, { recursive: true });
|
fs.mkdirSync(output, { recursive: true });
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||||
});
|
});
|
||||||
@@ -23,12 +24,10 @@ try {
|
|||||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||||
{ waitUntil: 'domcontentloaded' },
|
{ waitUntil: 'domcontentloaded' },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||||
() =>
|
timeout: 60000,
|
||||||
window.__godsEyeView?.sceneDirector &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
const result = await page.evaluate(async () => {
|
const result = await page.evaluate(async () => {
|
||||||
const { sceneDirector: d, styleManager: style } = window.__godsEyeView;
|
const { sceneDirector: d, styleManager: style } = window.__godsEyeView;
|
||||||
const file = {
|
const file = {
|
||||||
@@ -137,7 +136,7 @@ try {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
for (const [name, passed] of Object.entries(result)) check(name, passed);
|
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.screenshot({ path: path.join(output, 'austin.png') });
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const d = window.__godsEyeView.sceneDirector;
|
const d = window.__godsEyeView.sceneDirector;
|
||||||
@@ -145,7 +144,7 @@ try {
|
|||||||
d._setCameraView({ ...pose, heading: pose.heading + 25, pitch: -60 });
|
d._setCameraView({ ...pose, heading: pose.heading + 25, pitch: -60 });
|
||||||
d.viewer.scene.requestRender();
|
d.viewer.scene.requestRender();
|
||||||
});
|
});
|
||||||
await new Promise((r) => setTimeout(r, 3000));
|
await whenSettled(page);
|
||||||
await page.screenshot({ path: path.join(output, 'angle.png') });
|
await page.screenshot({ path: path.join(output, 'angle.png') });
|
||||||
const inputStarted = await page.evaluate(async () => {
|
const inputStarted = await page.evaluate(async () => {
|
||||||
const d = window.__godsEyeView.sceneDirector;
|
const d = window.__godsEyeView.sceneDirector;
|
||||||
@@ -205,6 +204,6 @@ try {
|
|||||||
);
|
);
|
||||||
check('no uncaught browser errors', errors.length === 0);
|
check('no uncaught browser errors', errors.length === 0);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
if (failed) process.exitCode = 1;
|
if (failed) process.exitCode = 1;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/** Real pointer/keyboard acceptance using a synthetic, self-authored feature pack. */
|
/** 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 fs from 'node:fs';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
const output = 'qa-shots/director-interactions';
|
const output = 'qa-shots/director-interactions';
|
||||||
fs.mkdirSync(output, { recursive: true });
|
fs.mkdirSync(output, { recursive: true });
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
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=`,
|
`${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' },
|
{ waitUntil: 'domcontentloaded' },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||||
() =>
|
timeout: 60000,
|
||||||
window.__godsEyeView?.sceneDirector &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const d = window.__godsEyeView.sceneDirector;
|
const d = window.__godsEyeView.sceneDirector;
|
||||||
const camera = {
|
const camera = {
|
||||||
@@ -163,11 +162,8 @@ try {
|
|||||||
window.__godsEyeView.sceneDirector.getInteractionState().count === 4,
|
window.__godsEyeView.sceneDirector.getInteractionState().count === 4,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||||
() => window.__godsEyeView.sceneDirector.engine.surface.tilesLoaded(),
|
await whenSettled(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
|
||||||
const point = await page.evaluate(async () => {
|
const point = await page.evaluate(async () => {
|
||||||
const d = window.__godsEyeView.sceneDirector;
|
const d = window.__godsEyeView.sceneDirector;
|
||||||
const { cartesian3 } = await import('/src/geo/index.js');
|
const { cartesian3 } = await import('/src/geo/index.js');
|
||||||
@@ -366,6 +362,6 @@ try {
|
|||||||
);
|
);
|
||||||
check('no uncaught browser errors', errors.length === 0);
|
check('no uncaught browser errors', errors.length === 0);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
process.exitCode = failures ? 1 : 0;
|
process.exitCode = failures ? 1 : 0;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
#!/usr/bin/env node
|
#!/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. */
|
/** 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
const output = path.resolve('qa-shots/director-packs');
|
const output = path.resolve('qa-shots/director-packs');
|
||||||
fs.mkdirSync(output, { recursive: true });
|
fs.mkdirSync(output, { recursive: true });
|
||||||
@@ -65,7 +67,7 @@ const geojson = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||||
});
|
});
|
||||||
@@ -105,12 +107,10 @@ try {
|
|||||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||||
{ waitUntil: 'domcontentloaded' },
|
{ waitUntil: 'domcontentloaded' },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||||
() =>
|
timeout: 60000,
|
||||||
window.__godsEyeView?.sceneDirector &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const d = window.__godsEyeView.sceneDirector;
|
const d = window.__godsEyeView.sceneDirector;
|
||||||
const manifest = (id, format, file, placement) => ({
|
const manifest = (id, format, file, placement) => ({
|
||||||
@@ -230,14 +230,11 @@ try {
|
|||||||
() => window.__godsEyeView.mapStackController.getActiveId() === 'photoreal',
|
() => window.__godsEyeView.mapStackController.getActiveId() === 'photoreal',
|
||||||
{ timeout: 15000 },
|
{ timeout: 15000 },
|
||||||
);
|
);
|
||||||
await new Promise((r) => setTimeout(r, 5000));
|
await whenSettled(page);
|
||||||
await page.waitForFunction(
|
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||||
() => window.__godsEyeView.tileset?.tilesLoaded === true,
|
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
const evidence = await page.evaluate(() => ({
|
const evidence = await page.evaluate(() => ({
|
||||||
mapStack: window.__godsEyeView.mapStackController.getActiveId(),
|
mapStack: window.__godsEyeView.mapStackController.getActiveId(),
|
||||||
tilesSettled: window.__godsEyeView.tileset?.tilesLoaded === true,
|
tilesSettled: window.__godsEyeView.engine.surface.tilesLoaded() === true,
|
||||||
}));
|
}));
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(output, 'report.json'),
|
path.join(output, 'report.json'),
|
||||||
@@ -262,11 +259,8 @@ try {
|
|||||||
pitch: -85,
|
pitch: -85,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await page.waitForFunction(
|
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||||
() => window.__godsEyeView.tileset?.tilesLoaded === true,
|
await whenSettled(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
|
||||||
await page.screenshot({ path: path.join(output, 'angle.png') });
|
await page.screenshot({ path: path.join(output, 'angle.png') });
|
||||||
const life = await page.evaluate(async () => {
|
const life = await page.evaluate(async () => {
|
||||||
const d = window.__godsEyeView.sceneDirector;
|
const d = window.__godsEyeView.sceneDirector;
|
||||||
@@ -306,9 +300,10 @@ try {
|
|||||||
'slow',
|
'slow',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const deadline = Date.now() + 15000;
|
await until(() => pending, {
|
||||||
while (!pending && Date.now() < deadline)
|
timeoutMs: 15000,
|
||||||
await new Promise((r) => setTimeout(r, 50));
|
label: 'pending pack transport',
|
||||||
|
});
|
||||||
check('pending transport reached', !!pending);
|
check('pending transport reached', !!pending);
|
||||||
const cancelled = await page.evaluate(async () => {
|
const cancelled = await page.evaluate(async () => {
|
||||||
const d = window.__godsEyeView.sceneDirector;
|
const d = window.__godsEyeView.sceneDirector;
|
||||||
@@ -354,6 +349,6 @@ try {
|
|||||||
if (errors.length)
|
if (errors.length)
|
||||||
console.error(errors.map((e) => e.replace(/https?:\/\/\S+/g, '[URL]')));
|
console.error(errors.map((e) => e.replace(/https?:\/\/\S+/g, '[URL]')));
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
if (failures) process.exitCode = 1;
|
if (failures) process.exitCode = 1;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/** Sharing/authoring acceptance with local synthetic assets and real installed controls. */
|
/** 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
const output = path.resolve('qa-shots/director-sharing');
|
const output = path.resolve('qa-shots/director-sharing');
|
||||||
fs.mkdirSync(output, { recursive: true });
|
fs.mkdirSync(output, { recursive: true });
|
||||||
const geometry = {
|
const geometry = {
|
||||||
@@ -66,7 +67,7 @@ const project = {
|
|||||||
};
|
};
|
||||||
const file = path.join(output, 'project.json');
|
const file = path.join(output, 'project.json');
|
||||||
fs.writeFileSync(file, JSON.stringify(project));
|
fs.writeFileSync(file, JSON.stringify(project));
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||||
});
|
});
|
||||||
@@ -110,12 +111,10 @@ try {
|
|||||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||||
{ waitUntil: 'domcontentloaded' },
|
{ waitUntil: 'domcontentloaded' },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||||
() =>
|
timeout: 60000,
|
||||||
window.__godsEyeView?.sceneDirector &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
await page.click('[data-collapse-target="scene-panel"]');
|
await page.click('[data-collapse-target="scene-panel"]');
|
||||||
const before = await page.evaluate(() =>
|
const before = await page.evaluate(() =>
|
||||||
JSON.stringify(window.__godsEyeView.sceneDirector._project),
|
JSON.stringify(window.__godsEyeView.sceneDirector._project),
|
||||||
@@ -304,16 +303,14 @@ try {
|
|||||||
window.__godsEyeView.sceneDirector.getInteractionState().count === 1,
|
window.__godsEyeView.sceneDirector.getInteractionState().count === 1,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
||||||
() => window.__godsEyeView.tileset?.tilesLoaded === true,
|
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
await page.screenshot({ path: path.join(output, 'rendered.png') });
|
await page.screenshot({ path: path.join(output, 'rendered.png') });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(output, 'report.json'),
|
path.join(output, 'report.json'),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
await page.evaluate(() => ({
|
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);
|
check('no uncaught browser errors', errors.length === 0);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
process.exitCode = failures ? 1 : 0;
|
process.exitCode = failures ? 1 : 0;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/** Exercise scene-clock ownership and authored seek behavior in the composed browser app. */
|
/** 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
const shots = path.resolve('qa-shots/director-timing');
|
const shots = path.resolve('qa-shots/director-timing');
|
||||||
fs.mkdirSync(shots, { recursive: true });
|
fs.mkdirSync(shots, { recursive: true });
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||||
});
|
});
|
||||||
@@ -23,12 +24,10 @@ try {
|
|||||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||||
{ waitUntil: 'domcontentloaded' },
|
{ waitUntil: 'domcontentloaded' },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||||
() =>
|
timeout: 60000,
|
||||||
window.__godsEyeView?.sceneDirector &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
const result = await page.evaluate(async () => {
|
const result = await page.evaluate(async () => {
|
||||||
const director = window.__godsEyeView.sceneDirector;
|
const director = window.__godsEyeView.sceneDirector;
|
||||||
const camera = director.styleManager.getCameraState();
|
const camera = director.styleManager.getCameraState();
|
||||||
@@ -172,7 +171,7 @@ try {
|
|||||||
'existing Nepal scene and authored shots load',
|
'existing Nepal scene and authored shots load',
|
||||||
content.exists && content.shots >= 25 && content.loaded,
|
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.screenshot({ path: path.join(shots, 'nepal-regional.png') });
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const g = window.__godsEyeView;
|
const g = window.__godsEyeView;
|
||||||
@@ -184,7 +183,7 @@ try {
|
|||||||
pitch: -55,
|
pitch: -55,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await whenSettled(page);
|
||||||
await page.screenshot({ path: path.join(shots, 'nepal-angle.png') });
|
await page.screenshot({ path: path.join(shots, 'nepal-angle.png') });
|
||||||
check(
|
check(
|
||||||
'no pending clocks after Stop',
|
'no pending clocks after Stop',
|
||||||
@@ -196,6 +195,6 @@ try {
|
|||||||
);
|
);
|
||||||
check('no browser exceptions', errors.length === 0);
|
check('no browser exceptions', errors.length === 0);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
if (failures) process.exitCode = 1;
|
if (failures) process.exitCode = 1;
|
||||||
+142
-158
@@ -1,8 +1,11 @@
|
|||||||
#!/usr/bin/env node
|
#!/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. */
|
/** 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||||
|
|
||||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
|
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||||
@@ -19,21 +22,14 @@ const VIEW = {
|
|||||||
pitch: -85,
|
pitch: -85,
|
||||||
};
|
};
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await launchBrowser({
|
||||||
headless: !HEADFUL,
|
headless: true,
|
||||||
...(HEADFUL
|
|
||||||
? {
|
|
||||||
executablePath: chromeExecutablePath(),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-timer-throttling',
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-renderer-backgrounding',
|
||||||
...(HEADFUL ? ['--window-size=1280,860'] : []),
|
...(HEADFUL ? ['--window-size=1280,860'] : []),
|
||||||
...(process.platform === 'darwin'
|
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||||
? ['--use-angle=metal', '--enable-gpu']
|
|
||||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
@@ -68,7 +64,6 @@ const shot = (name) =>
|
|||||||
type: 'jpeg',
|
type: 'jpeg',
|
||||||
quality: 82,
|
quality: 82,
|
||||||
});
|
});
|
||||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
/** A clip of the globe canvas, clear of the panels on every side. */
|
/** A clip of the globe canvas, clear of the panels on every side. */
|
||||||
const CANVAS_CLIP = { x: 340, y: 200, width: 560, height: 380 };
|
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. */
|
/** One click on the globe canvas, the way a person makes one. */
|
||||||
async function clickWorld(x, y, options = {}) {
|
async function clickWorld(x, y, options = {}) {
|
||||||
await page.mouse.click(x, y, options);
|
await page.mouse.click(x, y, options);
|
||||||
await wait(options.settle ?? 140);
|
await whenSettled(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawState = () =>
|
const drawState = () =>
|
||||||
@@ -211,50 +206,34 @@ try {
|
|||||||
{ waitUntil: 'domcontentloaded' },
|
{ waitUntil: 'domcontentloaded' },
|
||||||
);
|
);
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
() =>
|
() => window.__gevDrawTool && window.__gevAnnotations,
|
||||||
window.__gevDrawTool &&
|
|
||||||
window.__gevAnnotations &&
|
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
||||||
{ timeout: 90_000 },
|
{ timeout: 90_000 },
|
||||||
);
|
);
|
||||||
await page.evaluate((view) => {
|
await whenSettled(page);
|
||||||
const viewer = window.__godsEyeView.viewer;
|
await evaluateWithGeo(
|
||||||
viewer.camera.cancelFlight?.();
|
page,
|
||||||
viewer.scene.tweens?.removeAll?.();
|
async (qaGeo, view) => {
|
||||||
viewer.camera.setView({
|
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
window.__godsEyeView.engine.camera.setView({
|
||||||
longitude: (view.lon * Math.PI) / 180,
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
latitude: (view.lat * Math.PI) / 180,
|
longitude: (view.lon * Math.PI) / 180,
|
||||||
height: view.height,
|
latitude: (view.lat * Math.PI) / 180,
|
||||||
}),
|
height: view.height,
|
||||||
orientation: {
|
}),
|
||||||
heading: (view.heading * Math.PI) / 180,
|
orientation: {
|
||||||
pitch: (view.pitch * Math.PI) / 180,
|
heading: (view.heading * Math.PI) / 180,
|
||||||
roll: 0,
|
pitch: (view.pitch * Math.PI) / 180,
|
||||||
},
|
roll: 0,
|
||||||
});
|
},
|
||||||
viewer.scene.requestRender();
|
});
|
||||||
}, VIEW);
|
window.__godsEyeView.engine.render.request();
|
||||||
const settled = await page
|
},
|
||||||
.waitForFunction(
|
VIEW,
|
||||||
() => {
|
);
|
||||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
const settled = await whenTilesLoaded(page, { timeoutMs: 90_000 }).then(
|
||||||
for (let index = 0; index < primitives.length; index++) {
|
() => true,
|
||||||
const primitive = primitives.get(index);
|
() => false,
|
||||||
if (
|
);
|
||||||
typeof primitive.tilesLoaded === 'boolean' &&
|
|
||||||
!primitive.tilesLoaded
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
{ timeout: 90_000 },
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
() => true,
|
|
||||||
() => false,
|
|
||||||
);
|
|
||||||
check('visible tile content settles before drawing', settled);
|
check('visible tile content settles before drawing', settled);
|
||||||
|
|
||||||
// UTILITIES starts collapsed on a first run: open it the way a user does.
|
// UTILITIES starts collapsed on a first run: open it the way a user does.
|
||||||
@@ -321,9 +300,9 @@ try {
|
|||||||
const before = await drawState();
|
const before = await drawState();
|
||||||
for (let cycle = 0; cycle < 3; cycle += 1) {
|
for (let cycle = 0; cycle < 3; cycle += 1) {
|
||||||
await page.click('#draw-toggle');
|
await page.click('#draw-toggle');
|
||||||
await wait(120);
|
await whenSettled(page);
|
||||||
await page.click('#draw-toggle');
|
await page.click('#draw-toggle');
|
||||||
await wait(120);
|
await whenSettled(page);
|
||||||
}
|
}
|
||||||
const after = await drawState();
|
const after = await drawState();
|
||||||
check(
|
check(
|
||||||
@@ -379,7 +358,7 @@ try {
|
|||||||
await clickWorld(600, 470);
|
await clickWorld(600, 470);
|
||||||
const beforeBackspace = (await drawState()).vertices;
|
const beforeBackspace = (await drawState()).vertices;
|
||||||
await page.keyboard.press('Backspace');
|
await page.keyboard.press('Backspace');
|
||||||
await wait(120);
|
await whenSettled(page);
|
||||||
const afterBackspace = (await drawState()).vertices;
|
const afterBackspace = (await drawState()).vertices;
|
||||||
check(
|
check(
|
||||||
'Backspace removes the last vertex only',
|
'Backspace removes the last vertex only',
|
||||||
@@ -415,7 +394,7 @@ try {
|
|||||||
);
|
);
|
||||||
await page.mouse.click(410, 500, { count: 2 });
|
await page.mouse.click(410, 500, { count: 2 });
|
||||||
const areaLanded = await waitForMarks(3);
|
const areaLanded = await waitForMarks(3);
|
||||||
await wait(900);
|
await whenSettled(page);
|
||||||
const afterDoubleClick = await page.evaluate(() =>
|
const afterDoubleClick = await page.evaluate(() =>
|
||||||
window.__gevAnnotations.count(),
|
window.__gevAnnotations.count(),
|
||||||
);
|
);
|
||||||
@@ -440,7 +419,7 @@ try {
|
|||||||
await page.click(rowControl(areaId, 'name'));
|
await page.click(rowControl(areaId, 'name'));
|
||||||
await page.keyboard.type('Civic Center block');
|
await page.keyboard.type('Civic Center block');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await wait(300);
|
await whenSettled(page);
|
||||||
const areaMark = (await marks()).find(
|
const areaMark = (await marks()).find(
|
||||||
(mark) => mark.label === 'Civic Center block',
|
(mark) => mark.label === 'Civic Center block',
|
||||||
);
|
);
|
||||||
@@ -453,7 +432,7 @@ try {
|
|||||||
'naming a shape keeps Draw on and the shape in progress untouched',
|
'naming a shape keeps Draw on and the shape in progress untouched',
|
||||||
(await drawState()).active === true,
|
(await drawState()).active === true,
|
||||||
);
|
);
|
||||||
await wait(1200);
|
await whenSettled(page);
|
||||||
const amberAfter = await amberPixels();
|
const amberAfter = await amberPixels();
|
||||||
check(
|
check(
|
||||||
'the finished area actually PAINTS — amber pixels appear on this surface',
|
'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
|
// 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.
|
// middle of the line sits near the middle of the view.
|
||||||
await page.click(rowControl(ids[1], 'fly'));
|
await page.click(rowControl(ids[1], 'fly'));
|
||||||
await page
|
await whenSettled(page).catch(() => {});
|
||||||
.waitForFunction(() => !window.__godsEyeView.viewer.camera._currentFlight, {
|
await whenSettled(page);
|
||||||
timeout: 15_000,
|
const framed = await evaluateWithGeo(
|
||||||
})
|
page,
|
||||||
.catch(() => {});
|
async (qaGeo, id) => {
|
||||||
await wait(300);
|
// No Cesium global in the page: borrow Cartesian3 from a live object.
|
||||||
const framed = await page.evaluate((id) => {
|
const Cartesian3 = qaGeo.cartesian3;
|
||||||
const { viewer } = window.__godsEyeView;
|
const mark = window.__gevAnnotations.list().find((m) => m.id === id);
|
||||||
// No Cesium global in the page: borrow Cartesian3 from a live object.
|
const lons = mark.path.map((p) => p.lon);
|
||||||
const Cartesian3 = viewer.camera.positionWC.constructor;
|
const lats = mark.path.map((p) => p.lat);
|
||||||
const mark = window.__gevAnnotations.list().find((m) => m.id === id);
|
const middle = Cartesian3.fromDegrees(
|
||||||
const lons = mark.path.map((p) => p.lon);
|
(Math.min(...lons) + Math.max(...lons)) / 2,
|
||||||
const lats = mark.path.map((p) => p.lat);
|
(Math.min(...lats) + Math.max(...lats)) / 2,
|
||||||
const middle = Cartesian3.fromDegrees(
|
);
|
||||||
(Math.min(...lons) + Math.max(...lons)) / 2,
|
const at = window.__godsEyeView.engine.projector.worldToWindow(middle);
|
||||||
(Math.min(...lats) + Math.max(...lats)) / 2,
|
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||||
);
|
return (
|
||||||
const at = viewer.scene.cartesianToCanvasCoordinates(middle);
|
at && {
|
||||||
const canvas = viewer.scene.canvas;
|
dx: Math.abs(at.x - canvas.clientWidth / 2) / canvas.clientWidth,
|
||||||
return (
|
dy: Math.abs(at.y - canvas.clientHeight / 2) / canvas.clientHeight,
|
||||||
at && {
|
}
|
||||||
dx: Math.abs(at.x - canvas.clientWidth / 2) / canvas.clientWidth,
|
);
|
||||||
dy: Math.abs(at.y - canvas.clientHeight / 2) / canvas.clientHeight,
|
},
|
||||||
}
|
ids[1],
|
||||||
);
|
);
|
||||||
}, ids[1]);
|
|
||||||
check(
|
check(
|
||||||
'fly to frames that shape: its middle is near the middle of the view',
|
'fly to frames that shape: its middle is near the middle of the view',
|
||||||
Boolean(framed) && framed.dx < 0.2 && framed.dy < 0.2,
|
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, 'swatch'));
|
||||||
await page.click(rowControl(lineId, 'color-amber'));
|
await page.click(rowControl(lineId, 'color-amber'));
|
||||||
await markReaches(lineId, { color: 'amber', alpha: 1 });
|
await markReaches(lineId, { color: 'amber', alpha: 1 });
|
||||||
await wait(800);
|
await whenSettled(page);
|
||||||
const shown = await canvasShot();
|
const shown = await canvasShot();
|
||||||
await page.click(rowControl(lineId, 'toggle'));
|
await page.click(rowControl(lineId, 'toggle'));
|
||||||
await markReaches(lineId, { hidden: true, alpha: 0 });
|
await markReaches(lineId, { hidden: true, alpha: 0 });
|
||||||
await wait(800);
|
await whenSettled(page);
|
||||||
const hiddenShot = await canvasShot();
|
const hiddenShot = await canvasShot();
|
||||||
// Amber the shown line had and the hidden one lost; amber that appears
|
// 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.
|
// 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`,
|
`${gone} amber pixels went with the line, ${noise} appeared`,
|
||||||
);
|
);
|
||||||
await page.click(rowControl(lineId, 'toggle'));
|
await page.click(rowControl(lineId, 'toggle'));
|
||||||
await wait(1200);
|
await whenSettled(page);
|
||||||
const back = await amberOnlyIn(await canvasShot(), hiddenShot);
|
const back = await amberOnlyIn(await canvasShot(), hiddenShot);
|
||||||
check(
|
check(
|
||||||
'and paints again when shown',
|
'and paints again when shown',
|
||||||
@@ -581,7 +559,7 @@ try {
|
|||||||
await clickWorld(560, 340);
|
await clickWorld(560, 340);
|
||||||
check('a new shape is in progress', (await drawState()).vertices === 2);
|
check('a new shape is in progress', (await drawState()).vertices === 2);
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await wait(150);
|
await whenSettled(page);
|
||||||
const afterFirstEscape = await drawState();
|
const afterFirstEscape = await drawState();
|
||||||
check(
|
check(
|
||||||
'Escape cancels the shape in progress and stays in draw mode',
|
'Escape cancels the shape in progress and stays in draw mode',
|
||||||
@@ -589,7 +567,7 @@ try {
|
|||||||
`vertices ${afterFirstEscape.vertices}, active ${afterFirstEscape.active}`,
|
`vertices ${afterFirstEscape.vertices}, active ${afterFirstEscape.active}`,
|
||||||
);
|
);
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await wait(150);
|
await whenSettled(page);
|
||||||
const afterSecondEscape = await drawState();
|
const afterSecondEscape = await drawState();
|
||||||
check(
|
check(
|
||||||
'a second Escape leaves draw mode and gives the pointer back',
|
'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 page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await wait(150);
|
await whenSettled(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── what a reload actually does ─────────────────────────────────────────
|
// ── what a reload actually does ─────────────────────────────────────────
|
||||||
@@ -675,12 +653,10 @@ try {
|
|||||||
window.__gevAnnotations.count(),
|
window.__gevAnnotations.count(),
|
||||||
);
|
);
|
||||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(() => window.__gevAnnotations, {
|
||||||
() =>
|
timeout: 90_000,
|
||||||
window.__gevAnnotations &&
|
});
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
await whenSettled(page);
|
||||||
{ timeout: 90_000 },
|
|
||||||
);
|
|
||||||
const afterReload = await page.evaluate(() =>
|
const afterReload = await page.evaluate(() =>
|
||||||
window.__gevAnnotations.count(),
|
window.__gevAnnotations.count(),
|
||||||
);
|
);
|
||||||
@@ -722,25 +698,28 @@ try {
|
|||||||
// flight left a low, near-horizontal view there) with a boot flight that can
|
// flight left a low, near-horizontal view there) with a boot flight that can
|
||||||
// land after a setView: set the view until it holds.
|
// land after a setView: set the view until it holds.
|
||||||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||||||
await page.evaluate((view) => {
|
await evaluateWithGeo(
|
||||||
const viewer = window.__godsEyeView.viewer;
|
page,
|
||||||
viewer.camera.cancelFlight?.();
|
async (qaGeo, view) => {
|
||||||
viewer.camera.setView({
|
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
window.__godsEyeView.engine.camera.setView({
|
||||||
longitude: (view.lon * Math.PI) / 180,
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
latitude: (view.lat * Math.PI) / 180,
|
longitude: (view.lon * Math.PI) / 180,
|
||||||
height: view.height,
|
latitude: (view.lat * Math.PI) / 180,
|
||||||
}),
|
height: view.height,
|
||||||
orientation: {
|
}),
|
||||||
heading: 0,
|
orientation: {
|
||||||
pitch: (view.pitch * Math.PI) / 180,
|
heading: 0,
|
||||||
roll: 0,
|
pitch: (view.pitch * Math.PI) / 180,
|
||||||
},
|
roll: 0,
|
||||||
});
|
},
|
||||||
}, VIEW);
|
});
|
||||||
await wait(2500);
|
},
|
||||||
|
VIEW,
|
||||||
|
);
|
||||||
|
await whenSettled(page);
|
||||||
const held = await page.evaluate((view) => {
|
const held = await page.evaluate((view) => {
|
||||||
const carto = window.__godsEyeView.viewer.camera.positionCartographic;
|
const carto = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||||
return (
|
return (
|
||||||
Math.abs((carto.latitude * 180) / Math.PI - view.lat) < 0.01 &&
|
Math.abs((carto.latitude * 180) / Math.PI - view.lat) < 0.01 &&
|
||||||
Math.abs((carto.longitude * 180) / Math.PI - view.lon) < 0.01 &&
|
Math.abs((carto.longitude * 180) / Math.PI - view.lon) < 0.01 &&
|
||||||
@@ -767,7 +746,7 @@ try {
|
|||||||
const beforeButtonEnter = await drawState();
|
const beforeButtonEnter = await drawState();
|
||||||
await page.focus('#draw-clear');
|
await page.focus('#draw-clear');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await wait(600);
|
await whenSettled(page);
|
||||||
const afterButtonEnter = await drawState();
|
const afterButtonEnter = await drawState();
|
||||||
check(
|
check(
|
||||||
'Enter on a keyboard-focused button activates the button, it does not finish the shape',
|
'Enter on a keyboard-focused button activates the button, it does not finish the shape',
|
||||||
@@ -790,7 +769,7 @@ try {
|
|||||||
await waitForMarks(1),
|
await waitForMarks(1),
|
||||||
);
|
);
|
||||||
await page.click('#draw-clear');
|
await page.click('#draw-clear');
|
||||||
await wait(400);
|
await whenSettled(page);
|
||||||
const cleared = await drawState();
|
const cleared = await drawState();
|
||||||
check(
|
check(
|
||||||
'Clear removes every mark from the board',
|
'Clear removes every mark from the board',
|
||||||
@@ -815,7 +794,7 @@ try {
|
|||||||
`${degenerateVertices} vertices`,
|
`${degenerateVertices} vertices`,
|
||||||
);
|
);
|
||||||
await pressFinish();
|
await pressFinish();
|
||||||
await wait(600);
|
await whenSettled(page);
|
||||||
const degenerate = await drawState();
|
const degenerate = await drawState();
|
||||||
check(
|
check(
|
||||||
'three points in a line are refused with a reason, not placed',
|
'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 ──────
|
// ── a shape drawn across the antimeridian lands where it was drawn ──────
|
||||||
await page.evaluate(() => {
|
await evaluateWithGeo(page, async (qaGeo) => {
|
||||||
const viewer = window.__godsEyeView.viewer;
|
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||||
viewer.camera.cancelFlight?.();
|
window.__godsEyeView.engine.camera.setView({
|
||||||
viewer.camera.setView({
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
|
||||||
longitude: (179.9995 * Math.PI) / 180,
|
longitude: (179.9995 * Math.PI) / 180,
|
||||||
latitude: 0,
|
latitude: 0,
|
||||||
height: 3000,
|
height: 3000,
|
||||||
}),
|
}),
|
||||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
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(() => {
|
await page.evaluate(() => {
|
||||||
const tool = window.__gevDrawTool;
|
const tool = window.__gevDrawTool;
|
||||||
tool.cancel();
|
tool.cancel();
|
||||||
@@ -865,7 +843,7 @@ try {
|
|||||||
await page.evaluate(() => window.__gevAnnotations.clear());
|
await page.evaluate(() => window.__gevAnnotations.clear());
|
||||||
|
|
||||||
await page.click('#draw-toggle');
|
await page.click('#draw-toggle');
|
||||||
await wait(200);
|
await whenSettled(page);
|
||||||
const finalState = await drawState();
|
const finalState = await drawState();
|
||||||
check(
|
check(
|
||||||
'leaving draw mode gives back the pointer, the handler and the double-click',
|
'leaving draw mode gives back the pointer, the handler and the double-click',
|
||||||
@@ -880,7 +858,7 @@ try {
|
|||||||
() => window.__godsEyeView.viewer.dataSources.length,
|
() => window.__godsEyeView.viewer.dataSources.length,
|
||||||
);
|
);
|
||||||
await page.click('#draw-toggle'); // destroy from an ACTIVE session: the harder case
|
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 destroyed = await page.evaluate(async () => {
|
||||||
const tool = window.__gevDrawTool;
|
const tool = window.__gevDrawTool;
|
||||||
const wasActive = tool.active;
|
const wasActive = tool.active;
|
||||||
@@ -936,7 +914,7 @@ try {
|
|||||||
errors.join(' | '),
|
errors.join(' | '),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await closeBrowser(browser);
|
||||||
}
|
}
|
||||||
console.log(`RESULT: ${failures} failures`);
|
console.log(`RESULT: ${failures} failures`);
|
||||||
process.exitCode = failures ? 1 : 0;
|
process.exitCode = failures ? 1 : 0;
|
||||||
@@ -975,20 +953,23 @@ async function findClickableTarget() {
|
|||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
if (camera) {
|
if (camera) {
|
||||||
await page.evaluate((spot) => {
|
await evaluateWithGeo(
|
||||||
const viewer = window.__godsEyeView.viewer;
|
page,
|
||||||
viewer.camera.cancelFlight?.();
|
async (qaGeo, spot) => {
|
||||||
viewer.camera.setView({
|
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
window.__godsEyeView.engine.camera.setView({
|
||||||
longitude: (spot.lon * Math.PI) / 180,
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
latitude: (spot.lat * Math.PI) / 180,
|
longitude: (spot.lon * Math.PI) / 180,
|
||||||
height: 600,
|
latitude: (spot.lat * Math.PI) / 180,
|
||||||
}),
|
height: 600,
|
||||||
orientation: { heading: 0, pitch: (-80 * Math.PI) / 180, roll: 0 },
|
}),
|
||||||
});
|
orientation: { heading: 0, pitch: (-80 * Math.PI) / 180, roll: 0 },
|
||||||
viewer.scene.requestRender();
|
});
|
||||||
}, camera);
|
window.__godsEyeView.engine.render.request();
|
||||||
await wait(4000);
|
},
|
||||||
|
camera,
|
||||||
|
);
|
||||||
|
await whenSettled(page);
|
||||||
const spot = await onScreen(['cctv']);
|
const spot = await onScreen(['cctv']);
|
||||||
if (spot) return { ...spot, kind: 'cctv', layer: 'CCTV camera' };
|
if (spot) return { ...spot, kind: 'cctv', layer: 'CCTV camera' };
|
||||||
}
|
}
|
||||||
@@ -1021,20 +1002,23 @@ async function findClickableTarget() {
|
|||||||
})
|
})
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
if (!site) return null;
|
if (!site) return null;
|
||||||
await page.evaluate((spot) => {
|
await evaluateWithGeo(
|
||||||
const viewer = window.__godsEyeView.viewer;
|
page,
|
||||||
viewer.camera.cancelFlight?.();
|
async (qaGeo, spot) => {
|
||||||
viewer.camera.setView({
|
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
window.__godsEyeView.engine.camera.setView({
|
||||||
longitude: (spot.lon * Math.PI) / 180,
|
destination: qaGeo.cartographic.toCartesian({
|
||||||
latitude: (spot.lat * Math.PI) / 180,
|
longitude: (spot.lon * Math.PI) / 180,
|
||||||
height: 2500,
|
latitude: (spot.lat * Math.PI) / 180,
|
||||||
}),
|
height: 2500,
|
||||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
}),
|
||||||
});
|
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
||||||
viewer.scene.requestRender();
|
});
|
||||||
}, site);
|
window.__godsEyeView.engine.render.request();
|
||||||
await wait(4000);
|
},
|
||||||
|
site,
|
||||||
|
);
|
||||||
|
await whenSettled(page);
|
||||||
const spot = await onScreen([
|
const spot = await onScreen([
|
||||||
'local-datacenters-tips',
|
'local-datacenters-tips',
|
||||||
'local-datacenters-pins',
|
'local-datacenters-pins',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { execFile } from 'node:child_process';
|
import { execFile } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -52,8 +53,8 @@ export function frameStats(frames) {
|
|||||||
export async function installFrameProbe(page) {
|
export async function installFrameProbe(page) {
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
if (window.__gevFrameProbe) return;
|
if (window.__gevFrameProbe) return;
|
||||||
const { viewer, engine } = window.__godsEyeView;
|
const { engine } = window.__godsEyeView;
|
||||||
const events = viewer?.scene ?? engine.frames;
|
const events = engine.frames;
|
||||||
const probe = { frames: [], started: 0 };
|
const probe = { frames: [], started: 0 };
|
||||||
window.__gevFrameProbe = probe;
|
window.__gevFrameProbe = probe;
|
||||||
let last = performance.now();
|
let last = performance.now();
|
||||||
@@ -102,58 +103,25 @@ export function pageNow(page) {
|
|||||||
* @param {number} seconds Flight duration; 0 jumps.
|
* @param {number} seconds Flight duration; 0 jumps.
|
||||||
*/
|
*/
|
||||||
export async function moveCamera(page, view, seconds) {
|
export async function moveCamera(page, view, seconds) {
|
||||||
if (
|
await evaluateWithGeo(
|
||||||
await page.evaluate(() => window.__godsEyeView.engine?.kind === 'navara')
|
page,
|
||||||
) {
|
async (geo, v, duration) => {
|
||||||
await page.evaluate(
|
const camera = window.__godsEyeView.engine.camera;
|
||||||
async (v, duration) => {
|
camera.cancelFlight();
|
||||||
const geo = await import('/src/geo/index.js');
|
await new Promise((resolve) =>
|
||||||
const camera = window.__godsEyeView.engine.camera;
|
camera.flyTo({
|
||||||
camera.cancelFlight();
|
destination: geo.cartesian3.fromDegrees(v.lon, v.lat, v.height),
|
||||||
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,
|
|
||||||
}),
|
|
||||||
orientation: {
|
orientation: {
|
||||||
heading: ((v.heading ?? 0) * Math.PI) / 180,
|
heading: ((v.heading ?? 0) * Math.PI) / 180,
|
||||||
pitch: ((v.pitch ?? -45) * Math.PI) / 180,
|
pitch: ((v.pitch ?? -45) * Math.PI) / 180,
|
||||||
roll: 0,
|
roll: 0,
|
||||||
},
|
},
|
||||||
duration,
|
duration,
|
||||||
// Straight, eased motion like a drag or wheel zoom, not an arc.
|
|
||||||
maximumHeight: Math.max(v.height, C.positionCartographic.height),
|
|
||||||
complete: resolve,
|
complete: resolve,
|
||||||
cancel: resolve,
|
cancel: resolve,
|
||||||
});
|
}),
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
view,
|
view,
|
||||||
seconds,
|
seconds,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user