Fix the review findings across the engines, layers, data and QA

Navara now matches Cesium where the review found it diverging: a flight
holds the camera inputs, camera.changed counts turns as Cesium does,
render requests made during a frame survive it, follow pinches zoom,
ground widths follow the pixel ratio and Cyber sonar reaches glyph caches
and models. On Cesium, a staged imagery swap keeps loading tiles, setView
replaces a running flight, and a cleared weather detail goes away.

The contract gains MoverSet.failed, maps.terrainRevision,
render.maxTextureSize and surface.sampleTerrainMostDetailed on every
engine, so layers retry failed models, key road heights by terrain,
size weather textures to the GPU and place Bhote Koshi cards on detailed
terrain.

Engine switches keep the live view, follows, focus, layer options and
cards: a pick hands over the exact pose, applies no address options and
pans no carried-over card; a history visit still applies its entry.

Layers put GhostMaps pins, cable landings, OSM Places and directions on
the measured surface without per-frame sampling, draw the Cockpit contact
dot, hide disabled CCTV on attach, and keep ALPR running outside secure
contexts. KML imports draw raised polygon fills, rotated and coloured
ground overlays and north-referenced icon headings, and report files
with nothing to draw.

QA scripts read the contract instead of Cesium internals (test:track is
109/109 again), wait for the camera to rest before clicking, and give
cold switches and parked engines realistic time. Docs follow the code.
This commit is contained in:
German Khokhlov committed 2026-11-09 21:34:42 +04:00
1 parent 69fbf5b38d
commit 0ef29781e5
276 files changed
+7217 -1517

No files matched your search

