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 3 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
373edf2989
commit
53b07ba91f
24 files changed
+628
-875
No files matched your search
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* Real-browser proof for manager-owned periodic work and document visibility.
|
||||
* Uses the app's development-only layer registration seam; no provider keys.
|
||||
* Run: node scripts/qa-layer-visibility.mjs --url http://localhost:4173
|
||||
* Hiding uses a real background tab, never a patched document.hidden getter.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -17,8 +18,7 @@ const url = new URL(
|
||||
'http://localhost:4173',
|
||||
);
|
||||
url.searchParams.set('welcome', '0');
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: chromeExecutablePath(),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
@@ -39,6 +39,7 @@ try {
|
||||
await page.waitForFunction(
|
||||
() => window.__gevQaRegisterLayer && window.__godsEyeView?.dataManager,
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const manager = window.__godsEyeView.dataManager;
|
||||
window.__qaVisibilityUpdates = 0;
|
||||
@@ -120,5 +121,5 @@ try {
|
||||
);
|
||||
console.log('RESULT: 7 passed');
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
/** Location UI acceptance; controlled search results isolate UI races from geocoder availability. */
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -26,11 +25,10 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.styleManager?._locationControls &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__godsEyeView?.styleManager?._locationControls,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.click('#location-bar-toggle');
|
||||
await page.click('[data-pin-target="location-bar"]');
|
||||
const cityId = await page.$eval(
|
||||
@@ -114,10 +112,7 @@ try {
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => !window.__godsEyeView.viewer.camera._currentFlight,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
check(
|
||||
'native geo URI search shows the decimal coordinate readout',
|
||||
await page.evaluate(() => {
|
||||
@@ -135,7 +130,7 @@ try {
|
||||
check(
|
||||
'geo URI navigation settles near the requested coordinate',
|
||||
await page.evaluate(() => {
|
||||
const position = window.__godsEyeView.viewer.camera.positionCartographic;
|
||||
const position = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
const degrees = 180 / Math.PI;
|
||||
return (
|
||||
Math.abs(position.latitude * degrees - 39.7392) < 0.05 &&
|
||||
@@ -252,36 +247,17 @@ try {
|
||||
document.getElementById('poi-row').classList.contains('expanded'),
|
||||
);
|
||||
// Finish the camera's existing city flight before inspecting both layouts.
|
||||
await page.waitForFunction(
|
||||
() => !window.__godsEyeView.viewer.camera._currentFlight,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
),
|
||||
);
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
for (let index = 0; index < scene.primitives.length; index++) {
|
||||
const primitive = scene.primitives.get(index);
|
||||
if (
|
||||
typeof primitive.tilesLoaded === 'boolean' &&
|
||||
!primitive.tilesLoaded
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const settled = await whenTilesLoaded(page, { timeoutMs: 60000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
check('visible city tiles settle before screenshots', settled);
|
||||
fs.mkdirSync('qa-shots/location-controls', { recursive: true });
|
||||
await page.screenshot({ path: 'qa-shots/location-controls/desktop.png' });
|
||||
@@ -301,7 +277,8 @@ try {
|
||||
if (!pending) return false;
|
||||
await pending;
|
||||
return (
|
||||
!ui.orbitController.active && !window.__godsEyeView.viewer.trackedEntity
|
||||
!ui.orbitController.active &&
|
||||
!window.__godsEyeView.engine.camera.following()
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -329,7 +306,7 @@ try {
|
||||
);
|
||||
check('no uncaught browser errors', errors.length === 0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Browser acceptance for Map Source control lifecycle and truthful selection. */
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -25,17 +25,7 @@ try {
|
||||
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
|
||||
url.searchParams.set('welcome', '0');
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return (
|
||||
window.__godsEyeView?.styleManager?._mapSourceControls &&
|
||||
cover?.classList.contains('hidden') &&
|
||||
Number(getComputedStyle(cover).opacity) === 0
|
||||
);
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const controller = manager.mapStackController;
|
||||
@@ -132,19 +122,17 @@ try {
|
||||
});
|
||||
document.querySelector('.map-stack-chip.active')?.focus();
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.scene.tweens?.removeAll?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (-97.7431 * Math.PI) / 180,
|
||||
latitude: (30.2568 * Math.PI) / 180,
|
||||
height: 1200,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -0.85, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await page.evaluate(
|
||||
() =>
|
||||
@@ -152,26 +140,10 @@ try {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
),
|
||||
);
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let i = 0; i < primitives.length; i++) {
|
||||
const primitive = primitives.get(i);
|
||||
if (
|
||||
typeof primitive.tilesLoaded === 'boolean' &&
|
||||
!primitive.tilesLoaded
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const settled = await whenTilesLoaded(page, { timeoutMs: 60_000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
check('visible tile content settles before visual captures', settled);
|
||||
fs.mkdirSync('qa-shots/map-source-controls', { recursive: true });
|
||||
await page.screenshot({ path: 'qa-shots/map-source-controls/desktop.png' });
|
||||
@@ -206,7 +178,7 @@ try {
|
||||
check('no uncaught browser errors', errors.length === 0);
|
||||
if (errors.length) console.log(JSON.stringify(errors));
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/** Focused browser proof for the responsive, accessible Map Source tray. */
|
||||
import { whenSettled, whenUiSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const shotsDir = process.env.QA_SHOTS_DIR || path.join(repoRoot, 'qa-shots', 'map-source-tray');
|
||||
@@ -18,26 +19,12 @@ const headful = process.argv.includes('--headful');
|
||||
// branch.
|
||||
const forceKeyless = process.argv.includes('--keyless')
|
||||
|| process.env.QA_MAP_SOURCE_TRAY_KEYLESS === '1';
|
||||
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH
|
||||
|| await puppeteer.executablePath().catch(() => null);
|
||||
|
||||
if (!executablePath || !fs.existsSync(executablePath)) {
|
||||
throw new Error('Puppeteer Chrome for Testing is unavailable');
|
||||
}
|
||||
fs.mkdirSync(shotsDir, { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: headful ? false : 'new',
|
||||
executablePath,
|
||||
// Metal is a macOS-only ANGLE backend: passing it on Windows or Linux makes
|
||||
// WebGL initialization fail outright, so Cesium never constructs and this
|
||||
// harness dies at the boot wait before a single assertion runs.
|
||||
args: [
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
'--no-sandbox',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [...(process.platform === 'darwin' ? ['--enable-gpu'] : []), '--no-sandbox'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const failures = [];
|
||||
@@ -154,7 +141,7 @@ const pressTabs = async (count, backwards = false) => {
|
||||
if (backwards) await page.keyboard.up('Shift');
|
||||
}
|
||||
// The row scrolls smoothly when native Tab brings an edge button into view.
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
};
|
||||
// Phones (≤720px) present the dock's Presets inline in the More tab's sheet.
|
||||
// It opens from the keyboard, as a keyboard user would, so the focus it moves
|
||||
@@ -218,7 +205,7 @@ const tabTo = async (selector, { backwards = false, limit = 160 } = {}) => {
|
||||
for (let step = 1; step <= limit; step += 1) {
|
||||
await page.keyboard.press('Tab');
|
||||
if (await page.evaluate((match) => document.activeElement?.matches(match), selector)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
return { reached: true, steps: step };
|
||||
}
|
||||
}
|
||||
@@ -304,10 +291,7 @@ try {
|
||||
// tray assertion into a mission or voice action in a pristine browser.
|
||||
await page.goto(`${appUrl}/?welcome=0`, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
|
||||
const presentation = await page.evaluate(() => ({
|
||||
ids: [...document.querySelectorAll('.map-stack-chip')].map((chip) => chip.dataset.stackId),
|
||||
@@ -1103,9 +1087,9 @@ try {
|
||||
const clickTileThenLeave = async (stackId) => {
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager
|
||||
.setPanelCollapsed('control-panel', false, { explicit: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 240));
|
||||
await whenUiSettled(page);
|
||||
await page.click(`[data-stack-id="${stackId}"]`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
await whenUiSettled(page);
|
||||
const afterClick = await readTrayState();
|
||||
await page.mouse.move(20, 20); // leave the dock entirely → pointerleave
|
||||
// Past the 420 ms unpinned close delay with room for a slow frame.
|
||||
@@ -1121,7 +1105,7 @@ try {
|
||||
await setControlPanelPinned(false);
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager
|
||||
.setPanelCollapsed('control-panel', true, { explicit: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await whenUiSettled(page);
|
||||
await page.focus('#control-panel-toggle');
|
||||
await page.keyboard.press('Enter'); // opens and hands focus to the active tile
|
||||
const selectedForHold = await page.evaluate(() => window.__godsEyeView.styleManager.mapStackController.getActiveId());
|
||||
@@ -1136,7 +1120,7 @@ try {
|
||||
await page.keyboard.press('Tab');
|
||||
if (activeIsLastTile) await page.keyboard.up('Shift');
|
||||
await page.keyboard.press('Enter'); // activate it from the keyboard
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await whenUiSettled(page);
|
||||
const keyboardAfterActivate = await page.evaluate(() => ({
|
||||
focusedStack: document.activeElement?.dataset?.stackId || null,
|
||||
isChip: !!document.activeElement?.classList?.contains('map-stack-chip'),
|
||||
@@ -1148,7 +1132,7 @@ try {
|
||||
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
||||
});
|
||||
await page.mouse.move(chipPoint.x, chipPoint.y);
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
await whenUiSettled(page);
|
||||
await page.mouse.move(20, 20);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const keyboardHold = await readTrayState();
|
||||
@@ -1179,7 +1163,7 @@ try {
|
||||
await page.mouse.move(pinApproach.x, pinApproach.y);
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager
|
||||
.setPanelCollapsed('control-panel', false, { explicit: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 240));
|
||||
await whenUiSettled(page);
|
||||
check(
|
||||
'a tile click does not exempt the unpinned tray from mouse-away auto-dismiss',
|
||||
dismissAfterTileClick.afterClick.collapsed === false
|
||||
@@ -1205,7 +1189,7 @@ try {
|
||||
await checkResponsiveStyleFocus(1000);
|
||||
|
||||
await page.setViewport({ width: 620, height: 900, deviceScaleFactor: 1 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 180));
|
||||
await whenUiSettled(page);
|
||||
await waitForShellSettled();
|
||||
await openPhoneMoreSheet();
|
||||
const at620 = await trayMetrics();
|
||||
@@ -1222,7 +1206,7 @@ try {
|
||||
await checkResponsiveStyleFocus(620);
|
||||
|
||||
await page.setViewport({ width: 480, height: 900, deviceScaleFactor: 1 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 180));
|
||||
await whenUiSettled(page);
|
||||
const at480 = await trayMetrics();
|
||||
const allRectsInside = at480.chips.every((chip) => (
|
||||
chip.left >= 0 && chip.right <= at480.viewport.width
|
||||
@@ -1294,10 +1278,7 @@ try {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
}
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView.styleManager.mapStackController.getState()?.status !== 'switching',
|
||||
@@ -1429,7 +1410,7 @@ try {
|
||||
await page.keyboard.press('Enter');
|
||||
await page.focus('#control-panel-toggle');
|
||||
await page.keyboard.press('Enter');
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await whenUiSettled(page);
|
||||
const pinnedSibling = await locationState();
|
||||
check('Location: pinned tray survives sibling opening',
|
||||
pinnedSibling.pinned && pinnedSibling.locationExpanded === 'true'
|
||||
@@ -1449,14 +1430,14 @@ try {
|
||||
const searchTab = '#mobile-tab-bar [data-mobile-sheet="search"]';
|
||||
await page.focus(searchTab);
|
||||
await page.keyboard.press('Enter');
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
const field = await focusMetrics('#location-search');
|
||||
check(`Search: ${width} px keyboard opens the sheet on the location field`,
|
||||
field.focusedId === 'location-search' && field.visible && field.insideViewport,
|
||||
JSON.stringify(field));
|
||||
await page.screenshot({ path: path.join(shotsDir, `${width}-search-open.png`) });
|
||||
await page.keyboard.press('Escape');
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await whenUiSettled(page);
|
||||
const tab = await focusMetrics(searchTab);
|
||||
const closedSheet = await page.evaluate(() => !document.body.dataset.mobileSheet);
|
||||
check(`Search: ${width} px Escape closes the sheet with a visible ring on its tab`,
|
||||
@@ -1747,7 +1728,7 @@ try {
|
||||
|
||||
check('no new page or console errors', consoleErrors.length === 0, consoleErrors.join(' | '));
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Named military areas: continent performance, close identities and a recorded zoom. */
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import puppeteer from 'puppeteer';
|
||||
import {
|
||||
checkInstallationPan,
|
||||
checkInstallationSelection,
|
||||
@@ -34,14 +36,10 @@ const result = {
|
||||
video: null,
|
||||
};
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 180000,
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
args: ['--disable-background-timer-throttling', '--disable-renderer-backgrounding'],
|
||||
});
|
||||
let recorder;
|
||||
try {
|
||||
@@ -69,10 +67,10 @@ try {
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
window.__godsEyeView?.dataManager,
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -92,11 +90,10 @@ try {
|
||||
for (const [id] of g.dataManager.layers)
|
||||
await g.dataManager.setEnabled(id, false);
|
||||
g.styleManager.setDetection({ enabled: false });
|
||||
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
assert.equal(result.packs.length, 0, 'names pack stays unloaded at startup');
|
||||
result.renderer = await page.evaluate(() => {
|
||||
const gl = window.__godsEyeView.viewer.scene.context._gl;
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return ext
|
||||
? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
|
||||
@@ -111,7 +108,7 @@ try {
|
||||
.module.getStats(),
|
||||
);
|
||||
const settle = async () => {
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const s = window.__godsEyeView.dataManager.layers
|
||||
@@ -121,7 +118,7 @@ try {
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await sleep(1200);
|
||||
await whenFramesDrawn(page);
|
||||
};
|
||||
const us = { lon: -100, lat: 39, height: 6500000, pitch: -90 };
|
||||
await moveCamera(page, us, 0);
|
||||
@@ -213,7 +210,7 @@ try {
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.setCleanView(true),
|
||||
);
|
||||
await sleep(300);
|
||||
await whenFramesDrawn(page);
|
||||
result.cleanUi = await page.evaluate(async () => {
|
||||
const host = (await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
|
||||
const root = document.getElementById('world-overlay-root');
|
||||
@@ -278,7 +275,7 @@ try {
|
||||
const screen = new C.Cartesian2();
|
||||
window.__installationFrames = [];
|
||||
window.__stopInstallationFrames =
|
||||
g.viewer.scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
const stats = layer.getStats();
|
||||
const markers = [],
|
||||
labels = [],
|
||||
@@ -286,11 +283,7 @@ try {
|
||||
layer.visitNamedMarkers((id, point) => {
|
||||
const rect = getPaintRect('military-installations', id);
|
||||
if (rect) rects.push(rect);
|
||||
const p = C.SceneTransforms.worldToWindowCoordinates(
|
||||
g.viewer.scene,
|
||||
point.position,
|
||||
screen,
|
||||
);
|
||||
const p = window.__godsEyeView.engine.projector.worldToWindow(point.position, screen);
|
||||
if (
|
||||
point.show &&
|
||||
p &&
|
||||
@@ -316,7 +309,7 @@ try {
|
||||
)
|
||||
overlaps++;
|
||||
}
|
||||
const altitude = g.viewer.camera.positionCartographic.height;
|
||||
const altitude = window.__godsEyeView.engine.camera.pose().cartographic.height;
|
||||
window.__installationFrames.push({
|
||||
t: performance.now(),
|
||||
altitude,
|
||||
@@ -331,7 +324,7 @@ try {
|
||||
});
|
||||
});
|
||||
const clipStart = await pageNow(page);
|
||||
await sleep(1200);
|
||||
await whenFramesDrawn(page);
|
||||
for (const view of [
|
||||
{ lon: -116, lat: 35, height: 1500000, pitch: -85 },
|
||||
{ lon: -117.2, lat: 33, height: 200000, pitch: -80 },
|
||||
@@ -381,11 +374,11 @@ try {
|
||||
for (const [key, expected, lon, lat, height] of sites) {
|
||||
await moveCamera(page, { lon, lat, height, pitch: -80 }, 1.5);
|
||||
await settle();
|
||||
const snapshot = await page.evaluate(
|
||||
(lat, lon) => {
|
||||
const snapshot = await evaluateWithGeo(page,
|
||||
async (qaGeo, lat, lon) => {
|
||||
const g = window.__godsEyeView;
|
||||
const layer = g.dataManager.layers.get('military-installations').module;
|
||||
const position = g.viewer.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
const position = qaGeo.cartographic.toCartesian(
|
||||
{
|
||||
longitude: (lon * Math.PI) / 180,
|
||||
latitude: (lat * Math.PI) / 180,
|
||||
@@ -466,7 +459,7 @@ try {
|
||||
);
|
||||
assert.match(result.cardText, /Fort Hood/);
|
||||
assert.match(result.cardText, /TA-25/);
|
||||
await sleep(1800);
|
||||
await whenFramesDrawn(page);
|
||||
await page.$eval(
|
||||
'#military-awareness-panel .military-awareness-names',
|
||||
(el) => {
|
||||
@@ -481,5 +474,5 @@ try {
|
||||
if (recorder)
|
||||
result.video = await recorder.stop().catch((e) => ({ error: e.message }));
|
||||
await fs.writeFile(`${dir}/result.json`, JSON.stringify(result, null, 2));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
// Composed-app media policy/lifecycle checks. Pinokio UA is a policy fixture,
|
||||
// not native-host acceptance; the YouTube SDK clock below is deterministic.
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const output = path.resolve(
|
||||
process.env.QA_OUTPUT_DIR || 'qa-shots/nepal-media',
|
||||
);
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: process.env.QA_HEADFUL !== '1',
|
||||
args: process.env.QA_HEADFUL === '1' ? ['--use-angle=metal'] : [],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: process.env.QA_HEADFUL === '1' ? [] : [],
|
||||
});
|
||||
let assertions = 0;
|
||||
const check = (name, value) => {
|
||||
@@ -84,12 +85,10 @@ try {
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector &&
|
||||
document.querySelector('#loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
const result = await page.evaluate(async (pinokio) => {
|
||||
const d = window.__godsEyeView.sceneDirector;
|
||||
const scene = d._project.scenes.find((s) => /Nepal/.test(s.title));
|
||||
@@ -200,5 +199,5 @@ try {
|
||||
}
|
||||
console.log(`PASS: ${assertions} assertions`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -17,10 +17,11 @@
|
||||
* projection must fit one frame; Traffic does no outline projection and the
|
||||
* warm ask reuses it. Traffic activation Long Tasks are reported separately.
|
||||
*/
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const valueAfter = (flag, fallback) => {
|
||||
@@ -155,19 +156,13 @@ function workEvidence(entries, label) {
|
||||
|
||||
await fs.mkdir(path.dirname(OUTPUT), { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: args.includes('--headful') ? false : 'new',
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 240_000,
|
||||
...(process.env.PUPPETEER_EXECUTABLE_PATH
|
||||
? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH }
|
||||
: {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-gl=angle',
|
||||
...(args.includes('--headful')
|
||||
? []
|
||||
: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader']),
|
||||
...(args.includes('--headful') ? [] : ['--enable-unsafe-swiftshader']),
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -251,15 +246,15 @@ async function settleOutline(page, label) {
|
||||
}
|
||||
if (!annotation || annotation.pendingOutline)
|
||||
throw new Error(`${label}: outline did not settle`);
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
await new Promise((resolve) => {
|
||||
const timer = setTimeout(resolve, 1500);
|
||||
const remove = scene.postRender.addEventListener(() => {
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
resolve();
|
||||
});
|
||||
scene.requestRender();
|
||||
const remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
resolve();
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await new Promise((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
@@ -414,13 +409,13 @@ try {
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.viewer &&
|
||||
window.__godsEyeView?.engine &&
|
||||
window.__godsEyeView?.dataManager &&
|
||||
window.__gevVoiceCommands?.runner &&
|
||||
window.__gevAnnotations &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
window.__gevAnnotations,
|
||||
{ timeout: 120_000, polling: 250 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -428,10 +423,10 @@ try {
|
||||
window.__godsEyeView.styleManager.setDetection({ enabled: false }),
|
||||
);
|
||||
await page.evaluate(async (target) => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
await dataManager.setEnabled('traffic', false);
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: window.__CESIUM__.Cartesian3.fromDegrees(
|
||||
target.longitude,
|
||||
target.latitude,
|
||||
@@ -443,11 +438,11 @@ try {
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
window.__gevAnnotations.clear();
|
||||
performance.clearMeasures();
|
||||
}, TARGET);
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
|
||||
if (run === 0) {
|
||||
phase = 'overlap';
|
||||
@@ -469,7 +464,7 @@ try {
|
||||
await sleep(100);
|
||||
await heldTargetRequest.continue();
|
||||
const overlap = await settleOutline(page, 'QA Capitol overlap');
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
const trafficStats = await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.layers
|
||||
.get('traffic')
|
||||
@@ -538,7 +533,7 @@ try {
|
||||
},
|
||||
{ timeout: 180_000, polling: 250 },
|
||||
);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
const trafficMeasures = relevantMeasures(
|
||||
await page.evaluate(() =>
|
||||
performance.getEntriesByType('measure').map((entry) => ({
|
||||
@@ -582,7 +577,7 @@ try {
|
||||
|
||||
phase = 'outline-cold';
|
||||
const cold = await driveOutline(page, `QA Capitol cold ${run}`);
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
cold.measures = relevantMeasures(cold.measures);
|
||||
cold.work = workEvidence(cold.measures, 'cold');
|
||||
cold.network = network.filter(
|
||||
@@ -596,7 +591,7 @@ try {
|
||||
await page.evaluate(() => window.__gevAnnotations.clear());
|
||||
phase = 'outline-warm';
|
||||
const warm = await driveOutline(page, `QA Capitol warm ${run}`);
|
||||
await sleep(750);
|
||||
await whenSettled(page);
|
||||
warm.measures = relevantMeasures(warm.measures);
|
||||
warm.work = workEvidence(warm.measures, 'warm');
|
||||
warm.network = network.filter(
|
||||
@@ -674,7 +669,7 @@ try {
|
||||
assert.equal(warmDecodes.length, 0, 'decoded-cache hit performs no decode');
|
||||
|
||||
report.renderer ||= await page.evaluate(() => {
|
||||
const gl = window.__godsEyeView.viewer.scene.context._gl;
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
const debug = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return debug
|
||||
? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)
|
||||
@@ -849,5 +844,5 @@ try {
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await fs.writeFile(OUTPUT, `${JSON.stringify(report, null, 2)}\n`);
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
// Cesium-only probes: native entity, DataSource and primitive counts independently audit Cesium ownership.
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* Phase 0 world-overlay baseline harness.
|
||||
*
|
||||
@@ -25,13 +28,13 @@
|
||||
* and primitives (none of them a layer's own any more).
|
||||
*/
|
||||
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
|
||||
const CHROME_EXECUTABLE = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const CHROME_EXECUTABLE = chromeExecutablePath();
|
||||
const DEFAULT_URL = 'http://localhost:4176';
|
||||
const VIEWPORT = Object.freeze({ width: 1440, height: 900 });
|
||||
const SAMPLE_MS = 5_000;
|
||||
@@ -366,20 +369,19 @@ async function waitForApp(page) {
|
||||
() => Boolean(window.__godsEyeView?.engine && window.__godsEyeView?.dataManager),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
const { engine, viewer } = window.__godsEyeView;
|
||||
const { engine } = window.__godsEyeView;
|
||||
try { engine.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
try { viewer?.scene.tweens?.removeAll?.(); } catch { /* no tween collection */ }
|
||||
});
|
||||
await sleep(1_250);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
|
||||
async function setCamera(page, camera) {
|
||||
if (!camera) return;
|
||||
await page.evaluate(([destination, heading, pitch]) => {
|
||||
const { engine, viewer } = window.__godsEyeView;
|
||||
const { engine } = window.__godsEyeView;
|
||||
engine.camera.cancelFlight();
|
||||
viewer?.scene.tweens?.removeAll?.();
|
||||
engine.camera.setView({
|
||||
destination,
|
||||
orientation: { heading, pitch, roll: 0 },
|
||||
@@ -587,7 +589,7 @@ async function prepareCctv(page, heightM) {
|
||||
}, heightM);
|
||||
if (!camera) return 'CCTV catalog did not expose a geolocated camera';
|
||||
await setCamera(page, camera);
|
||||
await sleep(7_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -600,14 +602,14 @@ async function prepareFirms(page) {
|
||||
});
|
||||
if (!result.fire) return `FIRMS unavailable (${result.stats.error || 'no detections'})`;
|
||||
await setCamera(page, [result.fire.longitude, result.fire.latitude, 60_000, 0, -1.45]);
|
||||
await sleep(4_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function prepareVessels(page) {
|
||||
const state = await readLayerState(page, 'ais-live-vessels');
|
||||
if (!(state?.stats?.count > 0)) return `AIS unavailable (${state?.stats?.error || 'no live vessels'})`;
|
||||
await sleep(2_500);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -621,7 +623,7 @@ async function prepareDetection(page, densityPct) {
|
||||
densityPct: density,
|
||||
}), densityPct);
|
||||
if (!control?.ok) return `Detection control failed (${control?.error || 'unknown error'})`;
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -650,7 +652,7 @@ async function prepareTrackedFlight(page, cockpit) {
|
||||
} catch {
|
||||
return 'Civil tracking did not create a tracked entity';
|
||||
}
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
if (!cockpit) return null;
|
||||
const entryAvailable = await page.evaluate(() => {
|
||||
const button = document.getElementById('cockpit-entry');
|
||||
@@ -664,7 +666,7 @@ async function prepareTrackedFlight(page, cockpit) {
|
||||
} catch {
|
||||
return 'Cockpit entry control did not enter cockpit mode';
|
||||
}
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -683,7 +685,7 @@ async function prepareMission(page) {
|
||||
} catch {
|
||||
return 'Mission selection did not reach the layer probe';
|
||||
}
|
||||
await sleep(2_000);
|
||||
await whenFramesDrawn(page);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -703,7 +705,7 @@ async function prepareScene(page, scene) {
|
||||
if (!skipReason && scene.detectionDensity) skipReason = await prepareDetection(page, scene.detectionDensity);
|
||||
if (!skipReason && scene.trackedFlight) skipReason = await prepareTrackedFlight(page, scene.cockpit);
|
||||
if (!skipReason && scene.missionSelected) skipReason = await prepareMission(page);
|
||||
await sleep(DEFAULT_SETTLE_MS);
|
||||
await whenFramesDrawn(page);
|
||||
return { layerActivations, skipReason };
|
||||
}
|
||||
|
||||
@@ -868,7 +870,7 @@ async function runScene(browser, scene, shotsDir) {
|
||||
const motion = await samplePhase(page, true);
|
||||
const afterMotionShot = await captureShot(page, shotsDir, scene.id, 'after-motion');
|
||||
if (afterMotionShot) result.screenshots.push(afterMotionShot);
|
||||
await sleep(BETWEEN_PHASE_SETTLE_MS);
|
||||
await whenFramesDrawn(page);
|
||||
const rest = await samplePhase(page, false);
|
||||
result.samples = { motion, rest };
|
||||
result.entityInventoryAfter = await readEntityInventory(page);
|
||||
@@ -904,9 +906,6 @@ function unavailableRenderer(renderer) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(CHROME_EXECUTABLE)) {
|
||||
throw new Error(`Required Chrome executable not found: ${CHROME_EXECUTABLE}`);
|
||||
}
|
||||
if (!Number.isFinite(DPR) || DPR <= 0) throw new Error(`Invalid --dpr value: ${DPR}`);
|
||||
const scenes = selectedScenes();
|
||||
if (DIST_DIR && (!fs.existsSync(DIST_DIR) || !fs.statSync(DIST_DIR).isDirectory())) {
|
||||
@@ -917,18 +916,9 @@ async function main() {
|
||||
if (!response.ok) throw new Error(`Dev server unavailable at ${APP_URL}: ${response.status || response.statusText}`);
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROME_EXECUTABLE,
|
||||
headless: HEADFUL ? false : 'new',
|
||||
args: [
|
||||
...(process.env.QA_ANGLE ? ['--use-gl=angle', `--use-angle=${process.env.QA_ANGLE}`] : HARDWARE_GPU ? [] : ['--enable-unsafe-swiftshader', '--use-gl=swiftshader']),
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
`--window-size=${VIEWPORT.width},${VIEWPORT.height}`,
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [...(process.env.QA_ANGLE ? [] : HARDWARE_GPU ? [] : ['--enable-unsafe-swiftshader']), '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', `--window-size=${VIEWPORT.width},${VIEWPORT.height}`],
|
||||
});
|
||||
|
||||
const run = { generatedAt: new Date().toISOString(), context: null, scenes: [] };
|
||||
@@ -974,13 +964,9 @@ async function main() {
|
||||
webglRenderer: browserContext.webglRenderer,
|
||||
},
|
||||
viewport: { ...VIEWPORT, dpr: browserContext.dpr },
|
||||
gpuMode: process.env.QA_ANGLE || (HARDWARE_GPU ? 'hardware' : 'swiftshader'),
|
||||
gpuMode: process.env.QA_ANGLE || 'metal',
|
||||
chromeExecutable: CHROME_EXECUTABLE,
|
||||
chromeArgs: process.env.QA_ANGLE
|
||||
? ['--use-gl=angle', `--use-angle=${process.env.QA_ANGLE}`, '--no-sandbox']
|
||||
: HARDWARE_GPU
|
||||
? ['--no-sandbox']
|
||||
: ['--enable-unsafe-swiftshader', '--use-gl=swiftshader', '--no-sandbox'],
|
||||
chromeArgs: browser.process()?.spawnargs.slice(1),
|
||||
sampleMs: SAMPLE_MS,
|
||||
captureMode: DIST_DIR ? `static-dist:${path.resolve(DIST_DIR)}` : 'dev-server',
|
||||
};
|
||||
@@ -992,7 +978,7 @@ async function main() {
|
||||
printScene(result);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
run.summary = {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native primitives and globe instrumentation isolate Cesium paint and offload cost.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* qa-overpass-offload.mjs — keyless/keyed source replacement acceptance gate.
|
||||
*
|
||||
@@ -25,10 +27,11 @@
|
||||
* Screenshots and a JSON result go to qa-shots/ (gitignored). Exits nonzero on
|
||||
* a failed assertion. Works against keyed or keyless dev servers.
|
||||
*/
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { tilesForBounds } from '../src/data/tomtomTiles.js';
|
||||
import { trafficDetailBounds } from '../src/layers/traffic/source.js';
|
||||
|
||||
@@ -55,19 +58,13 @@ const profile = args.includes('--profile');
|
||||
const coverage = args.includes('--coverage');
|
||||
const shots = path.resolve('qa-shots');
|
||||
await fs.mkdir(shots, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !args.includes('--headful'),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300_000,
|
||||
...(process.env.PUPPETEER_EXECUTABLE_PATH
|
||||
? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH }
|
||||
: {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-gl=angle',
|
||||
...(args.includes('--software-gl')
|
||||
? ['--use-angle=swiftshader', '--enable-unsafe-swiftshader']
|
||||
: []),
|
||||
...(args.includes('--software-gl') ? ['--enable-unsafe-swiftshader'] : []),
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -346,14 +343,10 @@ try {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60_000, polling: 500 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000, polling: 250 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
// Startup restoration owns its camera flight until the loading cover is gone.
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
@@ -362,7 +355,7 @@ try {
|
||||
window.__godsEyeView.styleManager.setDetection({ enabled: false }),
|
||||
);
|
||||
result.renderer = await page.evaluate(() => {
|
||||
const gl = window.__godsEyeView.viewer.scene.context._gl;
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
const debug = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return debug
|
||||
? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)
|
||||
@@ -374,13 +367,13 @@ try {
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
async function fly(lat, lon, height = 2000, heading = 0, pitch = -75) {
|
||||
await page.evaluate(
|
||||
async (view) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, view) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
await new Promise((resolve) =>
|
||||
viewer.camera.flyTo({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
height: view.height,
|
||||
@@ -400,22 +393,7 @@ try {
|
||||
);
|
||||
}
|
||||
async function settleTiles() {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
if (scene.globe.show) return scene.globe.tilesLoaded;
|
||||
let found = false;
|
||||
for (let i = 0; i < scene.primitives.length; i++) {
|
||||
const primitive = scene.primitives.get(i);
|
||||
if (!primitive.show || typeof primitive.tilesLoaded !== 'boolean')
|
||||
continue;
|
||||
found = true;
|
||||
if (!primitive.tilesLoaded) return false;
|
||||
}
|
||||
return found;
|
||||
},
|
||||
{ timeout: 90_000, polling: 500 },
|
||||
);
|
||||
await whenTilesLoaded(page, { timeoutMs: 90_000 });
|
||||
}
|
||||
async function shot(name) {
|
||||
await settleTiles();
|
||||
@@ -440,30 +418,35 @@ try {
|
||||
const milliseconds = await page.evaluate(
|
||||
(allowEmpty) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const start = performance.now();
|
||||
const cleanup = () => {
|
||||
remove();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
const remove = viewer.scene.postRender.addEventListener(() => {
|
||||
const stats = dataManager.layers.get('traffic').module.getStats();
|
||||
if (stats.count <= 0) {
|
||||
if (
|
||||
allowEmpty &&
|
||||
!stats.loading &&
|
||||
stats.lastUpdate &&
|
||||
performance.now() - start > 500
|
||||
) {
|
||||
const remove =
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(
|
||||
() => {
|
||||
const stats = dataManager.layers
|
||||
.get('traffic')
|
||||
.module.getStats();
|
||||
if (stats.count <= 0) {
|
||||
if (
|
||||
allowEmpty &&
|
||||
!stats.loading &&
|
||||
stats.lastUpdate &&
|
||||
performance.now() - start > 500
|
||||
) {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
cleanup();
|
||||
resolve(elapsed);
|
||||
});
|
||||
resolve(elapsed);
|
||||
},
|
||||
);
|
||||
const timer = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error('No traffic dots within 180 seconds'));
|
||||
@@ -551,23 +534,24 @@ try {
|
||||
.map((p) => C.Cartesian3.clone(p.position)),
|
||||
);
|
||||
for (const position of positions) {
|
||||
const screen = C.SceneTransforms.worldToWindowCoordinates(
|
||||
scene,
|
||||
position,
|
||||
);
|
||||
const screen =
|
||||
window.__godsEyeView.engine.projector.worldToWindow(position);
|
||||
if (
|
||||
!screen ||
|
||||
screen.x < 0 ||
|
||||
screen.x >= scene.canvas.clientWidth ||
|
||||
screen.x >= window.__godsEyeView.engine.host.canvas.clientWidth ||
|
||||
screen.y < 0 ||
|
||||
screen.y >= scene.canvas.clientHeight
|
||||
screen.y >= window.__godsEyeView.engine.host.canvas.clientHeight
|
||||
)
|
||||
continue;
|
||||
inView++;
|
||||
const carto = C.Cartographic.fromCartesian(position);
|
||||
const height = scene.globe.show
|
||||
? scene.globe.getHeight(carto)
|
||||
: scene.sampleHeight(carto, collections);
|
||||
? window.__godsEyeView.engine.surface.terrainHeight(carto)
|
||||
: window.__godsEyeView.engine.surface.sampleHeight(
|
||||
carto,
|
||||
collections,
|
||||
);
|
||||
if (Number.isFinite(height) && Math.abs(height) <= 9000) {
|
||||
sampled++;
|
||||
const delta = carto.height - height;
|
||||
@@ -645,13 +629,13 @@ try {
|
||||
);
|
||||
view.layers = await waitForSources();
|
||||
view.actualCamera = await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const position = camera.positionCartographic;
|
||||
const position = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return {
|
||||
lat: (position.latitude * 180) / Math.PI,
|
||||
lon: (position.longitude * 180) / Math.PI,
|
||||
height: position.height,
|
||||
pitch: (camera.pitch * 180) / Math.PI,
|
||||
pitch:
|
||||
(window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
|
||||
};
|
||||
});
|
||||
assert.ok(
|
||||
@@ -736,14 +720,16 @@ try {
|
||||
surface.stats.mode === 'live' ? mode : 'osm',
|
||||
);
|
||||
const actualCamera = await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
const p = camera.positionCartographic;
|
||||
const p = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return {
|
||||
lat: (p.latitude * 180) / Math.PI,
|
||||
lon: (p.longitude * 180) / Math.PI,
|
||||
height: p.height,
|
||||
pitch: (camera.pitch * 180) / Math.PI,
|
||||
heading: (camera.heading * 180) / Math.PI,
|
||||
pitch:
|
||||
(window.__godsEyeView.engine.camera.pose().pitch * 180) / Math.PI,
|
||||
heading:
|
||||
(window.__godsEyeView.engine.camera.pose().heading * 180) /
|
||||
Math.PI,
|
||||
};
|
||||
});
|
||||
for (const [key, expected, tolerance] of [
|
||||
@@ -868,9 +854,7 @@ try {
|
||||
}
|
||||
} else if (pan) {
|
||||
const sessionBudget = 32; // <= 0.54% of the default 6,000/day upstream budget.
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
await page.evaluate(() => {});
|
||||
beginView('pan-start');
|
||||
await fly(30.2672, -97.7431, 450, 0, -35);
|
||||
await enableTrafficTimed('pan-start');
|
||||
@@ -992,9 +976,7 @@ try {
|
||||
// A continent jump can temporarily use the previous city's terrain height
|
||||
// for collision avoidance (São Paulo -> Austin raises a 450 m target).
|
||||
// Pin the prescribed camera fixtures so the revisit covers identical tiles.
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
await page.evaluate(() => {});
|
||||
for (const city of [
|
||||
['austin', 30.2672, -97.7431],
|
||||
['london', 51.5074, -0.1278],
|
||||
@@ -1043,7 +1025,7 @@ try {
|
||||
{ timeout: 120_000, polling: 500 },
|
||||
);
|
||||
const austin = await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const traffic = dataManager.layers.get('traffic').module.getStats();
|
||||
const alpr = dataManager.layers.get('alpr-cameras').module.getStats();
|
||||
// The camera badges: the layer's sprite mover sets (normal and
|
||||
@@ -1059,7 +1041,8 @@ try {
|
||||
alpr,
|
||||
cameraEntities,
|
||||
sourceLabel: traffic.loadingLabel,
|
||||
cameraHeight: viewer.camera.positionCartographic.height,
|
||||
cameraHeight:
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
};
|
||||
});
|
||||
result.traffic = austin.traffic;
|
||||
@@ -1219,14 +1202,10 @@ try {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60_000,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document
|
||||
.getElementById('loading-screen')
|
||||
?.classList.contains('hidden'),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -1282,9 +1261,9 @@ try {
|
||||
{ timeout: 120_000, polling: 500 },
|
||||
before,
|
||||
);
|
||||
result.military = await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const center = viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
result.military = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const center = qaGeo.cartographic.toCartesian({
|
||||
latitude: (30.314 * Math.PI) / 180,
|
||||
longitude: (-97.763 * Math.PI) / 180,
|
||||
height: 0,
|
||||
@@ -1367,7 +1346,7 @@ try {
|
||||
priorFort,
|
||||
);
|
||||
result.fortCavazos = await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
// Drawn site discs (context handles, see Camp Mabry) plus the named
|
||||
// markers the stats count.
|
||||
let markers = 0,
|
||||
@@ -1533,5 +1512,5 @@ try {
|
||||
path.join(shots, 'overpass-offload-result.json'),
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
+156
-197
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native flight interception and entity sentinels verify Cesium camera ownership.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* Deterministic browser proof for the Radio companion layer.
|
||||
*
|
||||
@@ -11,10 +13,11 @@
|
||||
*
|
||||
* Run: node scripts/qa-radio.mjs --url http://localhost:4193
|
||||
*/
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { stations } from './fixtures/layers/radio.mjs';
|
||||
import { isExpectedPreviewSetupError } from './qa-browserEvidence.mjs';
|
||||
|
||||
@@ -36,15 +39,6 @@ const RADIO_URL = new URL(APP_URL);
|
||||
RADIO_URL.searchParams.set('welcome', '0');
|
||||
const HEADFUL = args.includes('--headful');
|
||||
|
||||
const chromeCandidates = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
].filter(Boolean);
|
||||
const chrome = chromeCandidates.find((candidate) => {
|
||||
try { return fs.existsSync(candidate); } catch { return false; }
|
||||
});
|
||||
|
||||
|
||||
const results = [];
|
||||
@@ -86,14 +80,13 @@ async function restoreRadioPanels(page, prior) {
|
||||
// not on wall-clock sleeps, particularly with software rendering.
|
||||
async function settleRadioFrames(page) {
|
||||
await page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
let remaining = 4;
|
||||
const timeout = setTimeout(() => { remove(); reject(new Error('Radio rendered-frame deadline exceeded')); }, 10_000);
|
||||
const remove = scene.postRender.addEventListener(() => {
|
||||
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
if (--remaining === 0) { remove(); clearTimeout(timeout); resolve(); }
|
||||
else requestAnimationFrame(() => scene.requestRender());
|
||||
else requestAnimationFrame(() => window.__godsEyeView.engine.render.request());
|
||||
});
|
||||
scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -105,17 +98,9 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
fs.mkdirSync(SHOTS_DIR, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(chrome ? { executablePath: chrome } : {}),
|
||||
args: [
|
||||
'--no-sandbox', '--disable-setuid-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
'--disable-dev-shm-usage', '--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding', '--window-size=1440,900',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', ...(process.platform === 'darwin' ? ['--enable-gpu'] : []), '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1440,900'],
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -370,17 +355,15 @@ async function main() {
|
||||
|
||||
await page.goto(RADIO_URL.href, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, { timeout: 60_000 });
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.styleManager?._dataManager?.layers?.has('radio'), { timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(() => (
|
||||
typeof window.__gevQaRegisterLayer === 'function'
|
||||
&& typeof window.__gevQaUnregisterLayer === 'function'
|
||||
));
|
||||
await page.evaluate(() => {
|
||||
window.__godsEyeView.viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed('pp-toggles', true);
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed('cctv-panel', true);
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed('radio-panel', true);
|
||||
@@ -470,7 +453,7 @@ async function main() {
|
||||
const selectedBefore = module.getUIState().selected?.id || null;
|
||||
const playsBefore = window.__qaRadioPlayCalls.length;
|
||||
const camera = gev.viewer.camera;
|
||||
const originalFlyTo = camera.flyTo;
|
||||
const originalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
const flyToCalls = [];
|
||||
camera.flyTo = (options) => flyToCalls.push(options);
|
||||
const beganTuning = module.beginTuning();
|
||||
@@ -643,7 +626,7 @@ async function main() {
|
||||
const scroller = document.querySelector('#global-context-panel .global-context-panel-inner');
|
||||
const radio = document.getElementById('radio-panel');
|
||||
scroller.scrollTop = Math.max(0, radio.offsetTop - scroller.clientHeight + 70);
|
||||
const camera = gev.viewer.camera.positionWC;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().position;
|
||||
return {
|
||||
scrollTop: scroller.scrollTop,
|
||||
pageY: window.scrollY,
|
||||
@@ -716,7 +699,7 @@ async function main() {
|
||||
const viewport = scroller.getBoundingClientRect();
|
||||
const directory = document.querySelector('#radio-panel .radio-directory-row').getBoundingClientRect();
|
||||
const play = document.getElementById('radio-play-btn').getBoundingClientRect();
|
||||
const camera = gev.viewer.camera.positionWC;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().position;
|
||||
return {
|
||||
scrollTop: scroller.scrollTop,
|
||||
pageY: window.scrollY,
|
||||
@@ -762,23 +745,21 @@ async function main() {
|
||||
{ name: 'global', height: 24_200_000, limit: 16 },
|
||||
{ name: 'near', height: 500_000, limit: 32 },
|
||||
]) {
|
||||
await page.evaluate(({ lon, height }) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo, { lon, height }) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: lon * Math.PI / 180,
|
||||
latitude: 6 * Math.PI / 180,
|
||||
height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
}, { lon: spec.lon, height: view.height });
|
||||
await sleep(700);
|
||||
await whenFramesDrawn(page);
|
||||
await settleRadioFrames(page);
|
||||
const sample = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
@@ -806,11 +787,11 @@ async function main() {
|
||||
});
|
||||
};
|
||||
const timeout = setTimeout(finish, 500);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
finish();
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
});
|
||||
singletonViews.push({ ...spec, ...view, ...sample });
|
||||
@@ -1362,17 +1343,15 @@ async function main() {
|
||||
&& allPalette.music?.[1] > allPalette.music?.[2],
|
||||
JSON.stringify(allPalette),
|
||||
);
|
||||
const clusterBadge = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const clusterBadge = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
// Start from another view, so the clusters at the view below are a new
|
||||
// cohort the host must solve (the old check forced one by switching
|
||||
// Cesium's clustering off and on).
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: 68 * Math.PI / 180,
|
||||
latitude: 7 * Math.PI / 180,
|
||||
height: 24_200_000,
|
||||
@@ -1382,15 +1361,15 @@ async function main() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await drawn.frames(3);
|
||||
const baselineSolveRevision = getWorldOverlayDiagnostics().solveRevision;
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -98 * Math.PI / 180,
|
||||
latitude: 38 * Math.PI / 180,
|
||||
height: 18_000_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
// The layer clusters on screen itself now (render/display.js,
|
||||
// ../screenClusters.js) into the radio-clusters point set; the cluster
|
||||
@@ -1405,7 +1384,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
done({ rect: null, host: getWorldOverlayDiagnostics() });
|
||||
}, 150);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const host = getWorldOverlayDiagnostics();
|
||||
@@ -1421,7 +1400,7 @@ async function main() {
|
||||
host,
|
||||
});
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
}
|
||||
const position = typeof painted?.position === 'function' ? painted.position() : painted?.position;
|
||||
@@ -1451,30 +1430,27 @@ async function main() {
|
||||
&& clusterBadge?.rect?.h > 0,
|
||||
JSON.stringify(clusterBadge),
|
||||
);
|
||||
await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: 68 * Math.PI / 180,
|
||||
latitude: 7 * Math.PI / 180,
|
||||
height: 24_200_000,
|
||||
}),
|
||||
orientation: { heading: 15 * Math.PI / 180, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
});
|
||||
await settleRadioFrames(page);
|
||||
const highGlobalClusterLabels = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const highGlobalClusterLabels = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
const ellipsoid = qaGeo.ellipsoid.WGS84;
|
||||
const { getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const { distanceFade } = await import('/src/overlays/worldOverlayDraw.js');
|
||||
const source = radio.getOverlayDiagnostics();
|
||||
const cameraPosition = viewer.camera.positionWC;
|
||||
const cameraPosition = window.__godsEyeView.engine.camera.pose().position;
|
||||
const cameraRadius = Math.hypot(cameraPosition.x, cameraPosition.y, cameraPosition.z);
|
||||
const horizonDistance = Math.sqrt(Math.max(
|
||||
0,
|
||||
@@ -1486,8 +1462,8 @@ async function main() {
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
const { getPaintRect: getOverlayPaintRect } = window.__gevWorldOverlay;
|
||||
let clusterPoint = null;
|
||||
const width = viewer.canvas.clientWidth;
|
||||
const height = viewer.canvas.clientHeight;
|
||||
const width = window.__godsEyeView.engine.host.canvas.clientWidth;
|
||||
const height = window.__godsEyeView.engine.host.canvas.clientHeight;
|
||||
const anchors = source.clusterIds
|
||||
.map((entryId) => getOverlayPaintRect('radio', entryId)?.entry?.position)
|
||||
.map((position) => drawn.canvasPoint(typeof position === 'function' ? position() : position))
|
||||
@@ -1503,7 +1479,7 @@ async function main() {
|
||||
return {
|
||||
source,
|
||||
host: getWorldOverlayDiagnostics(),
|
||||
altitude: viewer.camera.positionCartographic.height,
|
||||
altitude: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
horizonDistance,
|
||||
horizonDistanceAlpha: distanceFade(horizonDistance, { maxDistance: 50_000_000 }),
|
||||
clusterPoint,
|
||||
@@ -1521,10 +1497,8 @@ async function main() {
|
||||
JSON.stringify(highGlobalClusterLabels),
|
||||
);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'high-global-all-labels.png') });
|
||||
const clusterContinuity = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const clusterContinuity = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const paintedClusters = () => new Promise((resolve) => {
|
||||
let removePostRender = null;
|
||||
@@ -1532,7 +1506,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
resolve({ clusters: [], fadingCount: -1, host: null, source: null });
|
||||
}, 500);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const source = radio.getOverlayDiagnostics();
|
||||
@@ -1553,20 +1527,20 @@ async function main() {
|
||||
source,
|
||||
});
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
const longitudes = [-25, -24, -23, -22, -23, -24, -25];
|
||||
const samples = [];
|
||||
for (const longitude of longitudes) {
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: longitude * Math.PI / 180,
|
||||
latitude: 38 * Math.PI / 180,
|
||||
height: 10_000_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
for (const delay of [50, 100, 200, 350]) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
samples.push({ longitude, delay, ...(await paintedClusters()) });
|
||||
@@ -1621,11 +1595,10 @@ async function main() {
|
||||
JSON.stringify(clusterContinuity),
|
||||
);
|
||||
const directoryRefreshContinuity = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const before = radio.getOverlayDiagnostics().clusterMemberships;
|
||||
await radio.update();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||
const after = radio.getOverlayDiagnostics().clusterMemberships;
|
||||
const beforeByMembership = new Map(before.map((entry) => [entry.membershipId, entry.entryId]));
|
||||
@@ -2451,7 +2424,7 @@ async function main() {
|
||||
await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
window.__qaFirstPlayFlyToCalls = [];
|
||||
window.__qaFirstPlayOriginalFlyTo = camera.flyTo;
|
||||
window.__qaFirstPlayOriginalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
camera.flyTo = (options) => window.__qaFirstPlayFlyToCalls.push(options);
|
||||
});
|
||||
const firstPlayPanels = await revealRadioControls(page, 'radio-play-btn');
|
||||
@@ -2496,11 +2469,10 @@ async function main() {
|
||||
check('first Play never moves the camera', firstPlay.flyToCalls === 0, `${firstPlay.flyToCalls} flyTo calls`);
|
||||
|
||||
await page.select('#radio-filter', 'news');
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -98 * Math.PI / 180,
|
||||
latitude: 38 * Math.PI / 180,
|
||||
height: 18_000_000,
|
||||
@@ -2508,7 +2480,7 @@ async function main() {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
});
|
||||
await sleep(700);
|
||||
await whenFramesDrawn(page);
|
||||
await settleRadioFrames(page);
|
||||
const newsClusterTarget = await page.evaluate(() => {
|
||||
// A drawn cluster: a painted cluster entry whose anchor picks a
|
||||
@@ -2555,7 +2527,7 @@ async function main() {
|
||||
);
|
||||
|
||||
await page.select('#radio-filter', 'weather');
|
||||
await sleep(700);
|
||||
await whenFramesDrawn(page);
|
||||
const weatherCluster = await page.evaluate(() => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
return { texts: radio.getOverlayDiagnostics().clusterTexts };
|
||||
@@ -2607,19 +2579,18 @@ async function main() {
|
||||
);
|
||||
const idleHorizonScans = await page.evaluate(async () => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
const positionBefore = {
|
||||
x: camera.positionWC.x,
|
||||
y: camera.positionWC.y,
|
||||
z: camera.positionWC.z,
|
||||
x: window.__godsEyeView.engine.camera.pose().position.x,
|
||||
y: window.__godsEyeView.engine.camera.pose().position.y,
|
||||
z: window.__godsEyeView.engine.camera.pose().position.z,
|
||||
};
|
||||
const before = radio.getStats().horizonScans;
|
||||
await new Promise((resolve) => setTimeout(resolve, 850));
|
||||
const dx = camera.positionWC.x - positionBefore.x;
|
||||
const dy = camera.positionWC.y - positionBefore.y;
|
||||
const dz = camera.positionWC.z - positionBefore.z;
|
||||
const dx = window.__godsEyeView.engine.camera.pose().position.x - positionBefore.x;
|
||||
const dy = window.__godsEyeView.engine.camera.pose().position.y - positionBefore.y;
|
||||
const dz = window.__godsEyeView.engine.camera.pose().position.z - positionBefore.z;
|
||||
return {
|
||||
before,
|
||||
after: radio.getStats().horizonScans,
|
||||
@@ -2729,21 +2700,19 @@ async function main() {
|
||||
&& slider.getBoundingClientRect().width > 0;
|
||||
});
|
||||
|
||||
const horizon = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
const horizon = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
// What is drawn, through the engine's picks at every fixture station's
|
||||
// canvas point: a station behind the globe projects onto the canvas
|
||||
// too (the point sets draw without a depth test), so a pick of one
|
||||
// there means it was not culled.
|
||||
const setView = (lon, lat = 0) => {
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({ longitude: lon * Math.PI / 180, latitude: lat * Math.PI / 180, height: 2_000_000 }),
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({ longitude: lon * Math.PI / 180, latitude: lat * Math.PI / 180, height: 2_000_000 }),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
};
|
||||
setView(-54);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
@@ -2894,7 +2863,7 @@ async function main() {
|
||||
buttons: type === 'pointercancel' || type === 'pointerup' ? 0 : 1,
|
||||
}));
|
||||
const camera = gev.viewer.camera;
|
||||
const originalFlyTo = camera.flyTo;
|
||||
const originalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
const flyToCalls = [];
|
||||
camera.flyTo = (options) => flyToCalls.push(options);
|
||||
const before = {
|
||||
@@ -3208,7 +3177,7 @@ async function main() {
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(tunerCommitTarget.targetX, tunerCommitTarget.y, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
await sleep(650);
|
||||
await whenFramesDrawn(page);
|
||||
const tunerCommit = await page.evaluate((target) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const slider = document.getElementById('radio-tuner-slider');
|
||||
@@ -3317,7 +3286,7 @@ async function main() {
|
||||
&& sample.ratio >= microDragSamples[index - 1].ratio)),
|
||||
JSON.stringify(microDragSamples),
|
||||
);
|
||||
const viewportTuner = await page.evaluate(async (selectedId) => {
|
||||
const viewportTuner = await evaluateWithGeo(page, async (qaGeo, selectedId) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const style = gev.styleManager;
|
||||
const before = {
|
||||
@@ -3326,11 +3295,11 @@ async function main() {
|
||||
selectedIndex: style._radioControls._radioTunerStations.findIndex((station) => station.id === selectedId),
|
||||
needle: Number(document.getElementById('radio-tuner').style.getPropertyValue('--radio-tuner-ratio')),
|
||||
};
|
||||
const current = gev.viewer.camera.positionCartographic;
|
||||
const current = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
const oppositeLongitude = ((current.longitude + 2 * Math.PI) % (2 * Math.PI)) - Math.PI;
|
||||
gev.viewer.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
gev.viewer.camera.setView({
|
||||
destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.host.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: oppositeLongitude,
|
||||
latitude: -current.latitude,
|
||||
height: 2_000_000,
|
||||
@@ -3373,7 +3342,7 @@ async function main() {
|
||||
};
|
||||
});
|
||||
await page.$eval('#radio-next-btn', (button) => button.click());
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
const nextNeedleAfter = await page.evaluate(() => {
|
||||
const style = window.__godsEyeView.styleManager;
|
||||
const selectedId = window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().selected?.id;
|
||||
@@ -3568,10 +3537,9 @@ async function main() {
|
||||
&& fullPoolNavigation.afterNextWrap === fullPoolNavigation.expectedNextWrap,
|
||||
JSON.stringify(fullPoolNavigation),
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: 2_000_000,
|
||||
@@ -3579,7 +3547,7 @@ async function main() {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
});
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
// Next left the Radio stopped; a release plays only from an active stream.
|
||||
await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('radio').module.togglePlayback({ origin: 'user' }));
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
@@ -3630,8 +3598,7 @@ async function main() {
|
||||
);
|
||||
await page.select('#radio-filter', 'news');
|
||||
|
||||
const markerTarget = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const markerTarget = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const drawn = window.__qaRadioDrawn;
|
||||
// Radio clusters on screen in its own display now (no Cesium
|
||||
@@ -3645,26 +3612,26 @@ async function main() {
|
||||
origin: 'programmatic',
|
||||
});
|
||||
}
|
||||
viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
const markerView = {
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -174 * Math.PI / 180,
|
||||
latitude: 6 * Math.PI / 180,
|
||||
height: 500_000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
};
|
||||
viewer.camera.setView(markerView);
|
||||
window.__godsEyeView.engine.camera.setView(markerView);
|
||||
await new Promise((resolve) => {
|
||||
let removePostRender = null;
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
removePostRender?.();
|
||||
resolve();
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
radio.setFilter('news');
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const { getPaintRect: getOverlayPaintRect, getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
return new Promise((resolve) => {
|
||||
@@ -3677,13 +3644,13 @@ async function main() {
|
||||
resolve(result);
|
||||
};
|
||||
const probe = () => {
|
||||
const camera = viewer.camera.positionCartographic;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
const longitude = camera.longitude * 180 / Math.PI;
|
||||
const latitude = camera.latitude * 180 / Math.PI;
|
||||
if (Math.abs(longitude + 174) > 0.5 || Math.abs(latitude - 6) > 0.5) {
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView(markerView);
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView(markerView);
|
||||
window.__godsEyeView.engine.render.request();
|
||||
return;
|
||||
}
|
||||
const diagnostics = radio.getOverlayDiagnostics();
|
||||
@@ -3726,16 +3693,16 @@ async function main() {
|
||||
overlayEntryCount: diagnostics.entryCount,
|
||||
worldOverlay: getWorldOverlayDiagnostics(),
|
||||
camera: {
|
||||
longitude: viewer.camera.positionCartographic.longitude * 180 / Math.PI,
|
||||
latitude: viewer.camera.positionCartographic.latitude * 180 / Math.PI,
|
||||
height: viewer.camera.positionCartographic.height,
|
||||
longitude: window.__godsEyeView.engine.camera.pose().cartographic.longitude * 180 / Math.PI,
|
||||
latitude: window.__godsEyeView.engine.camera.pose().cartographic.latitude * 180 / Math.PI,
|
||||
height: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
},
|
||||
};
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
};
|
||||
timeout = setTimeout(() => finish(lastObservation || { found: false, reason: 'no post-render probe' }), 8000);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(probe);
|
||||
viewer.scene.requestRender();
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(probe);
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
});
|
||||
check('Radio exposes a visible marker for click-tolerance QA', markerTarget?.found === true, JSON.stringify(markerTarget));
|
||||
@@ -3761,14 +3728,13 @@ async function main() {
|
||||
check('an offset click within 8px reliably selects the intended Radio dot', offsetSelected === markerTarget.id, offsetSelected);
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const camera = gev.viewer.camera;
|
||||
const radio = gev.dataManager.layers.get('radio').module;
|
||||
camera.cancelFlight();
|
||||
gev.viewer.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
camera.setView({
|
||||
destination: gev.viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.host.canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 1 }));
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: 2_000_000,
|
||||
@@ -3792,12 +3758,12 @@ async function main() {
|
||||
window.__qaRadioFailedNearest = false;
|
||||
window.__qaRadioDelayNextFailure = true;
|
||||
window.__qaRadioFlyToCalls = [];
|
||||
window.__qaRadioOriginalFlyTo = camera.flyTo;
|
||||
window.__qaRadioCameraHeight = camera.positionCartographic.height;
|
||||
window.__qaRadioOriginalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
window.__qaRadioCameraHeight = window.__godsEyeView.engine.camera.pose().cartographic.height;
|
||||
camera.flyTo = (options) => window.__qaRadioFlyToCalls.push(options);
|
||||
gev.dataManager.layers.get('flights').enabled = true;
|
||||
});
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('global-context-panel', false);
|
||||
@@ -3805,7 +3771,7 @@ async function main() {
|
||||
});
|
||||
await page.waitForSelector('#radio-next-btn', { visible: true });
|
||||
await page.evaluate(() => {
|
||||
window.__qaRadioCameraHeight = window.__godsEyeView.viewer.camera.positionCartographic.height;
|
||||
window.__qaRadioCameraHeight = window.__godsEyeView.engine.camera.pose().cartographic.height;
|
||||
});
|
||||
const radioOnlyNextDispatched = await page.evaluate(() => {
|
||||
const button = document.getElementById('radio-next-btn');
|
||||
@@ -3815,10 +3781,9 @@ async function main() {
|
||||
});
|
||||
if (!radioOnlyNextDispatched) throw new Error('Radio next button unavailable');
|
||||
await page.waitForFunction(() => typeof window.__qaRejectRadioPlay === 'function');
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: 2_400_000,
|
||||
@@ -3828,12 +3793,11 @@ async function main() {
|
||||
window.__qaRejectRadioPlay();
|
||||
});
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
const playing = await page.evaluate(async () => {
|
||||
const playing = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const { getDiagnostics: getWorldOverlayDiagnostics } = window.__gevWorldOverlay;
|
||||
const flightTargets = window.__qaRadioFlyToCalls.map(({ destination }) => {
|
||||
const position = viewer.scene.globe.ellipsoid.cartesianToCartographic(destination);
|
||||
const position = qaGeo.cartographic.fromCartesian(destination);
|
||||
return {
|
||||
lat: position.latitude * 180 / Math.PI,
|
||||
lon: position.longitude * 180 / Math.PI,
|
||||
@@ -3887,7 +3851,7 @@ async function main() {
|
||||
const run = async (ownerPrefix, buttonId) => {
|
||||
const owner = viewer.entities.add({
|
||||
id: `qa:${ownerPrefix}:radio-camera-owner`,
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
});
|
||||
owner.gevTrackedId = `${ownerPrefix}:qa-radio-camera-owner`;
|
||||
viewer.trackedEntity = owner;
|
||||
@@ -3903,7 +3867,7 @@ async function main() {
|
||||
afterStationId: radio.getUIState().selected?.id || null,
|
||||
flyToDelta: window.__qaRadioFlyToCalls.length - beforeFlyToCalls,
|
||||
trackedIdentityPreserved: viewer.trackedEntity === owner,
|
||||
trackedId: viewer.trackedEntity?.gevTrackedId || null,
|
||||
trackedId: window.__godsEyeView.engine.camera.following()?.label?.id || null,
|
||||
};
|
||||
viewer.trackedEntity = undefined;
|
||||
viewer.entities.remove(owner);
|
||||
@@ -3937,7 +3901,7 @@ async function main() {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const owner = viewer.entities.add({
|
||||
id: 'qa:flights:delayed-radio-fallback-owner',
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
});
|
||||
owner.gevTrackedId = 'flights:qa-delayed-radio-fallback-owner';
|
||||
viewer.trackedEntity = owner;
|
||||
@@ -3952,7 +3916,7 @@ async function main() {
|
||||
const result = {
|
||||
flyToDelta: window.__qaRadioFlyToCalls.length - beforeFlyToCalls,
|
||||
trackedIdentityPreserved: viewer.trackedEntity === owner,
|
||||
trackedId: viewer.trackedEntity?.gevTrackedId || null,
|
||||
trackedId: window.__godsEyeView.engine.camera.following()?.label?.id || null,
|
||||
selectedId: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().selected?.id || null,
|
||||
};
|
||||
viewer.trackedEntity = undefined;
|
||||
@@ -3968,10 +3932,9 @@ async function main() {
|
||||
&& Boolean(delayedFallbackOwnership.selectedId),
|
||||
JSON.stringify(delayedFallbackOwnership),
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: Math.PI,
|
||||
latitude: -2 * Math.PI / 180,
|
||||
height: window.__qaRadioCameraHeight,
|
||||
@@ -3992,21 +3955,20 @@ async function main() {
|
||||
&& playing.host.entriesBySource?.radio === playing.overlay.entryCount,
|
||||
JSON.stringify({ overlay: playing.overlay, host: playing.host.entriesBySource }),
|
||||
);
|
||||
const closeSelectedText = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const closeSelectedText = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const selected = radio.getUIState().selected;
|
||||
window.__qaRadioVisualView = {
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
direction: viewer.camera.directionWC.clone(),
|
||||
up: viewer.camera.upWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
direction: window.__godsEyeView.engine.camera.pose().direction,
|
||||
up: window.__godsEyeView.engine.camera.pose().up,
|
||||
};
|
||||
const destination = viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
const destination = qaGeo.cartographic.toCartesian({
|
||||
longitude: selected.lon * Math.PI / 180,
|
||||
latitude: selected.lat * Math.PI / 180,
|
||||
height: 5_000,
|
||||
});
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination,
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
@@ -4033,7 +3995,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
resolve({ rect: null, host: getWorldOverlayDiagnostics() });
|
||||
}, 150);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const rect = getOverlayPaintRect('radio', expectedEntryId);
|
||||
@@ -4042,7 +4004,7 @@ async function main() {
|
||||
host: getWorldOverlayDiagnostics(),
|
||||
});
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
shown = selectedShown();
|
||||
}
|
||||
@@ -4051,7 +4013,7 @@ async function main() {
|
||||
rect: painted?.rect || null,
|
||||
host: painted?.host || getWorldOverlayDiagnostics(),
|
||||
selectedEntityShow: shown,
|
||||
cameraHeight: viewer.camera.positionCartographic.height,
|
||||
cameraHeight: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
};
|
||||
});
|
||||
check(
|
||||
@@ -4062,13 +4024,11 @@ async function main() {
|
||||
JSON.stringify(closeSelectedText),
|
||||
);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'selected-label-desktop.png') });
|
||||
const highGlobalSelectedText = await page.evaluate(async () => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const highGlobalSelectedText = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const selected = radio.getUIState().selected;
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (selected.lon + 10) * Math.PI / 180,
|
||||
latitude: selected.lat * Math.PI / 180,
|
||||
height: 5_000_000,
|
||||
@@ -4086,7 +4046,7 @@ async function main() {
|
||||
removePostRender?.();
|
||||
resolve(null);
|
||||
}, 150);
|
||||
removePostRender = viewer.scene.postRender.addEventListener(() => {
|
||||
removePostRender = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
clearTimeout(timeout);
|
||||
removePostRender();
|
||||
const painted = getOverlayPaintRect('radio', expectedEntryId);
|
||||
@@ -4097,7 +4057,7 @@ async function main() {
|
||||
h: painted.h,
|
||||
} : null);
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
}
|
||||
// The selected point and its bracket are instances of the
|
||||
@@ -4129,14 +4089,14 @@ async function main() {
|
||||
entityAnchor: entityAnchor ? { x: entityAnchor.x, y: entityAnchor.y } : null,
|
||||
entityDistance: entityPosition
|
||||
? Math.hypot(
|
||||
viewer.camera.positionWC.x - entityPosition.x,
|
||||
viewer.camera.positionWC.y - entityPosition.y,
|
||||
viewer.camera.positionWC.z - entityPosition.z,
|
||||
window.__godsEyeView.engine.camera.pose().position.x - entityPosition.x,
|
||||
window.__godsEyeView.engine.camera.pose().position.y - entityPosition.y,
|
||||
window.__godsEyeView.engine.camera.pose().position.z - entityPosition.z,
|
||||
)
|
||||
: null,
|
||||
overlay: radio.getOverlayDiagnostics(),
|
||||
host: getWorldOverlayDiagnostics(),
|
||||
cameraHeight: viewer.camera.positionCartographic.height,
|
||||
cameraHeight: window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
};
|
||||
});
|
||||
check(
|
||||
@@ -4164,7 +4124,7 @@ async function main() {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
window.__godsEyeView.styleManager.toggleCleanView(true);
|
||||
viewer.resize();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
});
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'selected-label-mobile.png') });
|
||||
@@ -4173,19 +4133,18 @@ async function main() {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
window.__godsEyeView.styleManager.toggleCleanView(false);
|
||||
viewer.resize();
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const view = window.__qaRadioVisualView;
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: view.position,
|
||||
orientation: { direction: view.direction, up: view.up },
|
||||
});
|
||||
delete window.__qaRadioVisualView;
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.__qaRadioFlyToCalls = [];
|
||||
@@ -4202,10 +4161,10 @@ async function main() {
|
||||
radioOnlyNextClicked,
|
||||
`clicked=${radioOnlyNextClicked}`,
|
||||
);
|
||||
const radioOnlyFocusCalls = await page.evaluate(() => {
|
||||
const radioOnlyFocusCalls = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const calls = window.__qaRadioFlyToCalls.length;
|
||||
const heights = window.__qaRadioFlyToCalls.map((options) => (
|
||||
window.__godsEyeView.viewer.scene.globe.ellipsoid.cartesianToCartographic(options.destination).height
|
||||
qaGeo.cartographic.fromCartesian(options.destination).height
|
||||
));
|
||||
window.__godsEyeView.viewer.camera.flyTo = window.__qaRadioOriginalFlyTo;
|
||||
delete window.__qaRadioOriginalFlyTo;
|
||||
@@ -4218,19 +4177,19 @@ async function main() {
|
||||
JSON.stringify(radioOnlyFocusCalls),
|
||||
);
|
||||
|
||||
const stagedGlobeNavigation = await page.evaluate(async () => {
|
||||
const stagedGlobeNavigation = await evaluateWithGeo(page, async (qaGeo) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const viewer = gev.viewer;
|
||||
const camera = viewer.camera;
|
||||
const radio = gev.dataManager.layers.get('radio').module;
|
||||
const stationIds = radio.getTunerStations(3).map((station) => station.id);
|
||||
if (stationIds.length < 3) throw new Error('Radio recenter QA requires three visible stations');
|
||||
const originalFlyTo = camera.flyTo;
|
||||
const originalFlyTo = window.__godsEyeView.engine.camera.flyTo;
|
||||
const calls = [];
|
||||
camera.flyTo = (options) => calls.push(options);
|
||||
const setCamera = (height, pitchDegrees, headingDegrees = 38) => {
|
||||
camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: -133.9483 * Math.PI / 180,
|
||||
latitude: 23.4741 * Math.PI / 180,
|
||||
height,
|
||||
@@ -4241,18 +4200,18 @@ async function main() {
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
};
|
||||
const completeFlight = (index) => {
|
||||
const flight = calls[index];
|
||||
camera.setView({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: flight.destination,
|
||||
orientation: flight.orientation,
|
||||
});
|
||||
flight.complete?.();
|
||||
};
|
||||
const describe = (flight) => {
|
||||
const cartographic = viewer.scene.globe.ellipsoid.cartesianToCartographic(flight.destination);
|
||||
const cartographic = qaGeo.cartographic.fromCartesian(flight.destination);
|
||||
return {
|
||||
lat: cartographic.latitude * 180 / Math.PI,
|
||||
lon: cartographic.longitude * 180 / Math.PI,
|
||||
@@ -4279,7 +4238,7 @@ async function main() {
|
||||
radio.cycleStation(1, { rotate: true, autoplay: false, stationIds });
|
||||
const owner = viewer.entities.add({
|
||||
id: 'qa:flights:radio-recenter-stage-owner',
|
||||
position: viewer.camera.positionWC.clone(),
|
||||
position: window.__godsEyeView.engine.camera.pose().position,
|
||||
});
|
||||
owner.gevTrackedId = 'flights:qa-radio-recenter-stage-owner';
|
||||
viewer.trackedEntity = owner;
|
||||
@@ -4509,7 +4468,7 @@ async function main() {
|
||||
);
|
||||
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager.setPanelCollapsed('radio-panel', false));
|
||||
await sleep(350);
|
||||
await whenFramesDrawn(page);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'desktop.png') });
|
||||
const desktop = await page.$eval('#radio-panel', (element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -4531,7 +4490,7 @@ async function main() {
|
||||
await page.click('#radio-play-btn');
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 });
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
await page.$eval('#radio-panel', (element) => element.scrollIntoView({ block: 'nearest' }));
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'mobile.png') });
|
||||
const mobile = await page.$eval('#radio-panel', (element) => {
|
||||
@@ -4599,7 +4558,7 @@ async function main() {
|
||||
failedResponses.slice(0, 3).join(' | '),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
|
||||
const failures = results.filter((result) => !result.ok).length;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native imagery stacks and split position verify the Cesium imagery adapter.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenUiSettled, whenSettled } from './qa/ready.mjs';
|
||||
/**
|
||||
* Rendered acceptance for DATA ▸ Recent Imagery against the real NASA
|
||||
* endpoints (CMR catalog, GIBS tiles, Worldview snapshots): the rail panel,
|
||||
@@ -17,9 +20,9 @@
|
||||
* 4 s ZOOM IN wait and 0.8 s height-stability tail (defaults: 2 s / 1.2 s).
|
||||
* Every wait is bounded. Without NASA access the run skips with code 0.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
|
||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
@@ -51,21 +54,14 @@ if (!(await networkAvailable())) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !HEADFUL,
|
||||
...(HEADFUL
|
||||
? {
|
||||
executablePath: chromeExecutablePath(),
|
||||
}
|
||||
: {}),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
...(HEADFUL ? ['--window-size=1280,860'] : []),
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -200,7 +196,7 @@ const state = () =>
|
||||
: null,
|
||||
splitLeft: split ? split.getBoundingClientRect().left : null,
|
||||
splitPosition: gev.viewer.scene.splitPosition,
|
||||
canvasWidth: gev.viewer.scene.canvas.clientWidth,
|
||||
canvasWidth: window.__godsEyeView.engine.host.canvas.clientWidth,
|
||||
activeStack: qa.activeStack(),
|
||||
gibsOnGlobe: hosts.globe,
|
||||
gibsOnTileset: hosts.tileset,
|
||||
@@ -212,25 +208,27 @@ const hostsDetail = (s) =>
|
||||
|
||||
/** Put the camera straight down over a point at a height, no flight. */
|
||||
const setView = (view) =>
|
||||
page.evaluate((target) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.scene.tweens?.removeAll?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (target.lon * Math.PI) / 180,
|
||||
latitude: (target.lat * Math.PI) / 180,
|
||||
height: target.height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, view);
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, target) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (target.lon * Math.PI) / 180,
|
||||
latitude: (target.lat * Math.PI) / 180,
|
||||
height: target.height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
view,
|
||||
);
|
||||
|
||||
/** Ground width of the camera's view rectangle, in km (0 without one). */
|
||||
const viewWidthKm = () =>
|
||||
page.evaluate(() => {
|
||||
const rect = window.__godsEyeView.viewer.camera.computeViewRectangle?.();
|
||||
const rect = window.__godsEyeView.engine.camera.viewRectangle?.();
|
||||
if (!rect) return 0;
|
||||
const lat = (rect.south + rect.north) / 2;
|
||||
return Math.abs(rect.east - rect.west) * 6371 * Math.cos(lat);
|
||||
@@ -279,8 +277,9 @@ async function probe() {
|
||||
const tilesSettled = () =>
|
||||
until(() => {
|
||||
const gev = window.__godsEyeView;
|
||||
if (gev.viewer.scene.globe.show) return gev.viewer.scene.globe.tilesLoaded;
|
||||
return !gev.tileset || gev.tileset.tilesLoaded;
|
||||
if (gev.viewer.scene.globe.show)
|
||||
return window.__godsEyeView.engine.surface.tilesLoaded();
|
||||
return !gev.tileset || window.__godsEyeView.engine.surface.tilesLoaded();
|
||||
});
|
||||
const backOnPhotoreal = () =>
|
||||
until(() => window.__riQa.activeStack() === 'photoreal');
|
||||
@@ -334,7 +333,7 @@ const measure = () =>
|
||||
let layoutBaseline = null;
|
||||
/** Every control's rect matches the baseline within 1 px. */
|
||||
async function checkLayout(label) {
|
||||
await wait(150);
|
||||
await whenUiSettled(page);
|
||||
const now = await measure();
|
||||
if (!layoutBaseline) {
|
||||
layoutBaseline = now;
|
||||
@@ -370,12 +369,13 @@ try {
|
||||
() =>
|
||||
window.__gevRecentImagery &&
|
||||
window.__godsEyeView &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
window.__godsEyeView.engine,
|
||||
null,
|
||||
90_000,
|
||||
);
|
||||
check('the app boots with the Recent Imagery handle', booted);
|
||||
if (!booted) throw new Error('boot timeout');
|
||||
await whenSettled(page);
|
||||
await page.evaluate(installPageHelpers);
|
||||
|
||||
await setView(AUSTIN);
|
||||
@@ -586,10 +586,8 @@ try {
|
||||
changedColour = colour;
|
||||
break;
|
||||
}
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.viewer.scene.requestRender(),
|
||||
);
|
||||
await wait(1000);
|
||||
await page.evaluate(() => window.__godsEyeView.engine.render.request());
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
check(
|
||||
'the rendered frame inside the box changes once the preview drapes',
|
||||
@@ -681,7 +679,7 @@ try {
|
||||
);
|
||||
await checkLayout('pinned');
|
||||
await page.keyboard.press(targetIndex > 0 ? 'ArrowLeft' : 'ArrowRight');
|
||||
await wait(700);
|
||||
await whenUiSettled(page);
|
||||
current = await state();
|
||||
check(
|
||||
'with the image pinned, focus moves without changing the map',
|
||||
@@ -981,7 +979,7 @@ try {
|
||||
let refusalWidth = 0;
|
||||
for (const height of [400_000, 900_000, 1_800_000]) {
|
||||
await setView({ ...AUSTIN, height });
|
||||
await wait(300);
|
||||
await whenUiSettled(page);
|
||||
refusalWidth = await viewWidthKm();
|
||||
if (refusalWidth > 1_100) break;
|
||||
}
|
||||
@@ -1041,10 +1039,9 @@ try {
|
||||
await panelShot('05-refused-panel');
|
||||
// ZOOM IN: straight down to the height whose view is 400 km wide.
|
||||
const fitHeight = await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
const canvas = viewer.scene.canvas;
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
const halfWidth =
|
||||
Math.tan(viewer.camera.frustum.fovy / 2) *
|
||||
Math.tan(window.__godsEyeView.engine.camera.pose().fovy / 2) *
|
||||
(canvas.clientWidth / canvas.clientHeight);
|
||||
return Math.min(400, Math.max(5, 400 / (2 * halfWidth))) * 1000;
|
||||
});
|
||||
@@ -1052,14 +1049,14 @@ try {
|
||||
const zoomed = await until(
|
||||
(target) =>
|
||||
Math.abs(
|
||||
window.__godsEyeView.viewer.camera.positionCartographic.height - target,
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height - target,
|
||||
) <=
|
||||
target * 0.01,
|
||||
fitHeight,
|
||||
ZOOM_TIMEOUT_MS,
|
||||
);
|
||||
const zoomedHeight = await page.evaluate(
|
||||
() => window.__godsEyeView.viewer.camera.positionCartographic.height,
|
||||
() => window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
);
|
||||
check(
|
||||
`ZOOM IN reaches target within ${ZOOM_TIMEOUT_MS / 1_000} s`,
|
||||
@@ -1235,7 +1232,7 @@ try {
|
||||
}, id);
|
||||
const imageryPanel = 'recent-imagery-panel';
|
||||
const collapsedNow = (await togglePanel(imageryPanel)) === false;
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
const headerOnly = await page.evaluate(() => ({
|
||||
panel: document.getElementById('recent-imagery-panel')?.offsetHeight,
|
||||
body: document.getElementById('recent-imagery-panel-body')?.offsetHeight,
|
||||
@@ -1249,7 +1246,7 @@ try {
|
||||
JSON.stringify(headerOnly),
|
||||
);
|
||||
const expandedAgain = await togglePanel(imageryPanel);
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
const bodyBack = await page.evaluate(
|
||||
() => document.getElementById('recent-imagery-panel-body')?.offsetHeight,
|
||||
);
|
||||
@@ -1270,7 +1267,7 @@ try {
|
||||
cctv: document.getElementById('cctv-panel')?.offsetHeight,
|
||||
})),
|
||||
);
|
||||
await wait(100);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
const tail = (key) =>
|
||||
new Set(samples.slice(-HEIGHT_TAIL_SAMPLES).map((s) => s[key]));
|
||||
@@ -1315,7 +1312,7 @@ try {
|
||||
for (const height of [900, 700]) {
|
||||
const at = `(${height} px viewport)`;
|
||||
await page.setViewport({ width: 1280, height });
|
||||
await wait(600);
|
||||
await whenUiSettled(page);
|
||||
check(
|
||||
`the CCTV panel expands from its launcher ${at}`,
|
||||
(await togglePanel('cctv-panel')) === true,
|
||||
@@ -1372,7 +1369,7 @@ try {
|
||||
?.classList.contains('collapsed'),
|
||||
);
|
||||
if (imageryExpanded) await togglePanel(imageryPanel);
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
const onScreen = await page.evaluate(() =>
|
||||
Object.fromEntries(
|
||||
[
|
||||
@@ -1389,7 +1386,7 @@ try {
|
||||
JSON.stringify(onScreen),
|
||||
);
|
||||
await togglePanel(imageryPanel);
|
||||
await wait(400);
|
||||
await whenUiSettled(page);
|
||||
}
|
||||
|
||||
// ── scroll preservation (700 px viewport, body scrollable) ───────────────
|
||||
@@ -1397,7 +1394,7 @@ try {
|
||||
const header = document.querySelector('#ri-details .rail-card-header');
|
||||
if (header?.getAttribute('aria-expanded') !== 'true') header?.click();
|
||||
});
|
||||
await wait(300);
|
||||
await whenUiSettled(page);
|
||||
const bodyScroll = () =>
|
||||
page.evaluate(() => {
|
||||
const body = document.getElementById('recent-imagery-panel-body');
|
||||
@@ -1424,7 +1421,7 @@ try {
|
||||
layer.setToolActive(true);
|
||||
layer.setToolActive(false);
|
||||
});
|
||||
await wait(300);
|
||||
await whenUiSettled(page);
|
||||
const afterNotify = await bodyScroll();
|
||||
check(
|
||||
'the body scroll position survives alpha and state notifications',
|
||||
@@ -1434,7 +1431,7 @@ try {
|
||||
await focusStrip();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await wait(600);
|
||||
await whenUiSettled(page);
|
||||
const afterArrows = await bodyScroll();
|
||||
check(
|
||||
'the body scroll position survives arrow navigation',
|
||||
@@ -1488,7 +1485,7 @@ try {
|
||||
error?.stack || error?.message || String(error),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`shots: ${shots.join(', ')}`);
|
||||
console.log(`RESULT: ${checks} checks, ${failures} failures`);
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
/** Exercise installed Scene controls and real project/playback operations. */
|
||||
import { whenSettled, whenUiSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const shots = path.resolve('qa-shots/scene-controls');
|
||||
fs.mkdirSync(shots, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -34,11 +33,10 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.sceneDirector?._controls &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__godsEyeView?.sceneDirector?._controls,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
window.__qaSceneState = [];
|
||||
window.__godsEyeView.sceneDirector.subscribe((notification) =>
|
||||
@@ -294,6 +292,7 @@ try {
|
||||
);
|
||||
await page.click('#scene-start-btn');
|
||||
await page.waitForFunction(() => window.__godsEyeView.sceneDirector.running);
|
||||
await whenSettled(page);
|
||||
await page.$eval('#scene-stop-btn', (button) => button.click());
|
||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector.running);
|
||||
check(
|
||||
@@ -364,7 +363,7 @@ try {
|
||||
await page.setViewport({ width: 390, height: 844 });
|
||||
// Phones present Scenes in the Layers tab's sheet.
|
||||
await page.click('#mobile-tab-bar [data-mobile-sheet="layers"]');
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await whenUiSettled(page);
|
||||
await page.screenshot({ path: path.join(shots, 'narrow.png') });
|
||||
check(
|
||||
'Scene controls stay inside the narrow viewport',
|
||||
@@ -483,6 +482,6 @@ try {
|
||||
errors.length === 0,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (failures) process.exitCode = 1;
|
||||
@@ -14,13 +14,13 @@
|
||||
* Needs the repository's puppeteer (devDependencies). Exit status 0 when the
|
||||
* drop selects the file, 1 otherwise.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import http from 'node:http';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { createServer } from 'vite';
|
||||
import { VUE_PLUGIN_OPTIONS } from '../build/vueOptions.js';
|
||||
|
||||
@@ -77,7 +77,7 @@ async function main() {
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const url = `http://127.0.0.1:${server.address().port}/`;
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
const browser = await launchBrowser({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1000, height: 800 });
|
||||
@@ -100,7 +100,7 @@ async function main() {
|
||||
}
|
||||
console.log('OK: a file dropped on the visible control is selected');
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
server.close();
|
||||
await vite.close();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -7,17 +7,16 @@
|
||||
* feather 0/11/100/200, layout independence,
|
||||
* share/scene restore, and visible text backing including rotated metadata.
|
||||
*/
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -214,16 +213,7 @@ async function openApplication(url) {
|
||||
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const cover = document.getElementById('loading-screen');
|
||||
return (
|
||||
cover?.classList.contains('hidden') &&
|
||||
Number(getComputedStyle(cover).opacity) === 0
|
||||
);
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -668,7 +658,7 @@ try {
|
||||
}
|
||||
check('runtime has no uncaught browser errors', errors.length === 0, errors);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
/** Run against a dev server or built preview with QA_BASE_URL (default :4179). */
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH || (await puppeteer.executablePath()),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--use-gl=angle', '--use-angle=swiftshader'],
|
||||
args: [],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
@@ -30,11 +29,7 @@ try {
|
||||
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
// DISPLAY holds most segmented controls; Draw's shape group is in UTILITIES.
|
||||
await page.evaluate(() => {
|
||||
for (const panel of ['pp-toggles', 'utilities-panel'])
|
||||
@@ -347,7 +342,7 @@ try {
|
||||
await page.screenshot({ path: `${shots}/failure.png` }).catch(() => {});
|
||||
throw error;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures.length} failures`);
|
||||
process.exitCode = failures.length ? 1 : 0;
|
||||
@@ -1,17 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: standalone native scenes validate Cesium model lighting.
|
||||
/**
|
||||
* Lit airplane regression: StarrySky must match a black environment baseline.
|
||||
* A white environment is a positive control: it must visibly brighten the
|
||||
* same model. Each case uses a fresh scene and dynamic environment map.
|
||||
* Usage: node scripts/qa-sky-model-lighting.mjs [--url http://localhost:4333/]
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const i = process.argv.indexOf('--url');
|
||||
const base = i > 0 ? process.argv[i + 1] : 'http://localhost:4333/';
|
||||
const url = new URL('/__qa_sky_model__', base);
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||
});
|
||||
@@ -145,5 +146,5 @@ try {
|
||||
'PASS: lit airplane matches black sky; white control is brighter',
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
/** Badge acceptance on dev/preview. Camera state is a disposable controller fixture. */
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--use-gl=angle', '--use-angle=swiftshader'],
|
||||
args: [],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultNavigationTimeout(60000);
|
||||
@@ -31,11 +32,10 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.styleManager?._cctvControls &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__godsEyeView?.styleManager?._cctvControls,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const { dataManager, styleManager: manager } = window.__godsEyeView;
|
||||
await dataManager.setEnabled('wind', true);
|
||||
@@ -272,7 +272,7 @@ try {
|
||||
}
|
||||
check('no uncaught browser errors', errors.length === 0, errors);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`${checks} checks; ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
+34
-29
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
/**
|
||||
* Browser QA for the Street Level layer against a running server:
|
||||
@@ -223,6 +226,7 @@ async function boot(page, url) {
|
||||
await page.waitForFunction(() => Boolean(window.__godsEyeView?.dataManager), {
|
||||
timeout: 150_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() =>
|
||||
document.querySelector('.first-run-explore')?.click(),
|
||||
);
|
||||
@@ -338,8 +342,9 @@ async function assertKeyRequired(page) {
|
||||
const findPick = (page, prefix, { box = null, step = 3 } = {}) =>
|
||||
page.evaluate(
|
||||
(want, area, stride) => {
|
||||
const { scene } = window.__godsEyeView.viewer;
|
||||
const rect = scene.canvas.getBoundingClientRect();
|
||||
const { engine } = window.__godsEyeView;
|
||||
const rect =
|
||||
window.__godsEyeView.engine.host.canvas.getBoundingClientRect();
|
||||
const { x, y, w, h } = area || {
|
||||
x: 0,
|
||||
y: Math.round(rect.height / 2),
|
||||
@@ -348,7 +353,7 @@ const findPick = (page, prefix, { box = null, step = 3 } = {}) =>
|
||||
};
|
||||
for (let py = y; py < y + h; py += stride)
|
||||
for (let px = x; px < x + w; px += stride) {
|
||||
const id = scene.pick({ x: px, y: py })?.id;
|
||||
const id = engine.picker.pick({ x: px, y: py })?.id;
|
||||
if (typeof id === 'string' && id.startsWith(want))
|
||||
return {
|
||||
x: rect.left + px,
|
||||
@@ -378,30 +383,33 @@ async function findPickUntil(
|
||||
for (;;) {
|
||||
const hit = await findPick(page, prefix, { box, step });
|
||||
if (hit || Date.now() >= deadline) return hit;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await whenSettled(page);
|
||||
}
|
||||
}
|
||||
|
||||
/** Park the camera at `at`; a late startup flight can move it. */
|
||||
async function park(page, at = PARK) {
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
const stayed = await page.evaluate(async (at) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
v.camera.cancelFlight?.();
|
||||
const C = v.camera.positionCartographic.constructor;
|
||||
v.camera.setView({
|
||||
destination: v.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
C.fromDegrees(at.lon, at.lat, at.height),
|
||||
),
|
||||
orientation: { heading: 0, pitch: at.pitch, roll: 0 },
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const c = v.camera.positionCartographic;
|
||||
return (
|
||||
Math.abs((c.longitude * 180) / Math.PI - at.lon) < 0.001 &&
|
||||
Math.abs((c.latitude * 180) / Math.PI - at.lat) < 0.001
|
||||
);
|
||||
}, at);
|
||||
const stayed = await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, at) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
const C = qaGeo.cartographic;
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian(
|
||||
C.fromDegrees(at.lon, at.lat, at.height),
|
||||
),
|
||||
orientation: { heading: 0, pitch: at.pitch, roll: 0 },
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const c = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return (
|
||||
Math.abs((c.longitude * 180) / Math.PI - at.lon) < 0.001 &&
|
||||
Math.abs((c.latitude * 180) / Math.PI - at.lat) < 0.001
|
||||
);
|
||||
},
|
||||
at,
|
||||
);
|
||||
if (stayed) return;
|
||||
}
|
||||
throw new Error(`the camera would not stay parked at ${JSON.stringify(at)}`);
|
||||
@@ -414,14 +422,11 @@ async function main() {
|
||||
const url = urlIndex >= 0 ? args[urlIndex + 1] : 'http://localhost:4173';
|
||||
const fixtures = args.includes('--fixtures');
|
||||
const strict = args.includes('--strict');
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
(await puppeteer.executablePath()),
|
||||
defaultViewport: VIEWPORT,
|
||||
protocolTimeout: 300_000,
|
||||
args: ['--no-sandbox', '--use-gl=angle', '--use-angle=swiftshader'],
|
||||
args: ['--no-sandbox'],
|
||||
});
|
||||
let count = 0;
|
||||
const skips = [];
|
||||
@@ -494,7 +499,7 @@ async function main() {
|
||||
box === 'column'
|
||||
? await page.evaluate(() => {
|
||||
const { height, width } =
|
||||
window.__godsEyeView.viewer.scene.canvas.getBoundingClientRect();
|
||||
window.__godsEyeView.engine.host.canvas.getBoundingClientRect();
|
||||
return { x: Math.round(width * 0.3), y: 0, w: 1, h: height };
|
||||
})
|
||||
: null;
|
||||
@@ -560,7 +565,7 @@ async function main() {
|
||||
'keyless server: the MAPILLARY chip explains its key instead of switching',
|
||||
async () => {
|
||||
await press(page, '#sl-provider-chips [data-chip-id="mapillary"]');
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await whenSettled(page);
|
||||
assert.ok(litAlone(await getUI(page), 'kartaview'));
|
||||
},
|
||||
);
|
||||
@@ -831,7 +836,7 @@ async function main() {
|
||||
if (dir) console.error(`failure evidence saved in ${dir}/`);
|
||||
throw error;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-52
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native terrain-tree counters verify Cesium retry behavior.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, until } from './qa/ready.mjs';
|
||||
/**
|
||||
* Keyless terrain throttling probe.
|
||||
*
|
||||
@@ -21,8 +24,8 @@
|
||||
* Exits 1 when a throttled tile never recovered or a terrain tile is left in
|
||||
* Cesium's FAILED state.
|
||||
*/
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const appUrl = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
const argv = process.argv.slice(2);
|
||||
@@ -46,20 +49,10 @@ const headful = argv.includes('--headful');
|
||||
const TERRAIN_HOST = 'terrain.reearth.land';
|
||||
const TILE_PATTERN = /\/cesium-mesh\/ellipsoid\/\d+\/\d+\/\d+\.terrain/;
|
||||
|
||||
const executablePath =
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
(await puppeteer.executablePath().catch(() => null));
|
||||
if (!executablePath || !fs.existsSync(executablePath)) {
|
||||
throw new Error('Puppeteer Chrome for Testing is unavailable');
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: headful ? false : 'new',
|
||||
executablePath,
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
'--no-sandbox',
|
||||
],
|
||||
});
|
||||
@@ -149,31 +142,17 @@ page.on('console', (message) => {
|
||||
});
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const waitFor = async (predicate, timeoutMs, label) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (await predicate()) return;
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error(`timed out waiting for ${label}`);
|
||||
};
|
||||
|
||||
const setCamera = (lon, lat, height) =>
|
||||
page.evaluate(
|
||||
({ lon, lat, height }) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, { lon, lat, height }) => {
|
||||
// The app's own startup flight would otherwise keep steering the camera
|
||||
// through the burst and turn the zoom-out into a zoom-in.
|
||||
viewer.camera.cancelFlight();
|
||||
const cartographic =
|
||||
viewer.camera.positionCartographic.constructor.fromDegrees(
|
||||
lon,
|
||||
lat,
|
||||
height,
|
||||
);
|
||||
viewer.camera.setView({
|
||||
destination:
|
||||
viewer.scene.globe.ellipsoid.cartographicToCartesian(cartographic),
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
const cartographic = qaGeo.cartographic.fromDegrees(lon, lat, height);
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian(cartographic),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.requestRender?.();
|
||||
@@ -183,7 +162,7 @@ const setCamera = (lon, lat, height) =>
|
||||
|
||||
const cameraHeight = () =>
|
||||
page.evaluate(
|
||||
() => window.__godsEyeView.viewer.camera.positionCartographic.height,
|
||||
() => window.__godsEyeView.engine.camera.pose().cartographic.height,
|
||||
);
|
||||
|
||||
const globeState = () =>
|
||||
@@ -203,7 +182,7 @@ const globeState = () =>
|
||||
return {
|
||||
failed,
|
||||
loaded,
|
||||
tilesLoaded: viewer.scene.globe.tilesLoaded,
|
||||
tilesLoaded: window.__godsEyeView.engine.surface.tilesLoaded(),
|
||||
provider: viewer.terrainProvider?.constructor?.name,
|
||||
};
|
||||
});
|
||||
@@ -212,8 +191,9 @@ const burst = async () => {
|
||||
if (mode === 'zoomOut') {
|
||||
for (let i = 0; i < notches; i += 1) {
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.zoomOut(viewer.camera.positionCartographic.height * 0.6);
|
||||
window.__godsEyeView.engine.camera.zoomOut(
|
||||
window.__godsEyeView.engine.camera.pose().cartographic.height * 0.6,
|
||||
);
|
||||
window.__godsEyeView.requestRender?.();
|
||||
});
|
||||
await sleep(40);
|
||||
@@ -230,14 +210,22 @@ const burst = async () => {
|
||||
|
||||
try {
|
||||
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(() => Boolean(window.__godsEyeView?.viewer), {
|
||||
await page.waitForFunction(() => Boolean(window.__godsEyeView?.engine), {
|
||||
timeout: 60000,
|
||||
});
|
||||
await waitFor(() => layerJsonSeen, 60000, 'the Re:Earth layer.json fetch');
|
||||
await waitFor(
|
||||
await whenSettled(page);
|
||||
await until(() => layerJsonSeen, {
|
||||
timeoutMs: 60000,
|
||||
intervalMs: 250,
|
||||
label: 'the Re:Earth layer.json fetch',
|
||||
});
|
||||
await until(
|
||||
async () => (await globeState()).provider === 'CesiumTerrainProvider',
|
||||
30000,
|
||||
'the keyless terrain provider',
|
||||
{
|
||||
timeoutMs: 30000,
|
||||
intervalMs: 250,
|
||||
label: 'the keyless terrain provider',
|
||||
},
|
||||
);
|
||||
|
||||
const summaries = [];
|
||||
@@ -246,16 +234,16 @@ try {
|
||||
// overwrite the first setView on the very next frame.
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
await setCamera(-97.7431, 30.2672, 40000);
|
||||
await sleep(1500);
|
||||
await whenSettled(page);
|
||||
const height = await cameraHeight();
|
||||
if (Math.abs(height - 40000) < 1000) break;
|
||||
}
|
||||
await waitFor(
|
||||
async () => (await globeState()).tilesLoaded,
|
||||
30000,
|
||||
'the 40 km view to settle',
|
||||
).catch(() => {});
|
||||
await sleep(2000);
|
||||
await until(async () => (await globeState()).tilesLoaded, {
|
||||
timeoutMs: 30000,
|
||||
intervalMs: 250,
|
||||
label: 'the 40 km view to settle',
|
||||
}).catch(() => {});
|
||||
await whenSettled(page);
|
||||
const settledHeight = await cameraHeight();
|
||||
if (Math.abs(settledHeight - 40000) > 1000) {
|
||||
throw new Error(
|
||||
@@ -358,5 +346,5 @@ try {
|
||||
);
|
||||
process.exitCode = failures.length ? 1 : 0;
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-traffic-baseline.mjs — Traffic Phase 0 causal-chain capture.
|
||||
*
|
||||
@@ -35,7 +37,7 @@
|
||||
* the latency plan.
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
@@ -53,20 +55,7 @@ const HEADFUL = argv.includes('--headful');
|
||||
const TIMEOUT_MS = Math.max(10_000, getNumberOpt('--timeout-ms', 120_000));
|
||||
const DEGRADED_DELAY_MS = Math.max(0, getNumberOpt('--degraded-delay-ms', 2500));
|
||||
|
||||
const CHROME_EXECUTABLE_CANDIDATES = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
].filter(Boolean);
|
||||
|
||||
function findChromeExecutable() {
|
||||
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
|
||||
try { if (fs.existsSync(candidate)) return candidate; } catch { /* ignore */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -95,16 +84,15 @@ function debugUrl(rawUrl) {
|
||||
|
||||
/** Position the camera and explicitly raise the boundaries the layer observes. */
|
||||
async function moveCamera(page, view) {
|
||||
return page.evaluate((v) => {
|
||||
return evaluateWithGeo(page, async (qaGeo, v) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const module = gev.dataManager.layers.get('traffic').module;
|
||||
const beforeLastUpdate = module.getStats().lastUpdate;
|
||||
const boundaryTime = performance.now();
|
||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
||||
const radians = Math.PI / 180;
|
||||
try { gev.viewer.camera.cancelFlight(); } catch { /* no active flight */ }
|
||||
gev.viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* no active flight */ }
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: v.lon * radians,
|
||||
latitude: v.lat * radians,
|
||||
height: v.height,
|
||||
@@ -119,8 +107,7 @@ async function moveCamera(page, view) {
|
||||
// explicitly makes the capture boundary deterministic across GPU speeds.
|
||||
// Pairing still flows through camera.changed's schedule-time anchor; the
|
||||
// synthetic moveEnd is emitted only as the same diagnostic used naturally.
|
||||
gev.viewer.camera.changed.raiseEvent();
|
||||
gev.viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
return { beforeLastUpdate, boundaryTime };
|
||||
}, view);
|
||||
}
|
||||
@@ -144,7 +131,7 @@ async function waitForTrafficCapture(page, beforeLastUpdate, boundaryTime) {
|
||||
boundaryTime,
|
||||
);
|
||||
// Let a final full-pass postRender land after `_fetching` flips false.
|
||||
await sleep(150);
|
||||
await whenFramesDrawn(page);
|
||||
}
|
||||
|
||||
/** Return serializable Traffic User Timing measures created after a boundary. */
|
||||
@@ -272,7 +259,7 @@ function rowsForCapture(capture) {
|
||||
/** Read the active WebGL renderer for the real-GPU/SwiftShader warning. */
|
||||
function readRenderer(page) {
|
||||
return page.evaluate(() => {
|
||||
const gl = window.__godsEyeView?.viewer?.scene?.context?._gl;
|
||||
const gl = window.__godsEyeView.engine.host.canvas.getContext('webgl2');
|
||||
if (!gl) return 'unknown';
|
||||
const extension = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return extension
|
||||
@@ -285,7 +272,7 @@ async function main() {
|
||||
const url = debugUrl(APP_URL);
|
||||
console.log('\nTraffic Phase 0 — causal-chain baseline capture');
|
||||
console.log(` App URL : ${url}`);
|
||||
console.log(` Browser mode : ${HEADFUL ? 'headful (real GPU expected)' : 'headless SwiftShader (relative-only)'}`);
|
||||
console.log(` Browser mode : headless (QA_ANGLE=${process.env.QA_ANGLE || 'metal'})`);
|
||||
console.log(` Degraded delay : ${DEGRADED_DELAY_MS ? `${DEGRADED_DELAY_MS} ms/request` : 'skipped'}`);
|
||||
|
||||
try {
|
||||
@@ -296,19 +283,9 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const executablePath = findChromeExecutable();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(executablePath ? { executablePath } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1440,900',
|
||||
...(HEADFUL ? [] : ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1440,900', ...(HEADFUL ? [] : [])],
|
||||
});
|
||||
|
||||
let activeProxyDelayMs = 0;
|
||||
@@ -343,19 +320,19 @@ async function main() {
|
||||
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
|
||||
// Disable all persisted overlays, park above traffic's activation ceiling,
|
||||
// then enable traffic without accidentally starting an unmeasured load.
|
||||
await page.evaluate(async (highView) => {
|
||||
await evaluateWithGeo(page, async (qaGeo, highView) => {
|
||||
const gev = window.__godsEyeView;
|
||||
await gev.dataManager.restoreEnabledLayerIds([]);
|
||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
||||
const radians = Math.PI / 180;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: highView.lon * radians,
|
||||
latitude: highView.lat * radians,
|
||||
height: 9000,
|
||||
@@ -363,10 +340,9 @@ async function main() {
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
await gev.dataManager.setEnabled('traffic', true);
|
||||
gev.viewer.camera.changed.raiseEvent();
|
||||
gev.viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
}, VIEWS.austin);
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
const renderer = await readRenderer(page);
|
||||
const softwareRenderer = /swiftshader|software/i.test(renderer);
|
||||
@@ -386,22 +362,19 @@ async function main() {
|
||||
|
||||
// Clear the last-bounds gate without clearing the module's road cache, then
|
||||
// revisit the byte-identical camera pose for a true client-cache hit.
|
||||
await page.evaluate((view) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
|
||||
await evaluateWithGeo(page, async (qaGeo, view) => {
|
||||
const radians = Math.PI / 180;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ellipsoid.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: view.lon * radians,
|
||||
latitude: view.lat * radians,
|
||||
height: 9000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
gev.viewer.camera.changed.raiseEvent();
|
||||
gev.viewer.camera.moveEnd.raiseEvent();
|
||||
window.__godsEyeView.engine.camera.moveEnd.raiseEvent();
|
||||
}, VIEWS.austin);
|
||||
await sleep(500);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
captures.push(await captureState(page, 'exact-viewport revisit', VIEWS.austin, (msValue) => {
|
||||
activeProxyDelayMs = msValue;
|
||||
@@ -467,7 +440,7 @@ async function main() {
|
||||
for (const error of consoleErrors.slice(0, 5)) console.log(` ${error}`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
import { whenSettled, whenTilesLoaded, whenFramesDrawn } from './qa/ready.mjs';
|
||||
/**
|
||||
* qa-traffic-jamviz-ab.mjs — A/B screenshot capture for the jam-viz
|
||||
* congestion prototypes (feat/traffic-jam-viz).
|
||||
@@ -18,7 +20,7 @@
|
||||
* Exits non-zero on harness failure (missing live mode, zero dots, etc.).
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -53,50 +55,18 @@ const VIEWS = [
|
||||
];
|
||||
const MODES = ['none', 'density', 'heatline', 'both'];
|
||||
|
||||
const CHROME_EXECUTABLE_CANDIDATES = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
|
||||
// Chrome: /Applications auto-updates underneath the harnesses, and its
|
||||
// software-GL behavior shifts across majors (system Chrome 150 blew the
|
||||
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
|
||||
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
|
||||
// pinned browser beats the newest one for regression harnesses.
|
||||
await puppeteer.executablePath().catch(() => null),
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
].filter(Boolean);
|
||||
const findChromeExecutable = () =>
|
||||
CHROME_EXECUTABLE_CANDIDATES.find((c) => { try { return fs.existsSync(c); } catch { return false; } }) || null;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** Same pattern as qa-cctv-v2: wait for the 3D tileset to settle for crisp shots. */
|
||||
function waitForTilesLoaded(page, timeoutMs) {
|
||||
return page.waitForFunction(
|
||||
() => {
|
||||
const prims = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let i = 0; i < prims.length; i++) {
|
||||
const p = prims.get(i);
|
||||
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
|
||||
return p.tilesLoaded === true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: timeoutMs }
|
||||
).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
/** Teleport to a view (cancelling any camera flight first). */
|
||||
function setView(page, v) {
|
||||
return page.evaluate((view) => {
|
||||
const gev = window.__godsEyeView;
|
||||
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
const ell = gev.viewer.scene.globe.ellipsoid;
|
||||
return evaluateWithGeo(page, async (qaGeo, view) => {
|
||||
try { window.__godsEyeView.engine.camera.cancelFlight(); } catch { /* no flight */ }
|
||||
const d2r = Math.PI / 180;
|
||||
gev.viewer.camera.setView({
|
||||
destination: ell.cartographicToCartesian({
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: view.lon * d2r, latitude: view.lat * d2r, height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
@@ -155,15 +125,9 @@ async function main() {
|
||||
}
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
|
||||
args: [
|
||||
'--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader',
|
||||
'--disable-dev-shm-usage', '--disable-web-security',
|
||||
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
|
||||
'--window-size=1600,900',
|
||||
],
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1600,900'],
|
||||
});
|
||||
|
||||
const manifest = [];
|
||||
@@ -174,11 +138,12 @@ async function main() {
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => window.__godsEyeView.dataManager.setEnabled('traffic', true));
|
||||
await sleep(1500);
|
||||
await whenFramesDrawn(page);
|
||||
|
||||
let firstView = true;
|
||||
for (const view of VIEWS) {
|
||||
@@ -203,7 +168,7 @@ async function main() {
|
||||
);
|
||||
await setView(page, view);
|
||||
const stats = await waitForFreshRender(page, since);
|
||||
await waitForTilesLoaded(page, firstView ? 30000 : 15000);
|
||||
await whenTilesLoaded(page, { timeoutMs: firstView ? 30000 : 15000 }).then(() => true).catch(() => false);
|
||||
await sleep(1400); // flow-recolor race + heat-line pulse mid-bright
|
||||
|
||||
const final = await page.evaluate(() => {
|
||||
@@ -249,7 +214,7 @@ async function main() {
|
||||
exitCode = 3;
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`\nShots + manifest.json → ${OUT_DIR}`);
|
||||
process.exit(exitCode);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: native primitive inspection independently measures the Cesium road paint.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/**
|
||||
* Traffic motion acceptance: real-GPU Puppeteer, stable identities, stationary
|
||||
* camera holds and timestamped CDP JPEGs encoded with ffmpeg. No tile mocks.
|
||||
@@ -9,10 +11,11 @@
|
||||
* --journey follows a neighborhood-to-city route with layer toggles, then
|
||||
* checks Capitol/greenbelt/parking/corridor views against a 1.5 s limit.
|
||||
*/
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import puppeteer from 'puppeteer';
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--help')) {
|
||||
console.log(
|
||||
@@ -37,11 +40,10 @@ fs.mkdirSync(root, { recursive: true });
|
||||
const revision = execFileSync('git', ['rev-parse', 'HEAD'], {
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 300000,
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--window-size=1280,800',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
@@ -84,12 +86,10 @@ try {
|
||||
u.searchParams.set('trafficRoads', mode);
|
||||
u.searchParams.set('trafficDebug', '1');
|
||||
await page.goto(u.href, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 120000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -99,7 +99,6 @@ try {
|
||||
g.styleManager.setDetection({ enabled: false });
|
||||
for (const [id] of g.dataManager.layers)
|
||||
g.dataManager.setEnabled(id, false);
|
||||
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
const identity = await page.evaluate(() => ({
|
||||
title: document.title,
|
||||
@@ -159,10 +158,10 @@ try {
|
||||
clock.style.cssText =
|
||||
'position:fixed;bottom:4px;left:38%;padding:3px 8px;background:#000c;color:#fff;font:12px monospace;z-index:999999;pointer-events:none';
|
||||
document.body.append(clock);
|
||||
viewer.camera.moveEnd.addEventListener(() =>
|
||||
window.__godsEyeView.engine.camera.moveEnd.addEventListener(() =>
|
||||
window.__motion.moveEnds.push(performance.now()),
|
||||
);
|
||||
scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
if (!window.__motion.active) return;
|
||||
discoverCollections();
|
||||
const now = performance.now(),
|
||||
@@ -174,7 +173,9 @@ try {
|
||||
: ` | camera stopped +${((now - window.__motion.holdStart) / 1000).toFixed(1)}s`);
|
||||
const visit = (id, roadId, point) => {
|
||||
if (!point.show) return;
|
||||
const s = scene.cartesianToCanvasCoordinates(point.position);
|
||||
const s = window.__godsEyeView.engine.projector.worldToWindow(
|
||||
point.position,
|
||||
);
|
||||
if (s && s.x >= 0 && s.y >= 0 && s.x <= 1280 && s.y <= 800)
|
||||
points.set(id, { x: s.x, y: s.y, roadId });
|
||||
};
|
||||
@@ -268,21 +269,19 @@ try {
|
||||
});
|
||||
}, baseline);
|
||||
const fly = (lat, lon, height, pitch, duration, heading = 20) =>
|
||||
page.evaluate(
|
||||
async (v) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, v) => {
|
||||
window.__motion.label = 'camera moving';
|
||||
window.__motion.holdStart = null;
|
||||
viewer.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
await new Promise((resolve) =>
|
||||
viewer.camera.flyTo({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian(
|
||||
{
|
||||
latitude: (v.lat * Math.PI) / 180,
|
||||
longitude: (v.lon * Math.PI) / 180,
|
||||
height: v.height,
|
||||
},
|
||||
),
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
latitude: (v.lat * Math.PI) / 180,
|
||||
longitude: (v.lon * Math.PI) / 180,
|
||||
height: v.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: (v.heading * Math.PI) / 180,
|
||||
pitch: (v.pitch * Math.PI) / 180,
|
||||
@@ -534,7 +533,7 @@ try {
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'results.json'),
|
||||
JSON.stringify(
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
import { whenSettled, until } from './qa/ready.mjs';
|
||||
// Deterministic city-navigation exercise; road/flow responses are fixtures.
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { trafficFixtureResponse } from './traffic-fixtures.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const url = args[args.indexOf('--url') + 1] || 'http://localhost:4173';
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 120000,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--use-gl=angle',
|
||||
'--use-angle=swiftshader',
|
||||
'--disable-background-timer-throttling',
|
||||
],
|
||||
});
|
||||
@@ -44,15 +42,15 @@ try {
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 60000,
|
||||
});
|
||||
await sleep(5000);
|
||||
await whenSettled(page);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.evaluate(async (withCctv) => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const C = await import('/node_modules/.vite/deps/cesium.js');
|
||||
for (const id of ['satellites', 'flights', 'military'])
|
||||
await dataManager.setEnabled(id, false);
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
window.__godsEyeView.engine.camera.cancelFlight();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: C.Cartesian3.fromDegrees(-97.744, 30.267, 3200),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
@@ -71,27 +69,28 @@ try {
|
||||
console.log(
|
||||
'Traffic timeout state',
|
||||
await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
return {
|
||||
stats: dataManager.layers.get('traffic').module.getStats(),
|
||||
camera: viewer.camera.positionCartographic,
|
||||
camera: window.__godsEyeView.engine.camera.pose().cartographic,
|
||||
};
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
await sleep(2500);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(async () => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
const C = await import('/node_modules/.vite/deps/cesium.js');
|
||||
viewer.camera.flyTo({
|
||||
window.__godsEyeView.engine.camera.flyTo({
|
||||
destination: C.Cartesian3.fromDegrees(-0.1276, 51.5072, 3200),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
duration: 4,
|
||||
});
|
||||
});
|
||||
const deadline = Date.now() + 45000;
|
||||
while (londonRequests < 2 && Date.now() < deadline) await sleep(500);
|
||||
await until(() => londonRequests >= 2, {
|
||||
timeoutMs: 45000,
|
||||
label: 'London retry',
|
||||
});
|
||||
assert.ok(
|
||||
londonRequests >= 2,
|
||||
'London must recover its failed request without a toggle',
|
||||
@@ -121,5 +120,5 @@ try {
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
import {
|
||||
moveCamera,
|
||||
createScreencast,
|
||||
@@ -20,12 +21,10 @@ const modes = modeArg ? [modeArg] : ['tomtom', 'osm', 'hybrid'];
|
||||
const out = `qa-shots/oblique-${baseline ? 'before' : 'after'}`;
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const result = { views: [], errors: [], forbidden: [], failures: [] };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
protocolTimeout: 180000,
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
@@ -59,12 +58,10 @@ try {
|
||||
const url = new URL(args.find((a) => /^https?:/.test(a)));
|
||||
url.searchParams.set('trafficRoads', mode);
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
|
||||
timeout: 120000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
@@ -74,18 +71,19 @@ try {
|
||||
for (const [id] of g.dataManager.layers)
|
||||
await g.dataManager.setEnabled(id, false);
|
||||
g.styleManager.setDetection({ enabled: false });
|
||||
g.viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
|
||||
});
|
||||
const snap = () =>
|
||||
page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
let upper = 0,
|
||||
lower = 0;
|
||||
dataManager.layers
|
||||
.get('traffic')
|
||||
.module.visitMotionDots((id, key, p) => {
|
||||
if (!p.show) return;
|
||||
const s = viewer.scene.cartesianToCanvasCoordinates(p.position);
|
||||
const s = window.__godsEyeView.engine.projector.worldToWindow(
|
||||
p.position,
|
||||
);
|
||||
if (s && s.x >= 240 && s.x <= 1040 && s.y >= 0 && s.y <= 800) {
|
||||
if (s.y < 400) upper++;
|
||||
else lower++;
|
||||
@@ -98,7 +96,7 @@ try {
|
||||
};
|
||||
});
|
||||
const settle = async () => {
|
||||
await sleep(700);
|
||||
await whenSettled(page);
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
@@ -110,7 +108,7 @@ try {
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.catch(() => {});
|
||||
await sleep(1200);
|
||||
await whenSettled(page);
|
||||
return snap();
|
||||
};
|
||||
for (const [name, lat, lon, height] of [
|
||||
@@ -186,20 +184,22 @@ try {
|
||||
mode === 'hybrid' ? await createScreencast(page, dir, { fps: 24 }) : null;
|
||||
await installFrameProbe(page);
|
||||
await page.evaluate(() => {
|
||||
const { viewer, dataManager } = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
window.__obliqueFrames = [];
|
||||
window.__obliqueMoving = true;
|
||||
viewer.scene.postRender.addEventListener(() => {
|
||||
window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
|
||||
const s = dataManager.layers.get('traffic').module.getStats();
|
||||
let upper = 0,
|
||||
lower = 0,
|
||||
near = 0;
|
||||
const camera = viewer.camera.positionWC;
|
||||
const camera = window.__godsEyeView.engine.camera.pose().position;
|
||||
dataManager.layers
|
||||
.get('traffic')
|
||||
.module.visitMotionDots((id, key, p) => {
|
||||
if (!p.show) return;
|
||||
const q = viewer.scene.cartesianToCanvasCoordinates(p.position);
|
||||
const q = window.__godsEyeView.engine.projector.worldToWindow(
|
||||
p.position,
|
||||
);
|
||||
if (q && q.x >= 240 && q.x <= 1040 && q.y >= 0 && q.y <= 800) {
|
||||
if (q.y < 400) upper++;
|
||||
else lower++;
|
||||
@@ -251,7 +251,7 @@ try {
|
||||
window.__obliqueMoving = false;
|
||||
});
|
||||
const panned = await settle();
|
||||
await sleep(1800);
|
||||
await whenSettled(page);
|
||||
const frames = await page.evaluate(() => window.__obliqueFrames);
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('traffic', false),
|
||||
@@ -304,7 +304,7 @@ try {
|
||||
}
|
||||
} finally {
|
||||
fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2));
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
if (
|
||||
!baseline &&
|
||||
|
||||
Reference in new issue
Block a user