+7 -2
View File
@@ -12,6 +12,7 @@
import { existsSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { parse } from '@babel/parser';
import { vueScripts } from './vue-scripts.mjs';
const root = new URL('..', import.meta.url);
@@ -24,7 +25,7 @@ export function sourceFiles() {
.split('\n')
.filter(
(path) =>
/\.(?:js|mjs)$/.test(path) &&
/\.(?:js|mjs|vue)$/.test(path) &&
!/\.test\.m?js$/.test(path) &&
!path.startsWith('src/testSupport/') &&
!path.includes('/testing/') &&
@@ -75,7 +76,7 @@ function isReference(node, parent, key) {
parent.type === 'ImportSpecifier' ||
parent.type === 'ImportNamespaceSpecifier' ||
parent.type === 'ImportDefaultSpecifier' ||
parent.type === 'ExportSpecifier'
(parent.type === 'ExportSpecifier' && key !== 'local')
)
return false;
if (parent.type === 'LabeledStatement' || parent.type === 'BreakStatement')
@@ -89,6 +90,10 @@ function isReference(node, parent, key) {
* members and heritage run at load and stay not deferred.
*/
export function loadTimeReferences(code, path = '<module>') {
if (path.endsWith('.vue'))
return vueScripts(code, path).flatMap((script) =>
loadTimeReferences(script).map((reference) => ({ ...reference, path })),
);
const program = parse(code, {
sourceType: 'module',
plugins: ['classProperties', 'classStaticBlock', 'importAttributes'],
+24
View File
@@ -40,3 +40,27 @@ test('no shipped module reads Cesium while it loads', () => {
);
assert.deepEqual(found, []);
});
test('re-exporting imported Cesium bindings reads them while loading', () => {
assert.deepEqual(
reads("import { Color } from 'cesium'; export { Color as Tint };"),
['1:Color'],
);
assert.deepEqual(reads("import * as C from 'cesium'; export { C };"), [
'1:C',
]);
});
test('Vue scripts participate in the load-time check with original line numbers', () => {
assert.deepEqual(
loadTimeReferences(
`<template><span>Cesium</span></template>
<script>
import { Color } from 'cesium';
export const tint = Color.RED;
</script>`,
'src/View.vue',
).map(({ line, name }) => `${line}:${name}`),
['4:Color'],
);
});
+9 -26
View File
@@ -8,7 +8,7 @@ import { analyzeModule } from './module-analysis.mjs';
const builtins = new Set(
builtinModules.flatMap((name) => [name, `node:${name}`]),
);
const code = /\.[mc]?js$/;
const code = /\.(?:[mc]?js|vue)$/;
const entry = (file) =>
file === 'src/main.js' || file.startsWith('src/standalone/');
const tests = (file) =>
@@ -30,12 +30,6 @@ const globeEngine = (file) => /^src\/globe\/(?:cesium|navara)\//.test(file);
const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/;
// Navara renders through three.js and its postprocessing peer.
const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
// The modules that load Navara renderers, each through import().
const NAVARA_RENDERER_LOADERS = new Set([
'src/app/navaraMode.js',
'src/app/layers/renderers.js',
]);
const navaraLayerRenderer = (file) => /\/render\/navara[^/]*\.js$/.test(file);
const renderer = (file) =>
/^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
@@ -125,6 +119,7 @@ export function checkImportDirections(root) {
try {
analysis = analyzeModule(readFileSync(resolved, 'utf8'), {
dynamic: true,
filename: file,
});
} catch (error) {
report(file, error.message);
@@ -188,27 +183,12 @@ export function checkImportDirections(root) {
if (
to.startsWith('src/globe/navara/') &&
!file.startsWith('src/globe/navara/') &&
!analysis.dynamicImports.includes(specifier)
analysis.staticImports.includes(specifier)
)
report(
file,
`The Navara engine loads only through a dynamic import(): ${to}`,
);
// Like the engine, a layer's Navara renderer reaches the app only
// through an import() of the Navara mode (src/app/navaraMode.js) or
// of the layer manifest (src/app/layers/renderers.js).
if (
navaraLayerRenderer(to) &&
!navaraLayerRenderer(file) &&
!(
NAVARA_RENDERER_LOADERS.has(file) &&
analysis.dynamicImports.includes(specifier)
)
)
report(
file,
`A Navara layer renderer loads only through the Navara mode's dynamic import(): ${to}`,
);
if (file.startsWith('src/') && to.startsWith('server/'))
report(file, `Browser imports server: ${to}`);
if (
@@ -322,8 +302,8 @@ export function checkImportDirections(root) {
}
for (const file of files.filter(globeContract)) globe(file);
// Each engine graph stays on its own engine, through helpers too.
for (const start of files.filter(globeEngine)) {
const navara = start.startsWith('src/globe/navara/');
for (const owner of ['cesium', 'navara']) {
const navara = owner === 'navara';
const label = navara ? 'Navara globe engine' : 'Cesium globe engine';
const foreignPackage = navara ? cesiumPackage : navaraPackage;
const foreignEngine = navara ? 'src/globe/cesium/' : 'src/globe/navara/';
@@ -342,7 +322,10 @@ export function checkImportDirections(root) {
else visit(to);
}
};
visit(start);
for (const start of files.filter((file) =>
file.startsWith(`src/globe/${owner}/`),
))
visit(start);
}
// Common voice controls may use their DOM view, but never a protocol implementation.
const voiceSeen = new Set();
+9 -3
View File
@@ -1,4 +1,5 @@
import { parsers } from 'prettier/plugins/babel.mjs';
import { vueScripts } from './vue-scripts.mjs';
const browserGlobals = new Set([
'window',
@@ -19,9 +20,10 @@ const browserGlobals = new Set([
* Inspect literal module dependencies and browser-platform references without
* executing code. With `dynamic`, also list the import() specifiers apart.
*/
export function analyzeModule(source, { dynamic = false } = {}) {
export function analyzeModule(source, { dynamic = false, filename = '' } = {}) {
const imports = [];
const dynamicImports = [];
const staticImports = [];
const browser = new Set();
function walk(node, parent, field) {
if (!node || typeof node !== 'object') return;
@@ -41,6 +43,7 @@ export function analyzeModule(source, { dynamic = false } = {}) {
imports.push(node.source.value);
if (node.type === 'ImportExpression')
dynamicImports.push(node.source.value);
else staticImports.push(node.source.value);
}
if (node.type === 'CallExpression' && node.callee?.name === 'require')
throw new Error('Runtime modules must use ES imports');
@@ -76,11 +79,14 @@ export function analyzeModule(source, { dynamic = false } = {}) {
else if (value && typeof value === 'object') walk(value, node, key);
}
}
walk(parsers.babel.parse(source));
for (const script of filename.endsWith('.vue')
? vueScripts(source, filename)
: [source])
walk(parsers.babel.parse(script));
return {
imports,
browser: [...browser],
...(dynamic && { dynamicImports }),
...(dynamic && { dynamicImports, staticImports }),
};
}
+4
View File
@@ -2259,6 +2259,7 @@
"src/layers/shared/selectionKeeper.js",
"src/layers/shared/color.js",
"src/layers/submarineCables/render/display.js",
"src/layers/shared/render/surfacePoints.js",
"src/overlays/bindOverlayHost.js"
],
"external": ["cesium"]
@@ -3595,6 +3596,7 @@
"src/layers/earthquakes/render/display.js",
"src/layers/airQuality/render/display.js",
"src/layers/submarineCables/render/display.js",
"src/layers/shared/render/surfacePoints.js",
"src/layers/firms/render/sprites.js",
"src/layers/cyclones/render/display.js",
"src/layers/droneAirspace/render/display.js",
@@ -4352,6 +4354,7 @@
"src/layers/gnss/render/display.js",
"src/layers/firms/render/sprites.js",
"src/layers/submarineCables/render/display.js",
"src/layers/shared/render/surfacePoints.js",
"src/layers/perimeters/render/display.js",
"src/layers/droneAirspace/render/display.js",
"src/globe/ground.js",
@@ -4539,6 +4542,7 @@
"ui-share-restoration": {
"exports": ["./ui/share-restoration"],
"modules": [
"src/ui/layerNames.js",
"src/i18n/marker.js",
"src/data/installationFeedback.js",
"src/data/idSwitches.js",
+3 -2
View File
@@ -111,7 +111,7 @@ try {
// paths in one engine.ground corridor set, landing points and reference
// tips in mover sets, stems in engine.lines, labels on the world-overlay
// host. It owns no Cesium DataSource or Entity any more.
const counts = await page.evaluate((layerId) => {
const counts = await page.evaluate((layerId, legacy) => {
const gev = window.__godsEyeView;
const v = gev.viewer;
const engine = gev.engine;
@@ -126,6 +126,7 @@ try {
out.layerDataSources++;
out.layerEntities += ds.entities.values.length;
}
if (legacy) return out;
for (const set of engine.ground.list()) {
if (set.id !== `${layerId}-paths`) continue;
out.pathSets++;
@@ -146,7 +147,7 @@ try {
const stats = gev.dataManager.layers.get(layerId)?.module?.getStats?.() || {};
out.referenceLabelCount = stats.referenceLabelCount ?? null;
return out;
}, LAYER_ID);
}, LAYER_ID, legacy);
report(control ? 'counts (control, cables OFF)' : 'counts with cables ON', counts);
// ── a. per-frame scene.render cost over a ~10 s driven orbit ─────────
+4 -3
View File
@@ -1485,8 +1485,9 @@ async function main() {
const scene = viewer.scene;
const canvas = scene.canvas;
const rect = canvas.getBoundingClientRect();
const { hitTestWorldOverlay } =
(await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
const { hitTest } = (
await import('/src/overlays/worldOverlay.js')
).worldOverlayFor(window.__godsEyeView.engine);
const candidates = [
[0.55, 0.72],
[0.45, 0.72],
@@ -1508,7 +1509,7 @@ async function main() {
const x = Math.round(canvas.clientWidth * fx);
const y = Math.round(canvas.clientHeight * fy);
const picked = scene.pick({ x, y });
const card = hitTestWorldOverlay(x, y, { sourceId: 'cctv' });
const card = hitTest(x, y, { sourceId: 'cctv' });
const top = document.elementFromPoint(rect.left + x, rect.top + y);
if (
resolveId(picked) === null &&
+1 -2
View File
@@ -36,8 +36,7 @@ const check = (name, passed, detail = '') => {
const browser = await puppeteer.launch({
headless: process.env.QA_HEADFUL === '1' ? false : true,
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
executablePath: chromeExecutablePath(),
args: [
'--no-sandbox',
'--disable-background-timer-throttling',
+3 -1
View File
@@ -65,7 +65,9 @@ await strict.run({}, async (browser) => {
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.querySelector('#globe-engine')?.value === kind &&
!document.querySelector('#globe-engine').disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 90000 },
kind,
+18 -31
View File
@@ -2,7 +2,12 @@ import { assertKeylessPreview } from './qa-strict-harness.mjs';
import assert from 'node:assert/strict';
import { installAlprFixture, cameras } from './fixtures/layers/alpr.mjs';
import { firmsFixture, FIRMS_VALID_COUNT } from './fixtures/layers/firms.mjs';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
clickEmptyGround,
createStrict,
localPreview,
waitForCameraAtRest,
} from './qa-strict-harness.mjs';
const url = localPreview(process.argv[2]),
fire = process.argv.includes('--fires'),
id = fire ? 'local-firms' : 'alpr-cameras';
@@ -113,36 +118,14 @@ await strict.run({}, async (b) => {
);
// A click on empty ground (nothing picked there, nothing covering it).
const clickEmpty = async () => {
const handle = await p.waitForFunction(
() => {
const { engine } = window.__godsEyeView;
const canvas = engine.host.canvas;
const box = canvas.getBoundingClientRect();
for (let y = 240; y < 600; y += 9)
for (let x = 440; x < 840; x += 9) {
if (
!canvas.contains(
document.elementFromPoint(box.left + x, box.top + y),
)
)
continue;
let free = true;
for (const [dx, dy] of [
[0, 0],
[6, 0],
[-6, 0],
[0, 6],
[0, -6],
])
if (engine.picker.pick({ x: x + dx, y: y + dy })) free = false;
if (free) return { x: box.left + x, y: box.top + y };
}
return false;
},
{ timeout: 15000 },
);
const at = await handle.jsonValue();
await p.mouse.click(at.x, at.y);
await clickEmptyGround(p, {
left: 440,
top: 240,
right: 840,
bottom: 600,
step: 9,
radius: 6,
});
await p.waitForFunction(
(fire) =>
fire
@@ -165,6 +148,10 @@ await strict.run({}, async (b) => {
xy,
);
await new Promise((r) => setTimeout(r, 1600));
// The selection flies the camera to the record (1.9 s, longer on a busy
// machine); a switch before it lands opens the other engine on a view
// between the two, where the record can be off screen.
await waitForCameraAtRest(p);
const expectedId = fire
? await p.evaluate(() => window.qaGround.getSelectedInfo().id)
: xy.id;
+60 -51
View File
@@ -9,6 +9,7 @@ import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import { fromDegrees } from '../src/geo/cartesian3.js';
import { watchBrowserPage, redact } from './qa-browserEvidence.mjs';
const urlIndex = process.argv.indexOf('--url');
@@ -150,23 +151,25 @@ async function open(mode = 'normal') {
'0'),
{ timeout: 60000 },
);
await page.evaluate(async () => {
const { dataManager, engine } = window.__godsEyeView;
await window.__godsEyeView.styleManager.initialRestorePromise;
window.__godsEyeView.styleManager.orbitController.stop();
for (const [id, layer] of dataManager.layers) {
if (layer.enabled)
await dataManager.setEnabled(id, false, { origin: 'user' });
}
const geo = await import('/src/geo/index.js');
engine.camera.cancelFlight();
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(-97.7449, 30.2701, 3000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await dataManager.setEnabled('bikeshare', true, { origin: 'user' });
window.__godsEyeView.requestRender('gbfs-name-qa');
});
await page.evaluate(
async (destination) => {
const { dataManager, engine } = window.__godsEyeView;
await window.__godsEyeView.styleManager.initialRestorePromise;
window.__godsEyeView.styleManager.orbitController.stop();
for (const [id, layer] of dataManager.layers) {
if (layer.enabled)
await dataManager.setEnabled(id, false, { origin: 'user' });
}
engine.camera.cancelFlight();
engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await dataManager.setEnabled('bikeshare', true, { origin: 'user' });
window.__godsEyeView.requestRender('gbfs-name-qa');
},
fromDegrees(-97.7449, 30.2701, 3000),
);
return { page, errors };
}
@@ -220,15 +223,17 @@ async function w2Checks(page) {
['close', 3000],
['global', 100000],
]) {
await page.evaluate(async (height) => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('bikeshare', false);
const geo = await import('/src/geo/index.js');
app.engine.camera.setView({
destination: geo.cartesian3.fromDegrees(-97.7449, 30.2701, height),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, height);
await page.evaluate(
async (destination) => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('bikeshare', false);
app.engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
},
fromDegrees(-97.7449, 30.2701, height),
);
await settledTiles(page);
await nextFrame(page);
const off = await page.screenshot({
@@ -304,15 +309,17 @@ async function w2Checks(page) {
for (const element of document.querySelectorAll('.gbfs-raster-hidden'))
element.classList.remove('gbfs-raster-hidden');
});
await page.evaluate(async () => {
const app = window.__godsEyeView;
const geo = await import('/src/geo/index.js');
app.engine.camera.setView({
destination: geo.cartesian3.fromDegrees(-97.7449, 30.2701, 3000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await app.dataManager.setEnabled('bikeshare', false);
});
await page.evaluate(
async (destination) => {
const app = window.__godsEyeView;
app.engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await app.dataManager.setEnabled('bikeshare', false);
},
fromDegrees(-97.7449, 30.2701, 3000),
);
for (let i = 0; i < 10; i++) {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('bikeshare', true),
@@ -520,22 +527,24 @@ try {
);
}
await page.setViewport({ width: 390, height: 844 });
await page.evaluate(async () => {
window.__qaPainted = [];
const app = window.__godsEyeView;
// Use the supported clean view to inspect the mobile card clear of panels.
app.styleManager.toggleCleanView(true);
const geo = await import('/src/geo/index.js');
app.engine.camera.cancelFlight();
app.engine.camera.setView({
destination: geo.cartesian3.fromDegrees(-97.7449, 30.2701, 3000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
app.dataManager.layers
.get('bikeshare')
.module._selectBikeshareStationForTest('austin-capmetro:english');
app.requestRender('gbfs-name-mobile-qa');
});
await page.evaluate(
async (destination) => {
window.__qaPainted = [];
const app = window.__godsEyeView;
// Use the supported clean view to inspect the mobile card clear of panels.
app.styleManager.toggleCleanView(true);
app.engine.camera.cancelFlight();
app.engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
app.dataManager.layers
.get('bikeshare')
.module._selectBikeshareStationForTest('austin-capmetro:english');
app.requestRender('gbfs-name-mobile-qa');
},
fromDegrees(-97.7449, 30.2701, 3000),
);
await page.waitForFunction(
() => window.__qaPainted.includes('QA English Square'),
{ timeout: 10000 },
+15 -31
View File
@@ -2,9 +2,11 @@ import { fromDegrees } from '../src/geo/cartesian3.js';
import sharp from 'sharp';
import assert from 'node:assert/strict';
import {
clickEmptyGround,
createStrict,
localPreview,
openPage,
waitForCameraAtRest,
waitForEngine,
} from './qa-strict-harness.mjs';
@@ -400,39 +402,19 @@ await strict.run({}, async (b) => {
// Click-picking on the running engine: clear the selection with a click on
// empty water, then select the vessel with a click on its chevron.
const pickVessel = async () => {
// The last selection flies the camera to the vessel, and a click does
// not cut that flight short: click once it has landed.
await waitForCameraAtRest(p);
// Empty ground: a point in the middle of the view where the engine
// picks nothing and nothing covers the canvas.
const empty = await p
.waitForFunction(
() => {
const { engine } = window.__godsEyeView;
const canvas = engine.host.canvas;
const box = canvas.getBoundingClientRect();
for (let y = 220; y < 560; y += 11)
for (let x = 300; x < 720; x += 11) {
if (
!canvas.contains(
document.elementFromPoint(box.left + x, box.top + y),
)
)
continue;
let free = true;
for (const [dx, dy] of [
[0, 0],
[10, 0],
[-10, 0],
[0, 10],
[0, -10],
])
if (engine.picker.pick({ x: x + dx, y: y + dy })) free = false;
if (free) return { x: box.left + x, y: box.top + y };
}
return false;
},
{ timeout: 15000 },
)
.then((handle) => handle.jsonValue());
await p.mouse.click(empty.x, empty.y);
await clickEmptyGround(p, {
left: 300,
top: 220,
right: 720,
bottom: 560,
step: 11,
radius: 10,
});
await p.waitForFunction(
() => !window.qaVesselEntry.module.getSelectedInfo(),
);
@@ -447,6 +429,8 @@ await strict.run({}, async (b) => {
'ais-live-vessels',
'a click on the chevron selects the vessel',
);
// The next switch starts from the landed view.
await waitForCameraAtRest(p);
};
await click(-79.38, 43.65);
await p.waitForFunction(
+11 -13
View File
@@ -10,8 +10,9 @@ export async function checkInstallationPan(page, dir) {
);
await sleep(3000);
await page.evaluate(async () => {
const { getOverlayPaintRect } =
(await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
const { getPaintRect } = (
await import('/src/overlays/worldOverlay.js')
).worldOverlayFor(window.__godsEyeView.engine);
const g = window.__godsEyeView;
const installations = g.dataManager.layers.get(
'military-installations',
@@ -28,10 +29,10 @@ export async function checkInstallationPan(page, dir) {
const sites = [],
centers = [];
installations.visitNamedMarkers((id) => {
if (getOverlayPaintRect('military-installations', id)) sites.push(id);
if (getPaintRect('military-installations', id)) sites.push(id);
});
for (const id of centerIds)
if (getOverlayPaintRect('local-datacenters', id)) centers.push(id);
if (getPaintRect('local-datacenters', id)) centers.push(id);
window.__polishPanFrames.push({ t: performance.now(), sites, centers });
});
});
@@ -187,8 +188,9 @@ export async function checkInstallationSelection(
window.__polishSelectionFrames = [];
const { getSelectedEntityContext } =
await import('/src/data/contextStore.js');
const { getOverlayPaintRect, getWorldOverlayDiagnostics } =
(await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
const { getPaintRect, getDiagnostics } = (
await import('/src/overlays/worldOverlay.js')
).worldOverlayFor(window.__godsEyeView.engine);
// The selected site's fill is one engine.ground polygon set
// (render/display.js SELECTION_FILL_SET_ID), an item per footprint
// part; the layer owns no Cesium entities or ground primitives.
@@ -225,17 +227,13 @@ export async function checkInstallationSelection(
g.viewer.scene.postRender.addEventListener(() => {
window.__polishSelectionFrames.push({
paints: window.__polishTitlePaints.splice(0).length,
own: Boolean(
getOverlayPaintRect('military-installations', record.id),
),
own: Boolean(getPaintRect('military-installations', record.id)),
tracked: Boolean(
getOverlayPaintRect('tracked', `installations:${record.id}`),
getPaintRect('tracked', `installations:${record.id}`),
),
selected: getSelectedEntityContext()?.id === record.id,
total:
getWorldOverlayDiagnostics().paintedBySource[
'military-installations'
] || 0,
getDiagnostics().paintedBySource['military-installations'] || 0,
});
});
return {
+2 -2
View File
@@ -7,8 +7,8 @@
* node scripts/qa-label-readability.mjs --url http://localhost:4244 --tag after [--engine=cesium|navara]
*
* Engine-neutral: camera, tiles and layers go through the globe engine and the
* QA probe; an engine without the flights/satellites layers (Navara today)
* shoots the scenes without a contact field and says so.
* QA probe; an engine without the flights/satellites layers shoots the
* scenes without a contact field and says so.
*
* Every scene pins its own camera pose AND substitutes a deterministic
* synthetic contact field for the live pollers, so a `before` run and an
+2 -2
View File
@@ -7,8 +7,8 @@
*
* Engine-neutral: the camera and frames go through the globe engine and the
* layers through the QA probe (window.__godsEyeView.layers). On an engine
* without the flights, military and satellites layers (Navara today) the
* field checks are SKIPPED and the settings checks still run.
* without the flights, military and satellites layers the field checks are
* SKIPPED and the settings checks still run.
*
* The harness replaces only the three detection read APIs in the loaded page.
* It does not enable their network pollers or mutate repository data. The field
+3 -1
View File
@@ -111,7 +111,9 @@ await strict.run({}, async (browser) => {
page.waitForFunction(
(engine) =>
window.__godsEyeView?.engine?.kind === engine &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
document.querySelector('#loading-screen.hidden') &&
!document.querySelector('.engine-switch-frame'),
{ timeout: 90000 },
+13 -13
View File
@@ -152,14 +152,15 @@ try {
await settle();
const before = await stats();
const overlaps = await page.evaluate(async () => {
const { getOverlayPaintRect } =
(await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
const { getPaintRect } = (
await import('/src/overlays/worldOverlay.js')
).worldOverlayFor(window.__godsEyeView.engine);
const layer = window.__godsEyeView.dataManager.layers.get(
'military-installations',
).module;
const rects = [];
layer.visitNamedMarkers((id) => {
const rect = getOverlayPaintRect('military-installations', id);
const rect = getPaintRect('military-installations', id);
if (rect) rects.push(rect);
});
let overlaps = 0;
@@ -223,9 +224,7 @@ try {
opacityProperty: true,
}),
labels:
host.getWorldOverlayDiagnostics().paintedBySource[
'military-installations'
] || 0,
host.getDiagnostics().paintedBySource['military-installations'] || 0,
};
});
assert.ok(
@@ -244,10 +243,10 @@ try {
),
);
result.disabled = await page.evaluate(async () => {
const host = (await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
return host.getWorldOverlayDiagnostics().entriesBySource[
'military-installations'
];
const host = (
await import('/src/overlays/worldOverlay.js')
).worldOverlayFor(window.__godsEyeView.engine);
return host.getDiagnostics().entriesBySource['military-installations'];
});
assert.equal(result.disabled, 0);
await page.evaluate(() =>
@@ -266,8 +265,9 @@ try {
}
await page.evaluate(async () => {
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
const { getOverlayPaintRect } =
(await import('/src/overlays/worldOverlay.js')).worldOverlayFor(window.__godsEyeView.engine);
const { getPaintRect } = (
await import('/src/overlays/worldOverlay.js')
).worldOverlayFor(window.__godsEyeView.engine);
const g = window.__godsEyeView;
const layer = g.dataManager.layers.get('military-installations').module;
const overlay = document.createElement('div');
@@ -284,7 +284,7 @@ try {
labels = [],
rects = [];
layer.visitNamedMarkers((id, point) => {
const rect = getOverlayPaintRect('military-installations', id);
const rect = getPaintRect('military-installations', id);
if (rect) rects.push(rect);
const p = C.SceneTransforms.worldToWindowCoordinates(
g.viewer.scene,
+3 -1
View File
@@ -48,7 +48,9 @@ await strict.run(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
window.__gevDrawTool?.attached &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 120000 },
+6 -4
View File
@@ -2,8 +2,8 @@
// UTILITIES ▸ Import on Navara under the strict network policy: GeoJSON,
// KML, KMZ (a packed icon and a ground overlay) and GPX through the real
// file input, each one row; Navara draws their shapes in the row colour and
// their pins, counts what it cannot draw (the KMZ ground overlay) and says
// so in the hint; hide, fly-to and remove work on the engine; the files
// their pins and packed ground overlay; unsupported content is counted
// in the hint; hide, fly-to and remove work on the engine; the files
// survive a switch to Cesium and back and draw on each engine. No file makes
// the page request anything it names (the strict policy fails any request
// outside the preview and the free tile cache).
@@ -152,7 +152,9 @@ await strict.run(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
window.__gevGeoImport?.diagnostics().attached &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 120000 },
@@ -171,7 +173,7 @@ await strict.run(
rows: document.querySelectorAll('#geo-import-list .geo-import-row')
.length,
hint: document.getElementById('geo-import-hint')?.textContent || '',
// Navara draws the pins as mover sets; Cesium as entities (none).
// Both engines draw imported pins through mover sets.
pins: window.__godsEyeView.engine.movers
.list()
.filter(({ id }) => id.startsWith('geo-import-'))
+9 -9
View File
@@ -18,7 +18,7 @@
* node scripts/qa-overlay-baseline.mjs --engine=navara
*
* Engine-neutral: the camera and frames go through the globe engine and the
* layers through the QA probe (window.__godsEyeView.layers, dev server only).
* layers through the public engine and data manager surfaces.
* A scene whose layers the engine does not have is SKIPPED. The inventory
* counts the globe contract's sets (movers, lines, ground, imagery) on both
* engines; on Cesium it also counts what is left of Entities, LabelGraphics
@@ -29,6 +29,7 @@ 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 DEFAULT_URL = 'http://localhost:4176';
@@ -362,7 +363,7 @@ async function installPageInstrumentation(page) {
async function waitForApp(page) {
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForFunction(
() => Boolean(window.__godsEyeView?.layers && window.__godsEyeView?.dataManager),
() => Boolean(window.__godsEyeView?.engine && window.__godsEyeView?.dataManager),
{ timeout: 60_000 },
);
await page.evaluate(() => {
@@ -375,17 +376,16 @@ async function waitForApp(page) {
async function setCamera(page, camera) {
if (!camera) return;
await page.evaluate(async ([lon, lat, height, heading, pitch]) => {
await page.evaluate(([destination, heading, pitch]) => {
const { engine, viewer } = window.__godsEyeView;
const geo = await import('/src/geo/index.js');
engine.camera.cancelFlight();
viewer?.scene.tweens?.removeAll?.();
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(lon, lat, height),
destination,
orientation: { heading, pitch, roll: 0 },
});
engine.render.request('qa-overlay-baseline');
}, camera);
}, [fromDegrees(...camera.slice(0, 3)), ...camera.slice(3)]);
}
async function readLayerState(page, layerId) {
@@ -646,7 +646,7 @@ async function prepareTrackedFlight(page, cockpit) {
});
if (!tracked.ok) return `Civil tracking unavailable (${tracked.reason})`;
try {
await page.waitForFunction(() => Boolean(window.__godsEyeView.layers.following()), { timeout: 10_000 });
await page.waitForFunction(() => Boolean(window.__godsEyeView.engine.camera.following()), { timeout: 10_000 });
} catch {
return 'Civil tracking did not create a tracked entity';
}
@@ -679,7 +679,7 @@ async function prepareMission(page) {
});
if (!selected) return 'Mission roster did not expose a selectable mission';
try {
await page.waitForFunction(() => Boolean(window.__godsEyeView.layers.drawn('rocket-launches')?.selected), { timeout: 10_000 });
await page.waitForFunction(() => Boolean(window.__godsEyeView.dataManager.layers.get('rocket-launches')?.module?.qaDrawn?.()?.selected), { timeout: 10_000 });
} catch {
return 'Mission selection did not reach the layer probe';
}
@@ -689,7 +689,7 @@ async function prepareMission(page) {
async function prepareScene(page, scene) {
const missing = await page.evaluate(
(ids) => ids.filter((id) => !window.__godsEyeView.layers.available(id)),
(ids) => ids.filter((id) => !window.__godsEyeView.dataManager.layers.has(id)),
scene.layers,
);
if (missing.length)
+6 -4
View File
@@ -71,7 +71,7 @@
* Usage: node scripts/qa-perf.mjs [--url http://localhost:4173] [--engine=cesium|navara]
* Engine-neutral: the camera, frames and layers go through the globe engine
* and the QA probe (window.__godsEyeView.layers); on an engine without a
* layer (Navara today) that layer's checks are SKIPPED, not failed.
* layer that layer's checks are SKIPPED, not failed.
* Requires a running dev server. Headless; flags disable occlusion
* throttling so rAF cadence is trustworthy (hidden-pane gotcha).
*/
@@ -338,14 +338,16 @@ try {
await page.evaluate(() => { window.__godsEyeView.styleManager.setStyle('retro'); });
await new Promise((r) => setTimeout(r, 900)); // crossfade + first ticks
const dAnim = await diag();
// The animated styles are Cesium post-process stages; Navara draws its own
// looks and has no style animation (a known gap, not a regression).
// This assertion checks the Cesium style-anim render hold.
if (engineKind === 'cesium') {
check('animated style takes the style-anim hold (continuous)', dAnim?.mode === 'continuous' && dAnim.holds.includes('style-anim'), dAnim);
const animFrames = await countFrames(2_000);
check('animated style keeps frames flowing (≥50% rAF)', animFrames.renders >= animFrames.rafs * 0.5, animFrames);
} else {
skip('animated style takes the style-anim hold and keeps frames flowing', `the visual styles are Cesium post stages; ${engineKind} has none`);
skip(
'animated style takes the style-anim hold and keeps frames flowing',
`this check measures the Cesium style-anim hold, not ${engineKind} animation`,
);
}
// Returning to normal keeps detection ON by design (it is the default, and
// detection persists across style switches). Since 2026-08-22 detection holds
+3 -1
View File
@@ -67,7 +67,9 @@ await strict.run({}, async (b) => {
await p.waitForFunction(
() =>
(window.__godsEyeView &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden')) ||
document.querySelector('.engine-switch-retry'),
+6 -2
View File
@@ -229,7 +229,9 @@ await strict.run({}, async (b) => {
window.__qaSlow.at &&
!window.__qaSlow.coverGone &&
!document.querySelector('.engine-switch-frame') &&
!document.querySelector('#globe-engine')?.disabled
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
)
) {
window.__qaSlow.coverGone = performance.now();
observer.disconnect();
@@ -245,7 +247,9 @@ await strict.run({}, async (b) => {
await p.waitForFunction(
(target) =>
window.__godsEyeView?.engine?.kind === target &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
!document.querySelector('.engine-switch-frame'),
{ polling: 200, timeout: 60000 },
target,
+80 -9
View File
@@ -440,10 +440,11 @@ export async function pickEngine(
?.getAttribute('aria-expanded') === 'true',
);
}
await page.waitForSelector('#globe-engine:not([disabled])', {
visible: true,
timeout: 90000,
});
// The picker stays focusable while busy: aria-busy marks a switch.
await page.waitForSelector(
'#globe-engine:not([disabled]):not([aria-busy="true"])',
{ visible: true, timeout: 90000 },
);
const from = await page.$eval('#globe-engine', (el) => el.value);
assert.notEqual(from, kind, `the picker already shows ${kind}`);
// The key typed below is `kind`'s first letter: it selects the next option
@@ -479,14 +480,16 @@ export async function pickEngine(
);
window.__qaPickObserver?.disconnect();
window.__qaPickObserver = new MutationObserver(() => {
const disabled = document.querySelector('#globe-engine')?.disabled;
const disabled = document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
);
if (disabled) window.__qaPick.busy = true;
else if (window.__qaPick.busy && window.__qaPick.idleAt == null)
window.__qaPick.idleAt = performance.now();
});
window.__qaPickObserver.observe(document.body, {
attributes: true,
attributeFilter: ['disabled'],
attributeFilter: ['disabled', 'aria-busy'],
subtree: true,
});
const describe = (hit) =>
@@ -523,8 +526,10 @@ export async function pickEngine(
// Whatever the person does next, right after the key (no round trip to
// the page in between).
if (afterInput) await afterInput();
// The change starts a cold switch at once: Cesium.js then runs as one
// task, seconds long under CPU throttling, before the page answers.
await page.waitForFunction(() => window.__qaPick?.changed, {
timeout: 5000,
timeout: 30000,
polling: 50,
});
// Headless Chrome opens no menu for a click on the select: the key
@@ -538,7 +543,9 @@ export async function pickEngine(
await page.waitForFunction(
() =>
(window.__qaPick.busy &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
document.querySelector('#loading-screen.hidden') &&
!document.querySelector('.engine-switch-frame')) ||
document.querySelector('.engine-switch-retry'),
@@ -572,7 +579,9 @@ export async function waitForEngine(page, kind, { timeout = 120000 } = {}) {
window.__godsEyeView?.engine?.kind ===
(expected === 'cesium' ? 'cesium' : 'navara') &&
document.querySelector('#globe-engine')?.value === expected &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout },
@@ -837,3 +846,65 @@ export function createStrict({
};
return strict;
}
/** Click uncovered canvas where the engine picks nothing within the radius. */
export async function clickEmptyGround(
page,
{ left, top, right, bottom, step = 9, radius = 6 },
) {
const handle = await page.waitForFunction(
({ left, top, right, bottom, step, radius }) => {
const { engine } = window.__godsEyeView;
const canvas = engine.host.canvas;
const box = canvas.getBoundingClientRect();
for (let y = top; y < bottom; y += step)
for (let x = left; x < right; x += step) {
if (
!canvas.contains(
document.elementFromPoint(box.left + x, box.top + y),
)
)
continue;
const free = [
[0, 0],
[radius, 0],
[-radius, 0],
[0, radius],
[0, -radius],
].every(([dx, dy]) => !engine.picker.pick({ x: x + dx, y: y + dy }));
if (free) return { x: box.left + x, y: box.top + y };
}
return false;
},
{ timeout: 15000 },
{ left, top, right, bottom, step, radius },
);
try {
const at = await handle.jsonValue();
await page.mouse.click(at.x, at.y);
} finally {
await handle.dispose();
}
}
/**
* Wait until no flight holds the camera and it stays still. A selection's
* camera transfer is a flight; on both engines a flight turns the camera
* inputs off, and a click does not cut it short.
*/
export async function waitForCameraAtRest(page, { timeout = 15000 } = {}) {
await page.evaluate(() => {
window.__qaRestPose = null;
});
await page.waitForFunction(
() => {
const { engine } = window.__godsEyeView;
const { x, y, z } = engine.camera.pose().position;
const pose = [x, y, z].map((value) => value.toFixed(2)).join();
const still = window.__qaRestPose === pose;
window.__qaRestPose = pose;
return still && engine.input.cameraInputsEnabled();
},
{ timeout, polling: 100 },
);
}
+8 -2
View File
@@ -52,7 +52,11 @@ await strict.run({}, async (b) => {
window.__qaHidden.shown.push({
state: document.visibilityState,
at: performance.now(),
busy: !!document.querySelector('#globe-engine')?.disabled,
busy: Boolean(
document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
),
),
}),
);
}, kind);
@@ -69,7 +73,9 @@ await strict.run({}, async (b) => {
await p.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
) &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ polling: 250, timeout: 60000 },
+9 -7
View File
@@ -1,4 +1,4 @@
// Contract: a parked globe is released after ?parkedMs= (3 s here; three
// Contract: a parked globe is released after ?parkedMs= (8 s here; three
// minutes by default). A switch back in time is warm (the same engine
// object); after the delay the parked engine's WebGL context is lost and a
// switch back builds a new engine on the same session that draws a
@@ -21,7 +21,8 @@ import {
startSession,
} from './qa-strict-session.mjs';
const PARKED_MS = 3000;
// Room for a cold switch on a busy machine (2-3 s) before the switch back.
const PARKED_MS = 8000;
const base = localPreview(process.argv[2]);
const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
const strict = createStrict({ origin: base.origin, layers: [] });
@@ -41,8 +42,9 @@ await strict.run({}, async (b) => {
console.log('PASS', name);
};
const pick = async (kind) => {
await strict.pick(p, kind);
const { at } = await strict.pick(p, kind);
await waitForEngine(p, kind);
return at;
};
// The running engine is the one remembered under its kind (warm), and
// the context of `kind`'s remembered engine is lost or not.
@@ -77,17 +79,17 @@ await strict.run({}, async (b) => {
await pick(parked);
await rememberEngine(p);
}
await pick(running);
const parkedAt = await pick(running);
await rememberEngine(p);
// A switch back in time is warm.
await wait(PARKED_MS / 3);
await pick(parked);
await wait(1000);
const backAt = await pick(parked);
const early = await engines(parked);
await assertSession(p, parked, `a quick switch back to ${parked}`);
contract(
`a switch back to ${parked} within the delay reuses the parked engine`,
early.warm && early.lost === false,
early,
{ ...early, parkedMs: Math.round(backAt - parkedAt) },
);
// Parked past the delay: released, and the next switch back is cold.
await pick(running);
+5 -1
View File
@@ -89,7 +89,11 @@ await strict.run({}, async (b) => {
window.__qaSteps.push({
step,
engine: window.__godsEyeView?.engine?.kind ?? null,
busy: !!document.querySelector('#globe-engine')?.disabled,
busy: Boolean(
document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
),
),
});
window.__qaMark = mark;
});
+30 -1
View File
@@ -1,7 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { quantile, slope } from './qa-strict-session.mjs';
import { strictArguments } from './qa-strict-harness.mjs';
import { strictArguments, waitForCameraAtRest } from './qa-strict-harness.mjs';
test('slope is the least-squares growth per step', () => {
assert.equal(slope([[0, 5]]), 0);
@@ -45,3 +45,32 @@ test('the runner hands each scenario its layers and its manifest path', () => {
evidence: process.env.QA_STRICT_EVIDENCE ?? null,
});
});
test('the camera is at rest once a flight has landed and it stays still', async () => {
// A selection's flight holds the inputs off; once they are on, the camera
// still moves one more sample before it stays put.
const poses = [0, 400, 900, 900, 1250, 1250, 1250];
const inputs = [false, false, false, false, true, true, true];
let frame = 0;
const engine = {
camera: { pose: () => ({ position: { x: poses[frame], y: 0, z: 6.4e6 } }) },
input: { cameraInputsEnabled: () => inputs[frame] },
};
const previous = globalThis.window;
globalThis.window = { __godsEyeView: { engine } };
const page = {
evaluate: async (fn) => fn(),
async waitForFunction(fn) {
for (; frame < poses.length; frame++) if (fn()) return;
throw new Error('timed out');
},
};
try {
await waitForCameraAtRest(page);
// Not while the flight holds the inputs (sample 3 repeats a pose), nor
// while the camera moves (sample 4).
assert.equal(frame, 5);
} finally {
globalThis.window = previous;
}
});
+8 -6
View File
@@ -121,12 +121,14 @@ try {
// place every frame; it never empties and refills a collection).
// --baseline records the pre-contract build, whose PointPrimitive
// collections sat in scene.primitives and were rebuilt by removeAll.
const engine = window.__godsEyeView.engine;
const createMoverSet = engine.movers.createSet.bind(engine.movers);
engine.movers.createSet = (options) => {
if (/^traffic-(fill|outline)$/.test(options?.id ?? '')) rebuilds++;
return createMoverSet(options);
};
if (!baseline) {
const engine = window.__godsEyeView.engine;
const createMoverSet = engine.movers.createSet.bind(engine.movers);
engine.movers.createSet = (options) => {
if (/^traffic-(fill|outline)$/.test(options?.id ?? '')) rebuilds++;
return createMoverSet(options);
};
}
function discoverCollections() {
if (!baseline) return;
for (let i = 0; i < scene.primitives.length; i++) {
+1 -1
View File
@@ -201,7 +201,7 @@ export async function sampleTransitPixels(page, palette) {
?.getDetectionDiagnostics?.()
?.calloutRects?.filter((r) => r.alpha > 0.01) || [];
if (state._selectedKey) {
const card = overlay?.getOverlayPaintRect(
const card = overlay?.getPaintRect(
'transit-selected',
state._selectedKey,
);
+2 -3
View File
@@ -8,12 +8,11 @@ export async function runTransitHeadingRegression(base, check) {
base = transitQaBase(base);
const browser = await puppeteer.launch({
headless: true,
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
executablePath: chromeExecutablePath(),
args: [
'--no-sandbox',
'--disable-background-timer-throttling',
'--use-angle=metal',
...(process.platform === 'darwin' ? ['--use-angle=metal'] : []),
'--enable-gpu',
],
});
+2 -3
View File
@@ -11,10 +11,9 @@ const shots = process.env.QA_SHOTS || 'qa-shots/transit-recovery';
await mkdir(shots, { recursive: true });
const browser = await puppeteer.launch({
headless: true,
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
executablePath: chromeExecutablePath(),
args: [
'--use-angle=metal',
...(process.platform === 'darwin' ? ['--use-angle=metal'] : []),
'--enable-gpu',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
+1 -1
View File
@@ -1657,7 +1657,7 @@ try {
const t0 = performance.now();
const tick = () => {
const entry = state._vehicles.get(key);
const rect = overlay.getOverlayPaintRect('transit-selected', key);
const rect = overlay.getPaintRect('transit-selected', key);
if (entry?.marker) {
const view = scene.camera.viewMatrix;
const proj = scene.camera.frustum.projectionMatrix;
+15 -31
View File
@@ -1,6 +1,12 @@
import assert from 'node:assert/strict';
import { fromDegrees } from '../src/geo/cartesian3.js';
import { createStrict, localPreview, openPage, waitForEngine } from './qa-strict-harness.mjs';
import {
clickEmptyGround,
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
import { installTransitFixture } from './fixtures/layers/transit.mjs';
import {
combinedPayloads,
@@ -128,36 +134,14 @@ await strict.run({}, async (b) => {
);
// A click on empty ground (nothing picked around it, nothing covering it).
const clickEmpty = async () => {
const handle = await p.waitForFunction(
() => {
const { engine } = window.__godsEyeView;
const canvas = engine.host.canvas;
const box = canvas.getBoundingClientRect();
for (let y = 230; y < 600; y += 9)
for (let x = 440; x < 840; x += 9) {
if (
!canvas.contains(
document.elementFromPoint(box.left + x, box.top + y),
)
)
continue;
let free = true;
for (const [dx, dy] of [
[0, 0],
[8, 0],
[-8, 0],
[0, 8],
[0, -8],
])
if (engine.picker.pick({ x: x + dx, y: y + dy })) free = false;
if (free) return { x: box.left + x, y: box.top + y };
}
return false;
},
{ timeout: 15000 },
);
const at = await handle.jsonValue();
await p.mouse.click(at.x, at.y);
await clickEmptyGround(p, {
left: 440,
top: 230,
right: 840,
bottom: 600,
step: 9,
radius: 8,
});
await p.waitForFunction(
() =>
!window.qaUrban
+31 -1
View File
@@ -308,7 +308,11 @@ await strict.run({}, async (b) => {
engine: window.__godsEyeView?.engine?.kind ?? null,
picker: document.querySelector('#globe-engine')?.value ?? null,
focus: document.activeElement?.id ?? null,
busy: document.querySelector('#globe-engine')?.disabled ?? null,
busy: Boolean(
document.querySelector(
'#globe-engine:is([disabled], [aria-busy="true"])',
),
),
frame: Boolean(document.querySelector('.engine-switch-frame')),
loading: document.querySelector('#loading-screen')?.className ?? null,
}));
@@ -343,6 +347,9 @@ await strict.run({}, async (b) => {
let selected = null;
for (const { kind, via, click: id } of LEGS) {
if (via) {
const poseBefore = await p.evaluate(() =>
window.__godsEyeView.engine.camera.pose(),
);
const reads = { ...fixture.asked };
const sighting = byId.get(selected);
if (via === 'keyboard') {
@@ -387,6 +394,29 @@ await strict.run({}, async (b) => {
console.log('PASS', kind, 'picker click closed the card');
selected = null;
}
const poseAfter = await p.evaluate(() =>
window.__godsEyeView.engine.camera.pose(),
);
const positionErrorM = Math.hypot(
...['x', 'y', 'z'].map(
(axis) => poseAfter.position[axis] - poseBefore.position[axis],
),
);
const angleError = (key) =>
Math.abs(
Math.atan2(
Math.sin(poseAfter[key] - poseBefore[key]),
Math.cos(poseAfter[key] - poseBefore[key]),
),
);
await strict.check(
p,
LAYER,
'switch preserves the camera position and orientation',
positionErrorM < 1 &&
['heading', 'pitch', 'roll'].every((key) => angleError(key) < 1e-4),
{ poseBefore, poseAfter, positionErrorM },
);
await frame();
}
+54 -27
View File
@@ -2014,8 +2014,13 @@ async function main() {
await mil.update(v);
return {
groundSnap, milGroundSnap, detectable, airSnap, groundAgainSnap,
droppedGone: !snap('aaa066'),
milDroppedGone: !snap('bbb166'),
// Removed contacts can leave hidden slots in an engine's sprite pool.
droppedGone: !window.__godsEyeView.layers
.drawn('flights')
.some((row) => row.id === 'aaa066'),
milDroppedGone: !window.__godsEyeView.layers
.drawn('military')
.some((row) => row.id === 'bbb166'),
};
});
const fmtSnap = (s) => (s ? `show=${s.show} scale=${s.scale.toFixed(3)} rgba=(${s.red.toFixed(2)},${s.green.toFixed(2)},${s.blue.toFixed(2)},${s.alpha.toFixed(2)})` : 'missing');
@@ -2296,12 +2301,28 @@ async function main() {
try { p = coll.get(i); } catch { continue; }
if (!p) continue;
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
if (p.modelMatrix && typeof p.ready !== 'undefined' && p.id === id) found = p;
// A hidden pool slot can keep a removed contact's id: prefer
// the model shown.
if (p.modelMatrix && typeof p.ready !== 'undefined' && p.id === id && (!found || !found.show)) found = p;
}
};
walk(v.scene.primitives);
return found;
};
window.__g3dCountModels = function (id) {
let n = 0;
const walk = (coll) => {
for (let i = 0; i < coll.length; i++) {
let p;
try { p = coll.get(i); } catch { continue; }
if (!p) continue;
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
if (p.modelMatrix && typeof p.ready !== 'undefined' && p.id === id) n++;
}
};
walk(window.__godsEyeView.viewer.scene.primitives);
return n;
};
});
// (a)+(b): both grounded planes become models under the cap (the old code
@@ -2316,8 +2337,19 @@ async function main() {
skip('ground-3d: grounded planes are model-eligible and modeled (both layers)',
`independent GLB control unavailable (${glbControlDetail}); grounded GLB readiness is not observable`);
} else {
// On a miss, say what each layer's own model handle holds.
const g3dWhy = g3dModelsUp ? null : await evalPage(() => {
const dm = window.__godsEyeView.dataManager;
const of = (layerId, id) => {
const { model } = dm.layers.get(layerId).module.testing._aircraftHandlesForTest(id);
return model
? `${id}: handle ready=${model.ready} show=${model.show} failed=${model.failed}, engine model ready=${window.__g3dFindModel(id)?.ready} show=${window.__g3dFindModel(id)?.show} (${window.__g3dCountModels?.(id)} with this id)`
: `${id}: no model handle`;
};
return `${of('flights', 'aaa077')}; ${of('military', 'bbb177')}`;
});
record('ground-3d: grounded planes are model-eligible and modeled (both layers)', g3dModelsUp,
g3dModelsUp ? 'fleet models up (ready+shown)' : 'models never became ready+shown');
g3dModelsUp ? 'fleet models up (ready+shown)' : `models never became ready+shown (${g3dWhy})`);
}
if (g3dModelsUp) {
@@ -2767,8 +2799,10 @@ async function main() {
});
await nextFrames(3);
const angDiff = (a, b) => Math.abs(Math.atan2(Math.sin(a - b), Math.cos(a - b)));
const probe = async (id, course) => {
const bb = findBB(id);
const probe = async (layerId, id, course) => {
// Tamper with the layer's own glyph handle: a write to the engine's
// billboard bypasses the layer's sprite cache, so no pass rewrites it.
const bb = dm.layers.get(layerId).module.testing._aircraftHandlesForTest(id).sprite;
if (!bb || !bb.show) return { error: `${id} missing/hidden` };
const r0 = bb.rotation; // settled tamper origin; correctness uses a fresh projection below
const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id));
@@ -2813,8 +2847,8 @@ async function main() {
const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0;
const militaryCourse = window.__SYNTH.military.find((m) => m.hex === 'bbb101')?.track ?? 0;
return {
flights: await probe('aaa002', flightCourse),
military: await probe('bbb101', militaryCourse),
flights: await probe('flights', 'aaa002', flightCourse),
military: await probe('military', 'bbb101', militaryCourse),
};
});
const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? `${a.error}; ${arrivalAngleViolated(a) ? 'angle violation (not retried); ' : ''}${Number.isFinite(a.dMoveEnd) ? `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° ` : ''}${Number.isFinite(a.dReveal) ? `reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° ` : ''}elapsed=${Number(a.elapsedMs).toFixed(1)} ms attempts=${a.attempts}`
@@ -3780,24 +3814,13 @@ async function main() {
// (a) FLEET: retain the adversarial flags until this contact has passed
// through its installed fleet update and that frame has completed.
const findFleetModel = (id) => {
let found = null;
const walk = (coll) => {
const len = coll.length;
for (let i = 0; i < len; i++) {
let p; try { p = coll.get(i); } catch { continue; }
if (!p) continue;
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
if (p.activeAnimations !== undefined && p.minimumPixelSize !== undefined && p.id === id) found = p;
}
};
walk(v.scene.primitives);
return found;
};
// The layer's own model handle: the engine's Model is drawn from it,
// and a flag planted on the engine's object never reaches the layer.
const fleetModelOf = (id) => fl.testing._aircraftHandlesForTest(id).model;
fl.setParams({ models3d: true });
const fleetDeadline = Date.now() + 20000;
let fleetModel = null;
while (Date.now() < fleetDeadline && !(fleetModel = findFleetModel('aaa097'))) {
while (Date.now() < fleetDeadline && !(fleetModel = fleetModelOf('aaa097'))) {
await window.__dfSettle(500);
}
if (!fleetModel) {
@@ -3849,7 +3872,7 @@ async function main() {
return;
}
const currentBb = window.__dfFindBB('aaa097');
const currentModel = findFleetModel('aaa097');
const currentModel = fleetModelOf('aaa097');
sample = {
fleetTickCompleted: true,
fleetFrame: v.scene.frameState.frameNumber,
@@ -3859,7 +3882,7 @@ async function main() {
fleetSeeded: seededFleet,
fleetH: currentBb ? window.__dfCarto(currentBb.position).h : null,
};
if (currentBb !== bb || currentModel !== fleetModel || fleetModel.isDestroyed()
if (currentBb !== bb || currentModel !== fleetModel
|| fl.getTrackedInfo()) sample.error = 'fleet loading fixture changed before its completed frame';
});
armed = true;
@@ -4032,13 +4055,17 @@ async function main() {
const taxiMeshM = gfns?.cachedMeshFloor?.(taxiCarto.lat, taxiCarto.lon) ?? null;
const heldOwns = ns._driveFleetModelHandoffForTest({ icao24: holdIcao, position: taxiPos, course: 90 });
const heldH = window.__dfModelHeight(holdIcao);
const heldBb = !!window.__dfFindBB(holdIcao)?.show;
const heldBb = !!window.__godsEyeView.layers
.drawn('flights')
.find((row) => row.id === holdIcao && !row.trackedModel)?.show;
// 3. ~385 m out the memory stops describing anywhere this contact has
// been. It is released rather than stretched, and the gate takes over.
const farPos = Cesium.Cartesian3.fromDegrees(base.lon + 0.004, base.lat, base.h);
const releasedOwns = ns._driveFleetModelHandoffForTest({ icao24: holdIcao, position: farPos, course: 90 });
const releasedBb = !!window.__dfFindBB(holdIcao)?.show;
const releasedBb = !!window.__godsEyeView.layers
.drawn('flights')
.find((row) => row.id === holdIcao && !row.trackedModel)?.show;
fl.setParams({ models3d: false });
await window.__dfSettle(300);
+10
View File
@@ -0,0 +1,10 @@
import { parse } from 'vue/compiler-sfc';
/** JavaScript blocks with their original line numbers; templates are not code. */
export function vueScripts(source, filename) {
const { descriptor, errors } = parse(source, { filename });
if (errors.length) throw errors[0];
return [descriptor.script, descriptor.scriptSetup]
.filter(Boolean)
.map((block) => '\n'.repeat(block.loc.start.line - 1) + block.content);
}