Cut duplicated and dead code across the engines, the layers and the QA scripts

- src/geo drops the ported debug argument checks and the tests that
  bitExact already covers; package boundaries accept a directory entry,
  so src/geo/ is listed once per group.
- Cesium no longer carries movers and lines nobody draws with: they are
  optional engine capabilities. The fake engine and the Cesium camera use
  the shared cameraMath; unused contract tables, clock.ticked, the
  'finest' surface detail and Navara click statistics are gone.
- ALPR keeps one overlay core and one presentation model for both
  engines, bikeshare one station bookkeeping, the Navara ground effects
  one definition helper; image atlases, view centres, mover banks and
  colours are shared.
- The legacy switch route, the clock offset and rate controls and other
  unused exports are removed.
- The acceptance runs the Navara draw, geo import and scope checks;
  scenarios use the harness switch and page helpers; the main chunk
  check keeps the size budget only and runs once in CI.
This commit is contained in:
German Khokhlov committed 2026-11-05 13:32:39 +04:00
1 parent c502404e1d
commit 7abf74518f
272 files changed
+4828 -10213

No files matched your search

+2 -7
View File
@@ -59,7 +59,8 @@ jobs:
- name: Build production bundle
run: npm run build
- name: Main chunk (no three or Navara in Cesium mode; size budget when recorded)
- name: Main chunk size budget
if: matrix.node == '24.22.2'
run: npm run check:main-chunk
street-level-browser:
@@ -90,9 +91,6 @@ jobs:
MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture'
run: npm run build
- name: Main chunk (no three or Navara in Cesium mode; size budget when recorded)
run: npm run check:main-chunk
- name: Start the preview server
env:
MAPILLARY_CLIENT_TOKEN: 'MLY|0|qa-fixture'
@@ -154,6 +152,3 @@ jobs:
- name: Build production bundle
run: npm run build
- name: Main chunk (no three or Navara in Cesium mode; size budget when recorded)
run: npm run check:main-chunk
+3 -4
View File
@@ -52,14 +52,13 @@ resolved dependency inventory remains in `package-lock.json`.
are adapted from `@cesium/engine/Source/Core` (`Math.js`, `Ellipsoid.js`,
`scaleToGeodeticSurface.js`, `Cartographic.js`, `Cartesian2.js`,
`Cartesian3.js`, `Matrix3.js`, `Matrix4.js`, `Quaternion.js`,
`Transforms.js`, `Check.js`, `DeveloperError.js`) and say so in their first
`Transforms.js`) and say so in their first
line. The changes: the classes' static functions became module functions,
vectors, geodetic positions and quaternions became plain-data instances of
module-private constructors (own enumerable fields only, no methods),
matrices plain arrays, and the ellipsoid a frozen object; the debug
argument checks were ported with Cesium's conditions and messages and, like
Cesium's, are active in development and tests and stripped from production
builds; only the subset gev uses was kept.
argument checks were left out, as in Cesium's release build; only the subset
gev uses was kept.
- Author: Copyright 2011-2024 CesiumJS Contributors
- Source: <https://github.com/CesiumGS/cesium>
- License: Apache License 2.0
+1 -2
View File
@@ -2,8 +2,7 @@
// initial chunks are every entry chunk and what it imports statically;
// navaraMode.js reaches Navara only through import(), so three and
// @navaramap/* belong to lazy chunks. This checks the bundle's module ids
// (what Rollup put in each chunk), and fails the build otherwise;
// scripts/check-main-chunk.mjs checks the built files' text as well.
// (what Rollup put in each chunk), and fails the build otherwise.
/**
* Module ids that only the Navara engine may load, when no package list is
+2 -1
View File
@@ -55,7 +55,8 @@ See [application construction](APPLICATION.md) and the
2. `scripts/check-package-boundaries.mjs` builds every declared export without
app Vite configuration or environment files. `scripts/package-boundaries.json`
assigns each export exactly once and lists its owned modules and external
dependencies. Unused imports still count. Node exports have only a `node`
dependencies; a module entry ending in `/` (such as `src/geo/`) owns every
file under that directory. Unused imports still count. Node exports have only a `node`
condition; browser groups cannot use build-only dependency exceptions.
Portable source graphs cannot reach application/rendering, Node, Cesium or
+7 -7
View File
@@ -61,21 +61,21 @@
"@jtarrio/webrtlsdr": "^3.0.6",
"@mapbox/vector-tile": "^3.0.0",
"@meri-imperiumi/eccodes-wasm": "^2.48.2",
"@navaramap/three": "0.2.1",
"@navaramap/three-default-plugin": "0.2.1",
"cesium": "^1.124.0",
"egm96-universal": "^1.1.1",
"hls.js": "^1.7.3",
"kdbush": "4.0.2",
"mapillary-js": "^4.1.2",
"mgrs": "^2.1.0",
"pbf": "^5.1.2",
"pinia": "^2.3.1",
"satellite.js": "^6.0.2",
"vue": "^3.5.44",
"vue-i18n": "^11.4.15",
"@navaramap/three": "0.2.1",
"@navaramap/three-default-plugin": "0.2.1",
"kdbush": "4.0.2",
"postprocessing": "6.39.4",
"three": "0.185.1"
"satellite.js": "^6.0.2",
"three": "0.185.1",
"vue": "^3.5.44",
"vue-i18n": "^11.4.15"
},
"devDependencies": {
"@happy-dom/global-registrator": "^20.14.6",
+3 -3
View File
@@ -30,9 +30,9 @@ 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\/)/;
// Phase 3: the layers moved onto layer renderers (the *a tasks) keep each
// engine in their renderers: cesium only in render/cesium*.js, three and
// Navara only in render/navara*.js. The list grows with each moved layer.
// Layers built on layer renderers keep each engine in their renderers:
// cesium only in render/cesium*.js, three and Navara only in
// render/navara*.js. The list grows with each layer moved onto renderers.
const RENDERED_LAYER_DIRS = [
'src/layers/alpr/',
'src/layers/airQuality/',
+17 -97
View File
@@ -1,17 +1,12 @@
#!/usr/bin/env node
/**
* The main-chunk gate of the Cesium-mode bundle (run after `npm run build`):
* The size budget of the Cesium-mode main chunk (run after `npm run build`):
* gzip-9 of the entry chunk the built page loads (from the Vite manifest, or
* the module script dist/index.html loads) may exceed the baseline in
* scripts/main-chunk-baseline.json by at most its allowance. What the initial
* chunks may hold is the build's own check (build/initialChunkGuard.js).
*
* 1. Composition: the main chunk (the Vite manifest's entry, or the module
* script dist/index.html loads) and every chunk it imports statically carry
* no three or Navara code (text markers; the build itself also fails on
* three/@navaramap module ids in initial chunks, build/initialChunkGuard.js).
* 2. Budget, when scripts/main-chunk-baseline.json exists: gzip-9 of the
* largest dist/assets/index-*.js may exceed the recorded baseline by at
* most its allowance (the phase-4 branch's check; same exports,
* checkMainChunk and measureMainChunk, so the two merge into this script).
*
* Prints the sizes. `--json` prints the whole result; `--dist=DIR` reads
* Prints the size. `--json` prints the whole result; `--dist=DIR` reads
* another build.
*
* npm run build && npm run check:main-chunk
@@ -20,12 +15,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { gzipSync } from 'node:zlib';
import { fileURLToPath, pathToFileURL } from 'node:url';
import {
entryFromHtml,
entryFromManifest,
forbiddenMarkersIn,
initialChunks,
} from '../src/tooling/mainChunk.js';
import { entryFromHtml, entryFromManifest } from '../src/tooling/mainChunk.js';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
@@ -44,38 +34,14 @@ export function checkMainChunk(
/**
* gzip-9 size of the main chunk, with its name: the entry the built page
* loads (`entry`, or found from dist/index.html or the Vite manifest), else
* (phase 4's rule, for a dist without a page) the largest
* dist/assets/index-*.js.
* loads (`entry`, else found from the Vite manifest or dist/index.html).
*/
export function measureMainChunk(distDir, { entry = null } = {}) {
let file = entry;
if (!file) {
try {
file = findEntry(distDir).entry;
} catch {
file = null;
}
}
if (file) {
const bytes = fs.readFileSync(path.join(distDir, file));
return {
name: path.basename(file),
gzipBytes: gzipSync(bytes, { level: 9 }).length,
};
}
const assets = path.join(distDir, 'assets');
const candidates = fs
.readdirSync(assets)
.filter((name) => /^index-.*\.js$/.test(name))
.map((name) => ({ name, bytes: fs.readFileSync(path.join(assets, name)) }))
.sort((a, b) => b.bytes.length - a.bytes.length);
if (!candidates.length)
throw new Error('no dist/assets/index-*.js: run npm run build first');
const [main] = candidates;
const file = entry ?? findEntry(distDir).entry;
const bytes = fs.readFileSync(path.join(distDir, file));
return {
name: main.name,
gzipBytes: gzipSync(main.bytes, { level: 9 }).length,
name: path.basename(file),
gzipBytes: gzipSync(bytes, { level: 9 }).length,
};
}
@@ -96,44 +62,15 @@ function findEntry(dist) {
return { entry, via: 'index.html' };
}
/** Composition of the initial chunks: sizes and forbidden markers. */
export function checkComposition(dist) {
const read = (file) => {
const absolute = path.join(dist, file);
return fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf8') : null;
};
const { entry, via } = findEntry(dist);
const chunks = initialChunks(entry, read).map((file) => {
const code = read(file);
if (code == null)
return { file, missing: true, bytes: 0, gzipBytes: 0, markers: [] };
return {
file,
bytes: Buffer.byteLength(code),
gzipBytes: gzipSync(code).length,
markers: forbiddenMarkersIn(code),
};
});
return {
entry,
via,
mainBytes: chunks[0].bytes,
mainGzipBytes: chunks[0].gzipBytes,
initialBytes: chunks.reduce((sum, chunk) => sum + chunk.bytes, 0),
chunks,
ok: chunks.every((chunk) => !chunk.missing && !chunk.markers.length),
};
}
function main() {
const option = (name) =>
process.argv.find((arg) => arg.startsWith(`--${name}=`))?.split('=')[1];
const dist = path.resolve(ROOT, option('dist') ?? 'dist');
const composition = checkComposition(dist);
const { entry, via } = findEntry(dist);
const baselinePath = path.join(ROOT, 'scripts/main-chunk-baseline.json');
let budget = null;
if (fs.existsSync(baselinePath)) {
const measured = measureMainChunk(dist, { entry: composition.entry });
const measured = measureMainChunk(dist, { entry });
budget = {
...measured,
...checkMainChunk(
@@ -142,28 +79,11 @@ function main() {
),
};
}
const ok = composition.ok && (budget?.ok ?? true);
const ok = budget?.ok ?? true;
if (process.argv.includes('--json')) {
console.log(JSON.stringify({ composition, budget, ok }, null, 2));
console.log(JSON.stringify({ entry, via, budget, ok }, null, 2));
} else {
const kb = (bytes) => `${(bytes / 1000).toFixed(2)} kB`;
console.log(
`[check:main-chunk] entry ${composition.entry} (via ${composition.via})`,
);
for (const chunk of composition.chunks)
console.log(
`[check:main-chunk] ${chunk.file}: ${kb(chunk.bytes)} (gzip ${kb(chunk.gzipBytes)})` +
(chunk.missing ? ' MISSING' : '') +
(chunk.markers.length
? ` FORBIDDEN: ${chunk.markers.join(', ')}`
: ''),
);
console.log(
`[check:main-chunk] main ${composition.mainBytes} bytes (gzip ${composition.mainGzipBytes}); initial chunks ${composition.initialBytes} bytes; ` +
(composition.ok
? 'no three or Navara'
: 'three or Navara in the initial chunks'),
);
console.log(`[check:main-chunk] entry ${entry} (via ${via})`);
console.log(
budget
? `[check:main-chunk] ${budget.name}: ${budget.message}`
+11 -3
View File
@@ -37,7 +37,11 @@ export async function checkPackageBoundaries(root) {
) {
throw new Error(`Invalid package boundary: ${name}`);
}
// A module entry ending in "/" owns every file under that directory.
const allowed = new Set();
const allowedDirectories = [];
const owns = (id) =>
allowed.has(id) || allowedDirectories.some((dir) => id.startsWith(dir));
for (const module of group.modules) {
if (
typeof module !== 'string' ||
@@ -56,7 +60,11 @@ export async function checkPackageBoundaries(root) {
) {
throw new Error(`Boundary module escapes repository: ${name}`);
}
allowed.add(normalizePath(resolved));
if (module.endsWith('/')) {
allowedDirectories.push(`${normalizePath(resolved)}/`);
} else {
allowed.add(normalizePath(resolved));
}
}
for (const external of group.external) {
if (
@@ -82,7 +90,7 @@ export async function checkPackageBoundaries(root) {
if (
typeof target !== 'string' ||
!target.startsWith('./') ||
!allowed.has(normalizePath(path.resolve(root, target)))
!owns(normalizePath(path.resolve(root, target)))
) {
throw new Error(`Export must point to an owned module: ${key}`);
}
@@ -109,7 +117,7 @@ export async function checkPackageBoundaries(root) {
// template blocks belong to the component file itself.
if (info.id.startsWith('\0')) return;
const id = info.id.split('?')[0];
if (!allowed.has(id)) {
if (!owns(id)) {
throw new Error(
`Package boundary ${name} imports an unowned module: ${path.relative(root, id)}`,
);
+10 -4
View File
@@ -589,6 +589,8 @@
"src/layers/alpr/model.js",
"src/layers/alpr/policy.js",
"src/layers/alpr/visuals.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/overlay.test.mjs",
"src/layers/alpr/source.js",
"src/layers/alpr/source.test.mjs",
@@ -1217,6 +1219,7 @@
"src/layers/aircraft/render/contract.js",
"src/layers/shared/color.test.mjs",
"src/layers/shared/color.js",
"src/layers/shared/viewRectangle.js",
"src/layers/military/render/cesium.js",
"src/layers/military/render/navara.js",
"src/layers/aircraft/qaDrawn.js",
@@ -1225,13 +1228,18 @@
"src/layers/vessels/render/cesium.js",
"src/layers/vessels/render/contract.js",
"src/layers/vessels/renderer.test.mjs",
"src/layers/vessels/testFixtures.mjs",
"src/layers/vessels/render/navara.js",
"src/layers/shared/render/engineView.js",
"src/layers/shared/render/navaraGroundEffect.js",
"src/layers/shared/render/navaraMoverBank.js",
"src/layers/shared/render/imageAtlas.js",
"src/layers/installations/render/cesium/lifecycle.js",
"src/layers/installations/render/cesium/rendering.js",
"src/layers/installations/render/cesium/selection.js",
"src/layers/bikeshare/render/cesium/lifecycle.js",
"src/layers/bikeshare/presentation.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/bikeshare/presentation.test.mjs",
"src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/render/cesium.js",
@@ -1329,8 +1337,8 @@
"src/globe/cesium/render.js",
"src/globe/cesium/surface.js",
"src/globe/contract.js",
"src/globe/navara/cameraMath.js",
"src/globe/navara/cameraMath.test.mjs",
"src/globe/cameraMath.js",
"src/globe/cameraMath.test.mjs",
"src/globe/navara/cameraModel.js",
"src/globe/navara/cameraModel.test.mjs",
"src/globe/navara/createNavaraEngine.js",
@@ -1364,14 +1372,12 @@
"src/tooling/mainChunkComposition.test.mjs",
"src/tooling/initialChunkGuard.test.mjs",
"src/globe/movers.test.mjs",
"src/globe/cesium/movers.test.mjs",
"src/globe/navara/movers/pack.test.mjs",
"src/globe/navara/movers/pickIds.test.mjs",
"src/globe/navara/movers/moverMesh.test.mjs",
"src/globe/navara/movers/modelMover.test.mjs",
"src/bench/syntheticFlights.test.mjs",
"src/globe/lines.test.mjs",
"src/globe/cesium/lines.test.mjs",
"src/globe/navara/lines/lineMesh.test.mjs",
"src/globe/pickDelivery.test.mjs",
"src/globe/navara/picking.test.mjs",
+47 -534
View File
@@ -897,24 +897,7 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
"src/geo/"
],
"external": ["cesium"]
},
@@ -1176,24 +1159,7 @@
"src/globe/cesium/engineFromViewer.js",
"src/cameraAngles.js",
"src/globe/contract.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/app/layers/directory.js",
"src/globe/follow.js",
"src/ui/kit/UiSegmented.vue"
@@ -1333,24 +1299,7 @@
"src/ui/i18n.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/contract.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
"src/geo/"
],
"external": ["cesium", "vue", "pinia", "vue-i18n"]
},
@@ -1432,24 +1381,7 @@
"src/ui/unitLabels.js",
"src/data/render/cesiumProjection.js",
"src/data/render/cesiumConstants.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
@@ -1516,24 +1448,7 @@
"src/data/render/cesiumProjection.js",
"src/data/render/cesiumConstants.js",
"src/layers/shared/color.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js",
@@ -1571,24 +1486,7 @@
"src/data/inputOwnership.js",
"src/data/render/cesiumProjection.js",
"src/data/render/cesiumConstants.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
],
@@ -1635,24 +1533,7 @@
"src/layers/vessels/icons.js",
"src/layers/shared/color.js",
"src/data/render/cesiumProjection.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
],
@@ -1665,24 +1546,7 @@
"src/layers/earthquakes/model.js",
"src/layers/earthquakes/source.js",
"src/layers/earthquakes/records.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/cartographic.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/ellipsoid.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/",
"src/layers/shared/color.js"
],
"external": []
@@ -1751,24 +1615,7 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/layers/firms/rectangle.js",
"src/layers/shared/color.js"
],
@@ -1810,24 +1657,7 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/time/clock.js",
"src/layers/shared/color.js"
],
@@ -1872,24 +1702,7 @@
"src/i18n/locales/zh-Hant.json",
"src/layers/shared/color.js",
"src/layers/launches/render/cesium.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
@@ -1946,24 +1759,7 @@
"src/layers/installations/render/cesium/selection.js",
"src/layers/shared/color.js",
"src/layers/installations/render/cesium/namedMarkers.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
],
@@ -2015,24 +1811,7 @@
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js",
"src/data/render/cesiumProjection.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
],
@@ -2094,24 +1873,7 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
],
@@ -2151,24 +1913,7 @@
"src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/presentation.js",
"src/layers/shared/color.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
],
@@ -2227,24 +1972,7 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/layers/shared/color.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
"src/geo/"
],
"external": ["hls.js"]
},
@@ -2318,24 +2046,7 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/layers/streetLevel/providers/mapillary/policy.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
"src/geo/"
],
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
},
@@ -2371,24 +2082,7 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
"src/geo/"
],
"external": ["@mapbox/vector-tile", "pbf"]
},
@@ -2437,24 +2131,7 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
"src/geo/"
],
"external": []
},
@@ -2486,24 +2163,7 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
"src/geo/"
],
"external": ["cesium"]
},
@@ -3692,7 +3352,7 @@
"src/globe/cesium/render.js",
"src/globe/cesium/surface.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/navara/cameraMath.js",
"src/globe/cameraMath.js",
"src/globe/navara/cameraModel.js",
"src/globe/navara/createNavaraEngine.js",
"src/globe/navara/credits.js",
@@ -3722,6 +3382,8 @@
"src/data/render/cesiumTrailRenderer.js",
"src/layers/alpr/render/cesiumPresentation.js",
"src/layers/alpr/render/cesiumOverlay.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/render/cesium.js",
"src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/presentation.js",
@@ -3742,35 +3404,16 @@
"src/layers/localAdsb/qaDrawn.js",
"src/ui/imageryBoxNavara.js",
"src/layers/installations/render/cesium/namedMarkers.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/time/clock.js",
"src/globe/navara/overlay.js",
"src/globe/navara/frameDescriptor.js",
"src/globe/cesium/clock.js",
"src/globe/pickDelivery.js",
"src/globe/navara/clock.js",
"src/globe/cesium/lines.js",
"src/globe/follow.js",
"src/globe/lines.js",
"src/globe/navara/movers/pickIds.js",
"src/globe/cesium/movers.js",
"src/globe/movers.js",
"src/globe/cesium/follow.js",
"src/globe/cpuPicks.js",
@@ -3847,6 +3490,8 @@
"src/layers/cyclones/render/navara.js",
"src/layers/shared/render/groundRing.js",
"src/layers/bikeshare/render/navara.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/shared/viewRectangle.js",
"src/layers/installations/render/navaraGeometry.js",
"src/layers/installations/render/navaraOutlineBatch.js",
"src/layers/installations/render/navaraStencilBatch.js",
@@ -3857,6 +3502,7 @@
"src/layers/radio/render/navara.js",
"src/layers/transit/render/navara.js",
"src/layers/transit/render/navaraSprites.js",
"src/layers/shared/render/navaraMoverBank.js",
"src/layers/transit/render/navaraView.js",
"src/layers/transit/render/navaraTrail.js",
"src/layers/recentImagery/render/navara.js",
@@ -3868,14 +3514,18 @@
"src/layers/cctv/render/navaraLine.js",
"src/layers/alpr/render/navara.js",
"src/layers/alpr/render/navaraOverlay.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/radio/screenClusters.js",
"src/layers/shared/render/navaraCellGeometry.js",
"src/layers/shared/render/navaraGroundCells.js",
"src/layers/firms/render/navaraSprites.js",
"src/layers/shared/render/imageAtlas.js",
"src/layers/firms/render/navaraHeat.js",
"src/layers/shared/render/navaraGroundGeometry.js",
"src/layers/shared/render/navaraGroundGrid.js",
"src/layers/shared/render/navaraGroundSurface.js",
"src/layers/shared/render/navaraGroundEffect.js",
"src/annotations/outlineRungs.js",
"src/sources/openFreeMapOutlines.js",
"src/sources/nominatimOutlines.js",
@@ -4364,6 +4014,7 @@
"src/layers/transit/state.js",
"src/layers/transit/testing.js",
"src/layers/transit/viewport.js",
"src/layers/shared/viewRectangle.js",
"src/data/cameraSensitivity.js",
"src/data/contactPlayback.js",
"src/data/contactTrailRenderer.js",
@@ -4528,6 +4179,7 @@
"src/globe/cesium/input.js",
"src/globe/cesium/convert.js",
"src/globe/cesium/camera.js",
"src/globe/cameraMath.js",
"src/globe/cesium/frames.js",
"src/globe/cesium/picker.js",
"src/globe/cesium/events.js",
@@ -4543,6 +4195,8 @@
"src/data/render/cesiumTrailRenderer.js",
"src/layers/alpr/render/cesiumPresentation.js",
"src/layers/alpr/render/cesiumOverlay.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/render/cesium.js",
"src/layers/bikeshare/selectionModel.js",
"src/layers/bikeshare/presentation.js",
@@ -4550,6 +4204,7 @@
"src/layers/bikeshare/render/cesium/viewport.js",
"src/layers/bikeshare/render/cesium/lifecycle.js",
"src/layers/bikeshare/render/cesium/rendering.js",
"src/layers/bikeshare/stationPoints.js",
"src/layers/bikeshare/render/cesium/selection.js",
"src/layers/installations/render/cesium/lifecycle.js",
"src/layers/installations/render/cesium.js",
@@ -4561,24 +4216,7 @@
"src/data/render/cesiumContactTrailRenderer.js",
"src/layers/localAdsb/qaDrawn.js",
"src/layers/installations/render/cesium/namedMarkers.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/time/clock.js",
"src/globe/follow.js",
"src/globe/cesium/follow.js",
@@ -4687,24 +4325,7 @@
"src/globe/cesium/engineFromViewer.js",
"src/globe/cesium/credits.js",
"src/globe/contract.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/surfaceDetail.js",
"src/sources/openFreeMap.js",
"src/sources/vectorTiles.js",
@@ -4985,24 +4606,7 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js",
"src/geo/julianDate.js",
"src/geo/simon1994.js",
"src/geo/developerError.js",
"src/geo/check.js",
"src/geo/math.js",
"src/geo/cartesian2.js",
"src/geo/cartographic.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/quaternion.js",
"src/geo/matrix4.js",
"src/geo/ellipsoid.js",
"src/geo/transforms.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/debug.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/index.js"
"src/geo/"
],
"external": ["cesium"]
},
@@ -5315,6 +4919,7 @@
"src/layers/transit/testing.js",
"src/layers/transit/trails.js",
"src/layers/transit/viewport.js",
"src/layers/shared/viewRectangle.js",
"src/overlays/worldOverlay.js",
"src/overlays/worldOverlayDraw.js",
"src/overlays/worldOverlayTokens.js",
@@ -5349,6 +4954,7 @@
"src/globe/cesium/picker.js",
"src/globe/cesium/frames.js",
"src/globe/cesium/camera.js",
"src/globe/cameraMath.js",
"src/globe/cesium/convert.js",
"src/globe/cesium/input.js",
"src/maps/google3d.js",
@@ -5373,24 +4979,7 @@
"src/data/render/cesiumProjection.js",
"src/layers/shared/color.js",
"src/layers/transit/render/cesium.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/time/clock.js",
"src/globe/surfaceDetail.js",
"src/celestialRing/render/cesium.js"
@@ -5418,6 +5007,7 @@
"src/layers/transit/state.js",
"src/layers/transit/testing.js",
"src/layers/transit/viewport.js",
"src/layers/shared/viewRectangle.js",
"src/data/contactPlayback.js",
"src/data/contactTrailRenderer.js",
"src/data/transitProxy.js",
@@ -5445,24 +5035,7 @@
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/layers/shared/color.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/",
"src/globe/contract.js"
],
"external": ["cesium", "pbf"]
@@ -5506,9 +5079,6 @@
"exports": ["./layers/perimeters"],
"modules": [
"src/geo/cartesian3.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/math.js",
"src/layers/perimeters/index.js",
"src/layers/perimeters/render/cesium.js",
@@ -5611,9 +5181,6 @@
"src/layers/ghostMaps/kml.js",
"src/layers/ghostMaps/source.js",
"src/geo/cartesian3.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/math.js",
"src/sources/httpBody.js",
"src/data/geoFileImport.js",
@@ -5663,24 +5230,7 @@
"exports": ["./layers/drone-airspace"],
"modules": [
"src/layers/droneAirspace/index.js",
"src/geo/index.js",
"src/geo/ellipsoid.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/debug.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/boundingSphere.js",
"src/geo/transforms.js",
"src/geo/matrix4.js",
"src/geo/cartesian3.js",
"src/geo/quaternion.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/check.js",
"src/geo/developerError.js",
"src/geo/simon1994.js",
"src/geo/julianDate.js",
"src/geo/",
"src/layers/shared/render/cesiumZones.js",
"src/sources/httpBody.js",
"src/layers/droneAirspace/source.js",
@@ -5769,24 +5319,7 @@
"src/tools/queries/panelRequest.js",
"src/geoDistance.js",
"src/visualStylePolicy.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/cartographic.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/ellipsoid.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/index.js",
"src/geo/julianDate.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/simon1994.js",
"src/geo/transforms.js"
"src/geo/"
],
"external": ["satellite.js"]
},
@@ -5898,9 +5431,6 @@
"exports": ["./layers/air-quality"],
"modules": [
"src/geo/cartesian3.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/math.js",
"src/layers/airQuality/index.js",
"src/layers/airQuality/render/cesium.js",
@@ -5946,24 +5476,7 @@
"src/loadingFeedback.js",
"src/data/installationFeedback.js",
"src/i18n/marker.js",
"src/geo/boundingSphere.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/cartographic.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js",
"src/geo/ellipsoid.js",
"src/geo/ellipsoidGeodesic.js",
"src/geo/index.js",
"src/geo/julianDate.js",
"src/geo/math.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/simon1994.js",
"src/geo/transforms.js"
"src/geo/"
],
"external": ["cesium"]
}
+41 -57
View File
@@ -83,42 +83,28 @@ try {
// Every name the checkout's .env files define, plus key-like names in this
// environment: all blank for the build and the preview.
const KEY_LIKE = /(_KEY|_TOKEN|_SECRET|_CLIENT_ID|PASSWORD)$/;
const dotenvNames = () => {
const names = new Set();
for (const file of ['.env', '.env.local'])
if (existsSync(join(root, file)))
for (const line of readFileSync(join(root, file), 'utf8').split('\n')) {
const name = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1];
if (name) names.add(name);
}
return [...names].sort();
};
const envNames = () => {
const names = new Set();
for (const file of ['.env', '.env.local'])
if (existsSync(join(root, file)))
for (const line of readFileSync(join(root, file), 'utf8').split('\n')) {
const name = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1];
if (name) names.add(name);
}
for (const name of Object.keys(process.env))
if (KEY_LIKE.test(name)) names.add(name);
return [...names].sort();
};
const envValues = () => {
const values = [];
for (const file of ['.env', '.env.local'])
if (existsSync(join(root, file)))
for (const line of readFileSync(join(root, file), 'utf8').split('\n')) {
const [, name, raw] = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/) ?? [];
const value = raw?.trim().replace(/^["']|["']$/g, '');
// Credentials only (model names and URLs are settings the code may
// also carry as defaults).
if (KEY_LIKE.test(name ?? '') && value && value.length >= 8)
values.push(value);
}
return values;
};
/** The [name, value] pairs of the checkout's .env and .env.local, in order. */
const dotenv = () =>
['.env', '.env.local']
.filter((file) => existsSync(join(root, file)))
.flatMap((file) => readFileSync(join(root, file), 'utf8').split('\n'))
.map((line) => line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)/))
.filter(Boolean)
.map(([, name, raw]) => [name, raw.trim().replace(/^["']|["']$/g, '')]);
const dotenvNames = () => [...new Set(dotenv().map(([name]) => name))].sort();
const envNames = () =>
[
...new Set([
...dotenvNames(),
...Object.keys(process.env).filter((name) => KEY_LIKE.test(name)),
]),
].sort();
// Credentials only (model names and URLs are settings the code may also
// carry as defaults).
const envValues = () =>
dotenv()
.filter(([name, value]) => KEY_LIKE.test(name) && value.length >= 8)
.map(([, value]) => value);
const scrubbed = () => ({
...process.env,
...Object.fromEntries(envNames().map((name) => [name, ''])),
@@ -368,9 +354,9 @@ const runs = [
['rocket-launches'],
['--missions'],
],
// Stage 3's own checks (one application per page, voice across a switch).
// They block paid routes themselves but do not run under the strict
// harness, so they add no assertion manifest and no network ledger.
// One application per page, and voice across a switch: these block paid
// routes themselves but do not run under the strict harness, so they add
// no assertion manifest and no network ledger.
['app-once', 'app-once', [], [], 600, true],
['voice-engine-switch', 'voice-engine-switch', [], [], 300, true],
['strict-rapid-switch', 'strict-rapid-switch', []],
@@ -379,14 +365,18 @@ const runs = [
['strict-failure-matrix', 'strict-failure-matrix', [], [], 600],
['strict-cold-chunks', 'strict-cold-chunks', []],
['strict-prod-smoke', 'strict-prod-smoke', []],
// Stage 6: the app's visual styles, bloom, contact sonar, cockpit clouds
// and annotations drawn on Navara (annotations is a tool, not a catalog
// layer: TOOL_IDS).
// The app's visual styles, bloom, contact sonar, cockpit clouds, and the
// annotations, Draw, Import and the scope mask with the celestial ring on
// Navara (those are tools and display overlays, not catalog layers:
// TOOL_IDS).
['navara-visual-styles', 'navara-visual-styles', []],
['navara-bloom', 'navara-bloom', []],
['navara-sonar', 'navara-sonar', []],
['navara-cockpit-clouds', 'navara-cockpit-clouds', []],
['navara-annotations', 'navara-annotations', ['annotations']],
['navara-draw', 'navara-draw', ['draw']],
['navara-geo-import', 'navara-geo-import', ['geo-import']],
['navara-scope', 'navara-scope', ['scope-mask', 'celestial-ring']],
['strict-soak', 'strict-soak', [], soakRuns, 1200],
[
'strict-soak--long',
@@ -398,7 +388,13 @@ const runs = [
];
// Ids a scenario covers that are tools, not catalog layers: their
// assertions are recorded, but the catalog comparison leaves them out.
const TOOL_IDS = new Set(['annotations']);
const TOOL_IDS = new Set([
'annotations',
'draw',
'geo-import',
'scope-mask',
'celestial-ring',
]);
const scenarioIds = [
...new Set(
runs.flatMap(([, , ids]) => ids).filter((id) => !TOOL_IDS.has(id)),
@@ -414,9 +410,7 @@ const liveCatalog = {};
layers: [],
name: 'catalog',
});
const browser = await strict.launch({
args: ['--use-angle=metal', '--enable-gpu'],
});
const browser = await strict.launch();
try {
for (const engine of ENGINES) {
const page = await browser.newPage();
@@ -497,17 +491,7 @@ for (const [label, name, layers, extra = [], seconds = 300, external] of runs) {
const script = 'scripts/qa-' + name + '.mjs';
const evidence = join(output, 'evidence', label + '.json');
rmSync(evidence, { force: true });
const args =
name === 'w2-imagery'
? [
'--url',
base.origin + '/?engine=navara',
'--report',
join(output, 'evidence', 'imagery-report.json'),
'--switch',
'--preview',
]
: [base.origin, ...extra];
const args = [base.origin, ...extra];
if (external) args.splice(0, 1, base.origin + '/');
else args.push(`--layers=${layers.join(',')}`, `--evidence=${evidence}`);
const start = Date.now();
+1 -18
View File
@@ -221,7 +221,7 @@ try {
await new Promise((resolve) => setTimeout(resolve, 2000));
report.firstRoundTrip = await listenerCounts(page, { bySource: true });
}
// Both engines draw the session's marks and files (Navara since Stage 6).
// Both engines draw the session's marks and files (Navara included).
await page.waitForFunction(
() => window.__gevGeoImport.diagnostics().drawn === 1,
{ timeout: 30_000 },
@@ -412,23 +412,6 @@ try {
report.errors = errors;
check(errors.length === 0, 'page errors', errors);
// The legacy route (?switch=legacy) still builds an application per
// engine, for one release.
const legacy = await openSessionPage(
browser,
sessionAddress(base, { engine: 'cesium', extra: { switch: 'legacy' } }),
);
await waitReady(legacy.page, 'cesium');
await switchEngine(legacy.page, 'navara');
await switchEngine(legacy.page, 'cesium');
report.legacy = await readSession(legacy.page);
check(
report.legacy.counters.applications === 3,
'the legacy route builds an application per engine',
report.legacy.counters,
);
check(legacy.errors.length === 0, 'legacy page errors', legacy.errors);
await legacy.page.close();
console.log(JSON.stringify(report, null, 2));
if (!reportOnly) console.log('PASS application created once; session kept');
} finally {
+1 -1
View File
@@ -45,7 +45,7 @@ export function isExpectedPreviewSetupError(
}
}
// W1 acceptance shares one strict runtime verdict across its main page and
// The runtime evidence gate shares one strict runtime verdict across its main page and
// every share-link page. Response diagnostics never retain query credentials.
export function watchW1Page(page, report, { name, preview = false, baseUrl }) {
const errors = (report.browserErrors ??= []);
+1 -1
View File
@@ -37,7 +37,7 @@ const { stdout: video } = await promisify(execFile)(
{ encoding: 'buffer', maxBuffer: 16000000 },
);
const strict = createStrict({ origin: url.origin, layers: ['cctv'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
const p = await b.newPage();
const errors = [];
p.on('pageerror', (e) => {
+2 -2
View File
@@ -42,7 +42,7 @@
*
* This legacy harness is the geometry, lifecycle and real-input oracle.
* Migration runs use headless Metal and strict runtime error observation.
* Screenshots remain visual-review evidence here; the W2 companion must
* Screenshots remain visual-review evidence here; a separate pixel gate must
* independently prove actual marker, image-plane and video pixels.
*
* No `cesium` package import for in-page math: the app does not expose a
@@ -2245,7 +2245,7 @@ async function main() {
// -----------------------------------------------------------------------
// Screenshots supplement this state/geometry oracle. Positive rendered
// media pixels are a separate mandatory W2 companion gate.
// media pixels are a separate mandatory pixel gate.
// -----------------------------------------------------------------------
await sleep(1000);
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
+10 -26
View File
@@ -1,6 +1,11 @@
import assert from 'node:assert/strict';
import { fromDegrees } from '../src/geo/cartesian3.js';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -10,14 +15,8 @@ const strict = createStrict({
origin: base.origin,
layers: ['military-awareness', 'military-installations'],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
await p.evaluateOnNewDocument(() => {
const fetch = window.fetch.bind(window);
window.fetch = (input, options) => {
@@ -89,21 +88,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
);
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -235,8 +220,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
baseline,
);
for (const kind of ['cesium', 'navara']) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
const current = await sample();
console.log('handoff', kind, JSON.stringify(current));
assert.deepEqual(current, {
+11 -28
View File
@@ -1,19 +1,18 @@
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
base.hash =
'v=2&lat=55.3241&lon=10.7995&alt=25000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
const strict = createStrict({ origin: base.origin, layers: ['directions'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
let routeReads = 0,
release;
await p.setRequestInterception(true);
@@ -55,21 +54,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
});
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -91,8 +76,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
'directions',
'A and B set by clicks on the globe; one route request',
);
await strict.pick(p, 'cesium');
await ready('cesium');
await strict.switchTo(p, 'cesium');
release();
await p.waitForFunction(() => window.qaRoute.getStats().count === 3);
await p.waitForFunction(() => {
@@ -105,8 +89,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
'the route asked for on Navara arrives on Cesium; a step is selectable',
);
const transfer = async (kind) => {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
assert.equal(
await p.evaluate(
() =>
+1 -1
View File
@@ -135,7 +135,7 @@ const strict = createStrict({
layers: [],
fixtures: ['https://earthquake.usgs.gov/*'],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1280, height: 900 });
const errors = [];
+444 -460
View File
@@ -7,478 +7,462 @@ base.search = '?welcome=0&look=cinematic';
base.hash =
'v=2&lat=42.3554&lon=-71.0605&alt=6000&heading=0&pitch=-90&map=osm&l=e';
const strict = createStrict({ origin: base.origin, layers: ['earthquakes'] });
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
const fetch = window.fetch.bind(window);
// Two Boston vessels for the Contacts session; empty air and site feeds.
const fixtures = {
'/api/vessels': () => ({
status: 'live',
rows: ['123456789', '223456789'].map((mmsi, i) => ({
mmsi,
name: 'Boston ' + i,
lat: 42.3554,
lon: -71.0605 + i * 0.05,
last_position_epoch: Date.now() / 1000,
})),
}),
'/api/vessels/track': () => ({ samples: [] }),
'/api/flights': () => ({ time: Date.now() / 1000, states: [] }),
'/api/military': () => ({ now: Date.now(), ac: [] }),
'/api/military-installations': () => ({ elements: [], status: 'ok' }),
};
window.fetch = (url, options) => {
if (
String(url).startsWith(
'https://earthquake.usgs.gov/earthquakes/feed/',
)
)
return Promise.resolve(
Response.json({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'qa-boston',
geometry: {
type: 'Point',
coordinates: [-71.0605, 42.3554, 8],
},
properties: { mag: 4, time: Date.now(), place: 'QA Boston' },
},
],
}),
);
const fixture =
fixtures[new URL(url.url ?? url, document.baseURI).pathname];
return fixture
? Promise.resolve(Response.json(fixture()))
: fetch(url, options);
};
});
await page.setViewport({ width: 1280, height: 800 });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
async function ready(kind) {
await page.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.querySelector('#globe-engine')?.value === kind &&
!document.querySelector('#globe-engine').disabled &&
document
.getElementById('loading-screen')
?.classList.contains('hidden'),
{ timeout: 90000 },
kind,
);
await page.evaluate(async () => {
await window.__godsEyeView.styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || window.__godsEyeView.tileset)
throw new Error('Keyless preview required');
});
assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
}
async function verifyQuakeSurface(kind) {
const meanRed = async (enabled) => {
await page.evaluate(async (enabled) => {
const a = window.__godsEyeView;
await a.dataManager.setEnabled('earthquakes', enabled, {
origin: 'user',
});
a.engine.render.request('qa-quake-surface');
}, enabled);
if (enabled)
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('earthquakes')
.module.getAnalystRecords().length === 1,
);
await new Promise((resolve) => setTimeout(resolve, 1800));
const bytes = await page.screenshot({
clip: { x: 675, y: 403, width: 20, height: 20 },
});
const { channels } = await sharp(bytes).stats();
return channels[0].mean - channels[1].mean;
};
const off = await meanRed(false),
on = await meanRed(true);
assert.ok(
on > off + 8,
kind +
' must draw the shallow red earthquake footprint: ' +
off +
' -> ' +
on,
);
await strict.passed(page, 'earthquakes', 'shallow red footprint drawn', {
off,
on,
});
console.log(
'PASS',
kind,
'earthquake footprint red minus green:',
off,
'->',
on,
);
}
const stored = () =>
page.evaluate(() => localStorage.getItem('gev.engine'));
async function switchTo(kind) {
await strict.run({}, async (browser) => {
const page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
const fetch = window.fetch.bind(window);
// Two Boston vessels for the Contacts session; empty air and site feeds.
const fixtures = {
'/api/vessels': () => ({
status: 'live',
rows: ['123456789', '223456789'].map((mmsi, i) => ({
mmsi,
name: 'Boston ' + i,
lat: 42.3554,
lon: -71.0605 + i * 0.05,
last_position_epoch: Date.now() / 1000,
})),
}),
'/api/vessels/track': () => ({ samples: [] }),
'/api/flights': () => ({ time: Date.now() / 1000, states: [] }),
'/api/military': () => ({ now: Date.now(), ac: [] }),
'/api/military-installations': () => ({ elements: [], status: 'ok' }),
};
window.fetch = (url, options) => {
if (
await page.$eval(
'#control-panel-toggle',
(el) => el.getAttribute('aria-expanded') !== 'true',
)
) {
await page.focus('#control-panel-toggle');
await page.keyboard.press('Enter');
}
await page.waitForSelector('#globe-engine', { visible: true });
await page.evaluate(() => {
window.__switchDocument = document;
window.__quakeEntry =
window.__godsEyeView.dataManager.layers.get('earthquakes');
window.__quakeTimer = window.__quakeEntry.intervalId;
window.__renderers ??= new Map();
window.__renderers.set(
window.__godsEyeView.engine.kind,
window.__godsEyeView.engine,
);
window.__switchSplash = false;
window.__switchObserver = new MutationObserver(() => {
if (!document.querySelector('#loading-screen.hidden'))
window.__switchSplash = true;
});
window.__switchObserver.observe(
document.querySelector('#loading-screen'),
{ attributes: true },
);
window.__outgoingEngine = window.__godsEyeView.engine;
});
await strict.pick(page, kind);
await ready(kind);
assert.ok(
await page.evaluate(() => window.__switchDocument === document),
);
assert.ok(
await page.evaluate(() => {
window.__switchObserver.disconnect();
const { engine } = window.__godsEyeView;
const previous = window.__renderers.get(engine.kind);
return (
!window.__switchSplash &&
window.__quakeEntry ===
window.__godsEyeView.dataManager.layers.get('earthquakes') &&
window.__quakeTimer === window.__quakeEntry.intervalId &&
(!previous || previous === engine) &&
!window.__outgoingEngine.isDestroyed() &&
!window.__outgoingEngine.host.canvas.isConnected &&
engine.host.canvas.isConnected &&
window.__godsEyeView.dataManager
.getEnabledLayerIds()
.has('earthquakes')
);
}),
);
await strict.passed(
page,
'earthquakes',
'same layer entry, model and poll timer; no splash; outgoing canvas detached',
);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
assert.equal(
await page.evaluate(async () => {
const parked = window.__outgoingEngine;
let frames = 0;
const remove = parked.frames.postRender.addEventListener(() => {
frames++;
});
parked.render.request('qa-parked');
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
remove();
return frames;
}),
0,
);
const url = new URL(page.url());
assert.equal(url.searchParams.get('engine'), kind);
assert.equal(url.searchParams.get('look'), 'cinematic');
const hash = new URLSearchParams(url.hash.slice(1));
assert.equal(hash.get('map'), 'osm');
assert.ok(Math.abs(Number(hash.get('lat')) - 42.3554) < 0.01);
assert.ok(Math.abs(Number(hash.get('lon')) + 71.0605) < 0.01);
assert.ok(Math.abs(Number(hash.get('alt')) - 6000) < 30);
const view = await page.evaluate(() => {
const { engine, mapStackController } = window.__godsEyeView;
const { latitude, longitude, height } =
engine.camera.pose().cartographic;
return {
latitude: latitude * (180 / Math.PI),
longitude: longitude * (180 / Math.PI),
height,
map: mapStackController.getActiveId(),
};
});
assert.equal(view.map, 'osm');
assert.ok(Math.abs(view.latitude - 42.3554) < 0.01);
assert.ok(Math.abs(view.longitude + 71.0605) < 0.01);
assert.ok(Math.abs(view.height - 6000) < 30);
await strict.passed(
page,
'earthquakes',
'parked engine draws no frame; pose, map and layers carried over',
);
}
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('cesium');
await verifyQuakeSurface('cesium');
const sharpen = () =>
page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
await page.evaluate(() => {
const style = window.__godsEyeView.styleManager;
style.setStyle('retro');
style.setStyle('normal');
});
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 49 });
await switchTo('navara');
await verifyQuakeSurface('navara');
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 80 });
assert.ok(
await page.evaluate(
() =>
Math.abs(window.__godsEyeView.engine.render.sharpen().amount - 1.7) <
1e-6,
),
);
assert.equal(await stored(), 'navara');
await page.goBack({ waitUntil: 'domcontentloaded' });
await ready('cesium');
assert.equal((await sharpen()).intensityPct, 49);
await page.goForward();
await ready('navara');
assert.equal((await sharpen()).intensityPct, 80);
await page.evaluate(() =>
window.__godsEyeView.styleManager.setSharpen({
enabled: false,
intensityPct: 49,
}),
);
await switchTo('cesium');
await switchTo('navara');
assert.deepEqual(await sharpen(), { enabled: false, intensityPct: 49 });
// The address carries the running layer set once its debounced write lands.
const settledAddress = async () => {
let previous;
for (;;) {
await new Promise((resolve) => setTimeout(resolve, 1200));
const current = await page.evaluate(() =>
new URLSearchParams(location.hash.slice(1)).get('l'),
);
if (current === previous) return current;
previous = current;
}
};
const pick = async (kind) => {
await strict.pick(page, kind);
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
};
const visit = async (direction, kind) => {
await (direction === 'back'
? page.goBack({ waitUntil: 'domcontentloaded' })
: page.goForward());
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
};
const quakes = () =>
page.evaluate(() => {
const { dataManager } = window.__godsEyeView;
return {
model: window.__quakeModel === dataManager.layers.get('earthquakes'),
enabled: dataManager.getEnabledLayerIds().has('earthquakes'),
};
});
// Like a same-engine Back, each entry brings back its own layer set, on
// the same layer models.
await page.evaluate(() => {
window.__quakeModel =
window.__godsEyeView.dataManager.layers.get('earthquakes');
});
const navaraLayers = await settledAddress();
await pick('cesium');
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', false, {
origin: 'user',
}),
);
const cesiumLayers = await settledAddress();
assert.notEqual(cesiumLayers, navaraLayers);
await visit('back', 'navara');
assert.deepEqual(await quakes(), { model: true, enabled: true });
await strict.passed(
page,
'earthquakes',
'Back restores the enabled layer on the same model',
);
assert.equal(await settledAddress(), navaraLayers, 'Back keeps its entry');
await visit('forward', 'cesium');
assert.deepEqual(await quakes(), { model: true, enabled: false });
await strict.passed(
page,
'earthquakes',
'Forward restores the disabled layer on the same model',
);
assert.equal(
await settledAddress(),
cesiumLayers,
'Forward keeps its entry',
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', true, {
origin: 'user',
}),
);
await pick('navara');
console.log(
"PASS Back/Forward restore each entry's layers on the same models",
);
// Back/Forward hand the session off like the picker: the Contacts subject
// and its navigation history, its layers and the Director survive.
await page.evaluate(async () => {
const { dataManager, styleManager, sceneDirector } = window.__godsEyeView;
await dataManager.setEnabled('ais-live-vessels', true, {
origin: 'user',
});
const outcome = await styleManager.setContextMode('contacts');
if (!outcome.ok) throw Error(JSON.stringify(outcome));
window.qaSession = {
director: sceneDirector,
context: dataManager.layers.get('military-awareness').module,
vessels: dataManager.layers.get('ais-live-vessels'),
};
if (!window.qaSession.vessels.module.selectById('123456789'))
throw Error('Missing first vessel');
if (
!window.qaSession.context.navigateNext({
targetLayer: 'ais-live-vessels',
})
String(url).startsWith('https://earthquake.usgs.gov/earthquakes/feed/')
)
throw Error('Cannot navigate to the second vessel');
return Promise.resolve(
Response.json({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'qa-boston',
geometry: {
type: 'Point',
coordinates: [-71.0605, 42.3554, 8],
},
properties: { mag: 4, time: Date.now(), place: 'QA Boston' },
},
],
}),
);
const fixture =
fixtures[new URL(url.url ?? url, document.baseURI).pathname];
return fixture
? Promise.resolve(Response.json(fixture()))
: fetch(url, options);
};
});
await page.setViewport({ width: 1280, height: 800 });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
async function ready(kind) {
await page.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.querySelector('#globe-engine')?.value === kind &&
!document.querySelector('#globe-engine').disabled &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 90000 },
kind,
);
await page.evaluate(async () => {
await window.__godsEyeView.styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || window.__godsEyeView.tileset)
throw new Error('Keyless preview required');
});
const session = () =>
page.evaluate(() => {
const { dataManager, styleManager, sceneDirector } =
window.__godsEyeView;
const { director, context, vessels } = window.qaSession;
return {
director: director === sceneDirector && !director._destroyed,
owners:
context === dataManager.layers.get('military-awareness').module &&
vessels === dataManager.layers.get('ais-live-vessels'),
mode: styleManager.getContextModeState().mode,
subject: context.getContextSnapshot()?.subject?.id,
history: context._getAwarenessNavigationStateForTest().historyKeys,
enabled: [...dataManager.getEnabledLayerIds()].sort(),
};
});
const live = await session();
assert.equal(live.mode, 'flights');
assert.equal(live.subject, '223456789');
assert.ok(live.history.includes('ais-live-vessels:123456789'));
await settledAddress();
const historyLength = await page.evaluate(() => history.length);
await pick('cesium');
assert.deepEqual(await session(), live, 'a pick keeps the session');
await settledAddress();
await visit('back', 'navara');
assert.deepEqual(await session(), live, 'Back keeps the session');
await visit('forward', 'cesium');
assert.deepEqual(await session(), live, 'Forward keeps the session');
assert.equal(await page.evaluate(() => history.length), historyLength + 1);
assert.ok(
await page.evaluate(async () => {
const { dataManager, styleManager } = window.__godsEyeView;
const result = await styleManager.setContextMode('off');
await dataManager.setEnabled('ais-live-vessels', false, {
assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
}
async function verifyQuakeSurface(kind) {
const meanRed = async (enabled) => {
await page.evaluate(async (enabled) => {
const a = window.__godsEyeView;
await a.dataManager.setEnabled('earthquakes', enabled, {
origin: 'user',
});
return result.ok;
}),
);
await pick('navara');
console.log(
'PASS Back/Forward keep Contacts subject, history, layers and Director',
);
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('navara');
await switchTo('cesium');
assert.equal(await stored(), 'cesium');
await page.evaluate(() => {
const write = Storage.prototype.setItem;
Storage.prototype.setItem = function (key, value) {
if (key === 'gev.engine')
throw new DOMException('Blocked', 'SecurityError');
return write.call(this, key, value);
};
});
await switchTo('navara');
assert.equal(await stored(), 'cesium');
// The real Map Source chips move into daylight inspection and back to live lighting.
await page.$eval('[data-stack-id="sentinel2"]', (el) => el.click());
await page.waitForFunction(
() =>
window.__godsEyeView.mapStackController.getActiveId() === 'sentinel2' &&
window.__godsEyeView.engine.host.container.hasAttribute(
'data-terrain-daylight',
),
);
// Zooming out on the satellite map must keep the same inspection sun.
a.engine.render.request('qa-quake-surface');
}, enabled);
if (enabled)
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('earthquakes')
.module.getAnalystRecords().length === 1,
);
await new Promise((resolve) => setTimeout(resolve, 1800));
const bytes = await page.screenshot({
clip: { x: 675, y: 403, width: 20, height: 20 },
});
const { channels } = await sharp(bytes).stats();
return channels[0].mean - channels[1].mean;
};
const off = await meanRed(false),
on = await meanRed(true);
assert.ok(
await page.evaluate(async () => {
on > off + 8,
kind +
' must draw the shallow red earthquake footprint: ' +
off +
' -> ' +
on,
);
await strict.passed(page, 'earthquakes', 'shallow red footprint drawn', {
off,
on,
});
console.log(
'PASS',
kind,
'earthquake footprint red minus green:',
off,
'->',
on,
);
}
const stored = () => page.evaluate(() => localStorage.getItem('gev.engine'));
async function switchTo(kind) {
if (
await page.$eval(
'#control-panel-toggle',
(el) => el.getAttribute('aria-expanded') !== 'true',
)
) {
await page.focus('#control-panel-toggle');
await page.keyboard.press('Enter');
}
await page.waitForSelector('#globe-engine', { visible: true });
await page.evaluate(() => {
window.__switchDocument = document;
window.__quakeEntry =
window.__godsEyeView.dataManager.layers.get('earthquakes');
window.__quakeTimer = window.__quakeEntry.intervalId;
window.__renderers ??= new Map();
window.__renderers.set(
window.__godsEyeView.engine.kind,
window.__godsEyeView.engine,
);
window.__switchSplash = false;
window.__switchObserver = new MutationObserver(() => {
if (!document.querySelector('#loading-screen.hidden'))
window.__switchSplash = true;
});
window.__switchObserver.observe(
document.querySelector('#loading-screen'),
{ attributes: true },
);
window.__outgoingEngine = window.__godsEyeView.engine;
});
await strict.pick(page, kind);
await ready(kind);
assert.ok(await page.evaluate(() => window.__switchDocument === document));
assert.ok(
await page.evaluate(() => {
window.__switchObserver.disconnect();
const { engine } = window.__godsEyeView;
engine.camera.zoomOut(2_000_000);
await new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('qa-orbital-daylight');
});
const previous = window.__renderers.get(engine.kind);
return (
engine.camera.height() > 1_500_000 &&
engine.host.container.hasAttribute('data-terrain-daylight')
!window.__switchSplash &&
window.__quakeEntry ===
window.__godsEyeView.dataManager.layers.get('earthquakes') &&
window.__quakeTimer === window.__quakeEntry.intervalId &&
(!previous || previous === engine) &&
!window.__outgoingEngine.isDestroyed() &&
!window.__outgoingEngine.host.canvas.isConnected &&
engine.host.canvas.isConnected &&
window.__godsEyeView.dataManager
.getEnabledLayerIds()
.has('earthquakes')
);
}),
'the orbital frame keeps daylight on the inspected globe',
);
await page.$eval('[data-stack-id="osm"]', (el) => el.click());
await strict.passed(
page,
'earthquakes',
'same layer entry, model and poll timer; no splash; outgoing canvas detached',
);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
assert.equal(
await page.evaluate(async () => {
const parked = window.__outgoingEngine;
let frames = 0;
const remove = parked.frames.postRender.addEventListener(() => {
frames++;
});
parked.render.request('qa-parked');
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
remove();
return frames;
}),
0,
);
const url = new URL(page.url());
assert.equal(url.searchParams.get('engine'), kind);
assert.equal(url.searchParams.get('look'), 'cinematic');
const hash = new URLSearchParams(url.hash.slice(1));
assert.equal(hash.get('map'), 'osm');
assert.ok(Math.abs(Number(hash.get('lat')) - 42.3554) < 0.01);
assert.ok(Math.abs(Number(hash.get('lon')) + 71.0605) < 0.01);
assert.ok(Math.abs(Number(hash.get('alt')) - 6000) < 30);
const view = await page.evaluate(() => {
const { engine, mapStackController } = window.__godsEyeView;
const { latitude, longitude, height } = engine.camera.pose().cartographic;
return {
latitude: latitude * (180 / Math.PI),
longitude: longitude * (180 / Math.PI),
height,
map: mapStackController.getActiveId(),
};
});
assert.equal(view.map, 'osm');
assert.ok(Math.abs(view.latitude - 42.3554) < 0.01);
assert.ok(Math.abs(view.longitude + 71.0605) < 0.01);
assert.ok(Math.abs(view.height - 6000) < 30);
await strict.passed(
page,
'earthquakes',
'parked engine draws no frame; pose, map and layers carried over',
);
}
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('cesium');
await verifyQuakeSurface('cesium');
const sharpen = () =>
page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
await page.evaluate(() => {
const style = window.__godsEyeView.styleManager;
style.setStyle('retro');
style.setStyle('normal');
});
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 49 });
await switchTo('navara');
await verifyQuakeSurface('navara');
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 80 });
assert.ok(
await page.evaluate(
() =>
window.__godsEyeView.mapStackController.getActiveId() === 'osm' &&
!window.__godsEyeView.engine.host.container.hasAttribute(
'data-terrain-daylight',
),
Math.abs(window.__godsEyeView.engine.render.sharpen().amount - 1.7) <
1e-6,
),
);
assert.equal(await stored(), 'navara');
await page.goBack({ waitUntil: 'domcontentloaded' });
await ready('cesium');
assert.equal((await sharpen()).intensityPct, 49);
await page.goForward();
await ready('navara');
assert.equal((await sharpen()).intensityPct, 80);
await page.evaluate(() =>
window.__godsEyeView.styleManager.setSharpen({
enabled: false,
intensityPct: 49,
}),
);
await switchTo('cesium');
await switchTo('navara');
assert.deepEqual(await sharpen(), { enabled: false, intensityPct: 49 });
// The address carries the running layer set once its debounced write lands.
const settledAddress = async () => {
let previous;
for (;;) {
await new Promise((resolve) => setTimeout(resolve, 1200));
const current = await page.evaluate(() =>
new URLSearchParams(location.hash.slice(1)).get('l'),
);
if (current === previous) return current;
previous = current;
}
};
const pick = async (kind) => {
await strict.pick(page, kind);
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
assert.deepEqual(errors, []);
console.log(
'PASS Cesium default, both switches, same document, warm engine identity, parked canvas, no startup splash, share pose/map/layers, Back/Forward, remembered engine, blocked preference storage',
};
const visit = async (direction, kind) => {
await (direction === 'back'
? page.goBack({ waitUntil: 'domcontentloaded' })
: page.goForward());
await ready(kind);
await page.waitForFunction(
() => !document.querySelector('.engine-switch-frame'),
);
},
);
};
const quakes = () =>
page.evaluate(() => {
const { dataManager } = window.__godsEyeView;
return {
model: window.__quakeModel === dataManager.layers.get('earthquakes'),
enabled: dataManager.getEnabledLayerIds().has('earthquakes'),
};
});
// Like a same-engine Back, each entry brings back its own layer set, on
// the same layer models.
await page.evaluate(() => {
window.__quakeModel =
window.__godsEyeView.dataManager.layers.get('earthquakes');
});
const navaraLayers = await settledAddress();
await pick('cesium');
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', false, {
origin: 'user',
}),
);
const cesiumLayers = await settledAddress();
assert.notEqual(cesiumLayers, navaraLayers);
await visit('back', 'navara');
assert.deepEqual(await quakes(), { model: true, enabled: true });
await strict.passed(
page,
'earthquakes',
'Back restores the enabled layer on the same model',
);
assert.equal(await settledAddress(), navaraLayers, 'Back keeps its entry');
await visit('forward', 'cesium');
assert.deepEqual(await quakes(), { model: true, enabled: false });
await strict.passed(
page,
'earthquakes',
'Forward restores the disabled layer on the same model',
);
assert.equal(await settledAddress(), cesiumLayers, 'Forward keeps its entry');
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('earthquakes', true, {
origin: 'user',
}),
);
await pick('navara');
console.log(
"PASS Back/Forward restore each entry's layers on the same models",
);
// Back/Forward hand the session off like the picker: the Contacts subject
// and its navigation history, its layers and the Director survive.
await page.evaluate(async () => {
const { dataManager, styleManager, sceneDirector } = window.__godsEyeView;
await dataManager.setEnabled('ais-live-vessels', true, {
origin: 'user',
});
const outcome = await styleManager.setContextMode('contacts');
if (!outcome.ok) throw Error(JSON.stringify(outcome));
window.qaSession = {
director: sceneDirector,
context: dataManager.layers.get('military-awareness').module,
vessels: dataManager.layers.get('ais-live-vessels'),
};
if (!window.qaSession.vessels.module.selectById('123456789'))
throw Error('Missing first vessel');
if (
!window.qaSession.context.navigateNext({
targetLayer: 'ais-live-vessels',
})
)
throw Error('Cannot navigate to the second vessel');
});
const session = () =>
page.evaluate(() => {
const { dataManager, styleManager, sceneDirector } = window.__godsEyeView;
const { director, context, vessels } = window.qaSession;
return {
director: director === sceneDirector && !director._destroyed,
owners:
context === dataManager.layers.get('military-awareness').module &&
vessels === dataManager.layers.get('ais-live-vessels'),
mode: styleManager.getContextModeState().mode,
subject: context.getContextSnapshot()?.subject?.id,
history: context._getAwarenessNavigationStateForTest().historyKeys,
enabled: [...dataManager.getEnabledLayerIds()].sort(),
};
});
const live = await session();
assert.equal(live.mode, 'flights');
assert.equal(live.subject, '223456789');
assert.ok(live.history.includes('ais-live-vessels:123456789'));
await settledAddress();
const historyLength = await page.evaluate(() => history.length);
await pick('cesium');
assert.deepEqual(await session(), live, 'a pick keeps the session');
await settledAddress();
await visit('back', 'navara');
assert.deepEqual(await session(), live, 'Back keeps the session');
await visit('forward', 'cesium');
assert.deepEqual(await session(), live, 'Forward keeps the session');
assert.equal(await page.evaluate(() => history.length), historyLength + 1);
assert.ok(
await page.evaluate(async () => {
const { dataManager, styleManager } = window.__godsEyeView;
const result = await styleManager.setContextMode('off');
await dataManager.setEnabled('ais-live-vessels', false, {
origin: 'user',
});
return result.ok;
}),
);
await pick('navara');
console.log(
'PASS Back/Forward keep Contacts subject, history, layers and Director',
);
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
await ready('navara');
await switchTo('cesium');
assert.equal(await stored(), 'cesium');
await page.evaluate(() => {
const write = Storage.prototype.setItem;
Storage.prototype.setItem = function (key, value) {
if (key === 'gev.engine')
throw new DOMException('Blocked', 'SecurityError');
return write.call(this, key, value);
};
});
await switchTo('navara');
assert.equal(await stored(), 'cesium');
// The real Map Source chips move into daylight inspection and back to live lighting.
await page.$eval('[data-stack-id="sentinel2"]', (el) => el.click());
await page.waitForFunction(
() =>
window.__godsEyeView.mapStackController.getActiveId() === 'sentinel2' &&
window.__godsEyeView.engine.host.container.hasAttribute(
'data-terrain-daylight',
),
);
// Zooming out on the satellite map must keep the same inspection sun.
assert.ok(
await page.evaluate(async () => {
const { engine } = window.__godsEyeView;
engine.camera.zoomOut(2_000_000);
await new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('qa-orbital-daylight');
});
return (
engine.camera.height() > 1_500_000 &&
engine.host.container.hasAttribute('data-terrain-daylight')
);
}),
'the orbital frame keeps daylight on the inspected globe',
);
await page.$eval('[data-stack-id="osm"]', (el) => el.click());
await page.waitForFunction(
() =>
window.__godsEyeView.mapStackController.getActiveId() === 'osm' &&
!window.__godsEyeView.engine.host.container.hasAttribute(
'data-terrain-daylight',
),
);
assert.deepEqual(errors, []);
console.log(
'PASS Cesium default, both switches, same document, warm engine identity, parked canvas, no startup splash, share pose/map/layers, Back/Forward, remembered engine, blocked preference storage',
);
});
+1 -1
View File
@@ -67,7 +67,7 @@ const getOpt = (name, dflt) => {
};
const BASE_URL = getOpt('--url', 'http://localhost:4173');
// --engine=cesium|navara (phase 3): Navara reads the layer through the QA
// --engine=cesium|navara: Navara reads the layer through the QA
// probe (window.__godsEyeView.layers.drawn) and moves the engine camera.
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
const APP_URL =
+1 -1
View File
@@ -21,7 +21,7 @@ const strict = createStrict({
// installAlprFixture answers the camera tiles.
fixtures: ['https://tiles.dontgetflocked.com/*'],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1280, height: 900 });
const errors = [];
+2 -2
View File
@@ -11,7 +11,7 @@ import fs from 'node:fs';
// QA_BASE_URL matches the sibling harnesses (qa-height-datum / qa-cctv-v2) so a
// secondary checkout can verify against its own dev server instead of the default :4173.
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
// --engine=cesium|navara (phase 3): on Navara the camera, the mesh probes and
// --engine=cesium|navara: on Navara the camera, the mesh probes and
// the sprite census go through the engine and the QA probe.
const ENGINE = (process.argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
const APP_URL =
@@ -125,7 +125,7 @@ const report = await page.evaluate(async () => {
drawnBy: modelAt ? 'model' : 'sprite',
meshM: meshH != null ? +meshH.toFixed(1) : null,
aboveMeshM: meshH != null ? +(c.height - meshH).toFixed(1) : null,
// Why a contact sits where it does (10b review 6): the probe's own
// Why a contact sits where it does: the probe's own
// answer (source, tile size) and the coarse DEM floor (the prior).
probe: answer ? { source: answer.source, tileSizeM: answer.tileSizeM ?? null } : null,
demFloorM: cachedGroundFloor(c.latitude * 180 / Math.PI, c.longitude * 180 / Math.PI) ?? null,
+10 -26
View File
@@ -1,21 +1,20 @@
import { fromDegrees } from '../src/geo/cartesian3.js';
import { readFile } from 'node:fs/promises';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
base.hash =
'v=2&lat=43.6500&lon=-79.3800&alt=46000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
const strict = createStrict({ origin: base.origin, layers: ['s2-ghostmaps'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
const file = await readFile(
new URL('./fixtures/ghostmaps-scenes.kmz', import.meta.url),
);
@@ -63,21 +62,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
[...file],
);
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager } = window.__godsEyeView;
await dataManager.setEnabled('s2-ghostmaps', true, { origin: 'user' });
@@ -111,8 +96,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.evaluate(() => {
window.ghostStats.titles = [];
});
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
await p.waitForFunction(() =>
window.ghostStats.titles.some((v) => /QA zone/i.test(v)),
);
+12 -30
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js';
import sharp from 'sharp';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -16,14 +21,8 @@ const strict = createStrict({
'fire-perimeters',
],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
await p.evaluateOnNewDocument(() => {
const fetch = window.fetch.bind(window);
window.fetch = (input, o) => {
@@ -122,20 +121,6 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
};
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
const capture = async (visible, layerId = 'air-quality') => {
await p.evaluate(
async (show, id) => {
@@ -230,7 +215,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}
return on;
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(() => {
const d = window.__godsEyeView;
if (d.tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -240,8 +225,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
});
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
const same = await p.evaluate(
() =>
window.__godsEyeView.dataManager.layers.get('air-quality').module ===
@@ -357,8 +341,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
};
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
await card(true);
await strict.check(
p,
@@ -478,8 +461,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
let histories = await p.evaluate(() => window.qaVesselHistories);
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
}
const sample = await p.evaluate(() => {
const entry =
+10 -26
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -26,14 +31,8 @@ const strict = createStrict({
origin: base.origin,
layers: ['local-dams', 'local-datacenters'],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
const reads = { datacenters: 0, dams: 0 };
p.on('request', (req) => {
const path = new URL(req.url()).pathname;
@@ -52,21 +51,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
});
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
const ids = ['local-datacenters', 'local-dams'];
const dataset = await p.evaluate(async (ids) => {
const { dataManager, tileset } = window.__godsEyeView;
@@ -184,8 +169,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.evaluate(() => {
window.qaSelections = [];
});
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
await p.waitForFunction(
() => window.qaSelections.includes('local-datacenters:342664739'),
{ timeout: 15000 },
+1 -2
View File
@@ -314,8 +314,7 @@ async function main() {
(ids) => ids.filter((id) => !window.__godsEyeView.layers.available(id)),
fieldLayers,
);
// An engine with flights but not the other field layers (Navara, W1 so
// far): the same 12,000 / 5,200 observations, all as aircraft, so the
// An engine with flights but not the other field layers: the same 12,000 / 5,200 observations, all as aircraft, so the
// detection lane is measured on the layers the engine has.
let fieldCounts = FIELD_COUNTS;
let normalCounts = NORMAL_COUNTS;
+482 -493
View File
@@ -15,514 +15,503 @@ url.hash =
// left.
const HIDDEN_PICKER = 'the Globe Engine picker is hidden in cockpit mode';
const strict = createStrict({ origin: url.origin, layers: [layerId] });
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.setViewport({ width: 1280, height: 720 });
await page.evaluateOnNewDocument(
(layerId, aircraftId) => {
window.__qaAircraft = { layerId, aircraftId };
const fetch = window.fetch.bind(window);
window.__flightPolls = 0;
const interval = window.setInterval.bind(window);
window.setInterval = function (callback, delay, ...args) {
if (typeof callback !== 'function')
return interval(callback, delay, ...args);
const id = interval(
function (...values) {
if (window.__handoff?.timer === id) window.__handoff.ticks++;
return callback.apply(this, values);
},
delay,
...args,
);
return id;
};
window.fetch = (input, options) => {
const path = new URL(input.url ?? input, document.baseURI).pathname;
const now = Math.floor(Date.now() / 1000);
if (path === '/api/flights') {
if (layerId === 'flights') window.__flightPolls++;
return Promise.resolve(
Response.json({
time: now,
states: [
[
'71be26',
'GEV123',
'QA',
now,
now,
-125.5779,
40.2539,
9700,
false,
200,
45,
0,
null,
9700,
null,
false,
0,
],
],
}),
);
}
if (path === '/api/military') {
if (layerId === 'military') window.__flightPolls++;
return Promise.resolve(
Response.json({
now: Date.now(),
ac: [
{
hex: 'aaa052',
flight: 'GEVMIL',
lon: -125.5779,
lat: 40.2539,
alt_geom: 31825,
alt_baro: 31825,
gs: 389,
track: 45,
seen: 0,
seen_pos: 0,
dbFlags: 1,
type: 'adsb_icao',
},
],
}),
);
}
if (path === '/api/flights/track')
return Promise.resolve(Response.json({ path: [] }));
if (path.startsWith('/api/flights/'))
return Promise.resolve(Response.json({}));
return fetch(input, options);
};
},
layerId,
aircraftId,
);
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
const ready = (kind) =>
page.waitForFunction(
(engine) =>
window.__godsEyeView?.engine?.kind === engine &&
!document.querySelector('#globe-engine')?.disabled &&
document.querySelector('#loading-screen.hidden') &&
!document.querySelector('.engine-switch-frame'),
{ timeout: 90000 },
kind,
);
await ready('navara');
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.waitForSelector('#cockpit-entry', { visible: true });
await page.click('#cockpit-entry');
await page.waitForFunction(
() => window.__godsEyeView.styleManager.getCockpitState().active,
);
await page.evaluate(() => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required');
styleManager.cockpitView.setVisionMode('thermal');
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
window.__handoff = {
dataManager,
entry,
row: document.querySelector(
`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
),
timer: entry.intervalId,
ticks: 0,
model: entry.module,
pollCount: window.__flightPolls,
timestamp: entry.module.getStats().lastUpdate,
await strict.run({}, async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.setViewport({ width: 1280, height: 720 });
await page.evaluateOnNewDocument(
(layerId, aircraftId) => {
window.__qaAircraft = { layerId, aircraftId };
const fetch = window.fetch.bind(window);
window.__flightPolls = 0;
const interval = window.setInterval.bind(window);
window.setInterval = function (callback, delay, ...args) {
if (typeof callback !== 'function')
return interval(callback, delay, ...args);
const id = interval(
function (...values) {
if (window.__handoff?.timer === id) window.__handoff.ticks++;
return callback.apply(this, values);
},
delay,
...args,
);
return id;
};
});
// Refuse the first Cesium canvas. The application must roll back with the
// original aircraft still in the cockpit, then allow an ordinary retry.
await page.evaluate(() => {
const original = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, ...args) {
if (String(type).includes('webgl')) {
window.__refusedContext = true;
throw new Error('QA renderer startup failure');
window.fetch = (input, options) => {
const path = new URL(input.url ?? input, document.baseURI).pathname;
const now = Math.floor(Date.now() / 1000);
if (path === '/api/flights') {
if (layerId === 'flights') window.__flightPolls++;
return Promise.resolve(
Response.json({
time: now,
states: [
[
'71be26',
'GEV123',
'QA',
now,
now,
-125.5779,
40.2539,
9700,
false,
200,
45,
0,
null,
9700,
null,
false,
0,
],
],
}),
);
}
return original.call(this, type, ...args);
};
window.__restoreContext = () => {
HTMLCanvasElement.prototype.getContext = original;
};
});
const historyLength = await page.evaluate(() => history.length);
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await page.waitForFunction(() => window.__refusedContext === true);
await ready('navara');
// A failed switch leaves the session usable on the engine that worked and
// adds no history entry.
const survived = (kind) =>
page.evaluate(
(kind, length) => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
return (
engine.kind === kind &&
new URL(location.href).searchParams.get('engine') === kind &&
history.length === length &&
dataManager === window.__handoff.dataManager &&
dataManager.layers.get(window.__qaAircraft.layerId) ===
window.__handoff.entry &&
styleManager.getCockpitState().active &&
styleManager.getCockpitState().visionMode === 'thermal' &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId
if (path === '/api/military') {
if (layerId === 'military') window.__flightPolls++;
return Promise.resolve(
Response.json({
now: Date.now(),
ac: [
{
hex: 'aaa052',
flight: 'GEVMIL',
lon: -125.5779,
lat: 40.2539,
alt_geom: 31825,
alt_baro: 31825,
gs: 389,
track: 45,
seen: 0,
seen_pos: 0,
dbFlags: 1,
type: 'adsb_icao',
},
],
}),
);
},
kind,
historyLength,
);
await page.evaluate(() => window.__restoreContext());
await strict.check(
page,
layerId,
'cold Cesium failure rolls back to Navara: same session, cockpit, aircraft, no history entry',
await survived('navara'),
);
console.log(
'PASS cold Cesium failure rolled back to Navara with the same flight, cockpit and history',
);
for (const kind of ['cesium', 'navara']) {
await strict.selectEngine(page, kind, HIDDEN_PICKER);
await ready(kind);
const result = await page.evaluate(() => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
const before = window.__handoff;
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const cockpit = styleManager.getCockpitState();
return {
sameOwner:
before.dataManager === dataManager &&
before.entry === entry &&
before.model === entry.module,
sameRow:
Boolean(before.row) &&
before.row ===
document.querySelector(
`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
),
sameTimer:
before.timer === entry.intervalId && entry.intervalId != null,
sourcePolls: window.__flightPolls - before.pollCount,
timerTicks: before.ticks,
stamp: entry.module.getStats().lastUpdate,
initialStamp: before.timestamp,
id: cockpit.subject?.id,
active: cockpit.active,
mode: cockpit.visionMode,
hud: !document.querySelector('#cockpit-hud').hidden,
drawn: entry.module
.qaDrawn()
.some((row) => row.id === window.__qaAircraft.aircraftId),
height: engine.camera.height(),
};
});
assert.ok(
result.sameOwner &&
result.sameTimer &&
result.sameRow &&
result.active &&
result.hud &&
result.drawn,
JSON.stringify(result),
);
assert.equal(result.id, aircraftId);
assert.equal(result.mode, 'thermal');
assert.ok(
result.sourcePolls <= result.timerTicks,
'switch must not add a source request',
);
if (result.timerTicks === 0) {
assert.equal(result.stamp, result.initialStamp);
}
await strict.passed(
page,
layerId,
'same manager entry, model, row, poll timer, cockpit, thermal mode, HUD and drawn aircraft',
result,
);
// Polling goes on after the switch: the same timer ticks and each tick
// reads the source once (no tick may be skipped, none added).
const polls = await page.evaluate(async (layerId) => {
const entry = window.__godsEyeView.dataManager.layers.get(layerId);
const every =
Number(entry.module.refreshInterval) ||
Number(entry.module.updateInterval);
const start = {
polls: window.__flightPolls,
ticks: window.__handoff.ticks,
stamp: entry.module.getStats().lastUpdate,
};
const deadline = Date.now() + 2.5 * every + 5000;
while (
window.__handoff.ticks - start.ticks < 2 &&
Date.now() < deadline
)
await new Promise((resolve) => setTimeout(resolve, 250));
// Let the last tick's request land.
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
every,
ticks: window.__handoff.ticks - start.ticks,
polls: window.__flightPolls - start.polls,
updated: entry.module.getStats().lastUpdate !== start.stamp,
sameTimer: entry.intervalId === window.__handoff.timer,
};
}, layerId);
await strict.check(
page,
layerId,
'polling continues after the switch: two timer ticks, one source read each, fresh data',
polls.every > 0 &&
polls.ticks >= 2 &&
polls.polls === polls.ticks &&
polls.updated &&
polls.sameTimer,
polls,
);
await page.waitForFunction(
() =>
Math.abs(window.__godsEyeView.engine.camera.height() - 9700) < 1000,
);
console.log(
`PASS ${kind} cockpit, aircraft, HUD, manager and timer (${result.sourcePolls} polls since entry)`,
);
}
// The other direction, warm: the parked Navara renderer fails to resume on
// the way back from Cesium. The session rolls back to Cesium, then an
// ordinary retry starts Navara cold.
await page.evaluate(() => {
window.__parkedNavara = window.__godsEyeView.engine;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await ready('cesium');
const lengthOnCesium = await page.evaluate(() => {
const request = window.__parkedNavara.render.request;
window.__parkedNavara.render.request = function (...args) {
window.__parkedNavara.render.request = request;
window.__refusedResume = true;
throw new Error('QA parked renderer failure');
}
if (path === '/api/flights/track')
return Promise.resolve(Response.json({ path: [] }));
if (path.startsWith('/api/flights/'))
return Promise.resolve(Response.json({}));
return fetch(input, options);
};
return history.length;
});
await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
await page.waitForFunction(() => window.__refusedResume === true);
await ready('cesium');
assert.ok(
(await page.evaluate(
(length) => history.length === length,
lengthOnCesium,
)) &&
(await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
return (
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId &&
new URL(location.href).searchParams.get('engine') === 'cesium'
);
})),
'a failed warm Navara resume must leave Cesium usable without a history entry',
},
layerId,
aircraftId,
);
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
const ready = (kind) =>
page.waitForFunction(
(engine) =>
window.__godsEyeView?.engine?.kind === engine &&
!document.querySelector('#globe-engine')?.disabled &&
document.querySelector('#loading-screen.hidden') &&
!document.querySelector('.engine-switch-frame'),
{ timeout: 90000 },
kind,
);
await strict.passed(
page,
layerId,
'warm Navara failure rolls back to Cesium: same aircraft and cockpit, no history entry',
);
console.log(
'PASS warm Navara failure rolled back to Cesium with the same flight and cockpit',
);
await page.evaluate(() => {
window.__parkedCesium = window.__godsEyeView.engine;
});
await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
await ready('navara');
assert.ok(
await page.evaluate(() => {
const { engine, styleManager } = window.__godsEyeView;
return (
engine !== window.__parkedNavara &&
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId
);
}),
'the retry starts a new Navara renderer on the same session',
);
console.log('PASS retry after the warm failure started Navara cold');
// Both engines fail: the warm Cesium resume and the warm Navara recovery.
// No engine runs, but the error screen offers the engine picker, and a
// retry starts Cesium cold on the same session without a reload.
const lengthBeforeDoubleFailure = await page.evaluate(() => {
window.__failResume = true;
for (const engine of [
window.__godsEyeView.engine,
window.__parkedCesium,
]) {
const request = engine.render.request;
engine.render.request = function (reason, ...args) {
if (reason === 'engine-resume' && window.__failResume) {
window.__refusedResumes = (window.__refusedResumes ?? 0) + 1;
throw new Error('QA parked renderer failure');
}
return request.call(this, reason, ...args);
};
}
window.__startDocument = document;
return history.length;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await page
.waitForSelector('#loading-screen .engine-switch-retry button', {
visible: true,
timeout: 90000,
})
.catch(async (error) => {
console.log(
'no retry picker',
JSON.stringify(
await page.evaluate(() => ({
refused: window.__refusedResumes,
engine: window.__godsEyeView?.engine?.kind ?? null,
retry: !!document.querySelector('.engine-switch-retry'),
loading: document.querySelector('#loading-screen')?.className,
status: document.querySelector('#loading-screen .loader-status')
?.textContent,
address: location.search,
})),
),
);
throw error;
});
assert.ok(
await page.evaluate(
(length) =>
window.__refusedResumes === 2 &&
// No globe runs (the page's debug object may stay, without one).
!window.__godsEyeView?.engine &&
history.length === length &&
!document
.querySelector('#loading-screen')
.classList.contains('hidden') &&
/QA parked renderer failure/.test(
document.querySelector('#loading-screen .loader-status')
.textContent,
),
lengthBeforeDoubleFailure,
await ready('navara');
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
await page.waitForSelector('#cockpit-entry', { visible: true });
await page.click('#cockpit-entry');
await page.waitForFunction(
() => window.__godsEyeView.styleManager.getCockpitState().active,
);
await page.evaluate(() => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required');
styleManager.cockpitView.setVisionMode('thermal');
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
window.__handoff = {
dataManager,
entry,
row: document.querySelector(
`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
),
'a failed recovery shows its error and the engine picker ' +
JSON.stringify(
await page.evaluate(() => ({
refused: window.__refusedResumes,
app: !!window.__godsEyeView,
engine: window.__godsEyeView?.engine?.kind ?? null,
history: history.length,
loading: document.querySelector('#loading-screen')?.className,
status: document.querySelector('#loading-screen .loader-status')
?.textContent,
})),
),
);
await page.evaluate(() => {
window.__failResume = false;
});
await page.click(
'#loading-screen .engine-switch-retry [data-engine="cesium"]',
);
await ready('cesium');
assert.ok(
await page.evaluate(() => {
const { dataManager, styleManager } = window.__godsEyeView;
timer: entry.intervalId,
ticks: 0,
model: entry.module,
pollCount: window.__flightPolls,
timestamp: entry.module.getStats().lastUpdate,
};
});
// Refuse the first Cesium canvas. The application must roll back with the
// original aircraft still in the cockpit, then allow an ordinary retry.
await page.evaluate(() => {
const original = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, ...args) {
if (String(type).includes('webgl')) {
window.__refusedContext = true;
throw new Error('QA renderer startup failure');
}
return original.call(this, type, ...args);
};
window.__restoreContext = () => {
HTMLCanvasElement.prototype.getContext = original;
};
});
const historyLength = await page.evaluate(() => history.length);
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await page.waitForFunction(() => window.__refusedContext === true);
await ready('navara');
// A failed switch leaves the session usable on the engine that worked and
// adds no history entry.
const survived = (kind) =>
page.evaluate(
(kind, length) => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
return (
window.__startDocument === document &&
!document.querySelector('.engine-switch-retry') &&
engine.kind === kind &&
new URL(location.href).searchParams.get('engine') === kind &&
history.length === length &&
dataManager === window.__handoff.dataManager &&
dataManager.layers.get(window.__qaAircraft.layerId) ===
window.__handoff.entry &&
styleManager.getCockpitState().active &&
styleManager.getCockpitState().visionMode === 'thermal' &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId
);
},
kind,
historyLength,
);
await page.evaluate(() => window.__restoreContext());
await strict.check(
page,
layerId,
'cold Cesium failure rolls back to Navara: same session, cockpit, aircraft, no history entry',
await survived('navara'),
);
console.log(
'PASS cold Cesium failure rolled back to Navara with the same flight, cockpit and history',
);
for (const kind of ['cesium', 'navara']) {
await strict.selectEngine(page, kind, HIDDEN_PICKER);
await ready(kind);
const result = await page.evaluate(() => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
const before = window.__handoff;
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const cockpit = styleManager.getCockpitState();
return {
sameOwner:
before.dataManager === dataManager &&
before.entry === entry &&
before.model === entry.module,
sameRow:
Boolean(before.row) &&
before.row ===
document.querySelector(
`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`,
),
sameTimer:
before.timer === entry.intervalId && entry.intervalId != null,
sourcePolls: window.__flightPolls - before.pollCount,
timerTicks: before.ticks,
stamp: entry.module.getStats().lastUpdate,
initialStamp: before.timestamp,
id: cockpit.subject?.id,
active: cockpit.active,
mode: cockpit.visionMode,
hud: !document.querySelector('#cockpit-hud').hidden,
drawn: entry.module
.qaDrawn()
.some((row) => row.id === window.__qaAircraft.aircraftId),
height: engine.camera.height(),
};
});
assert.ok(
result.sameOwner &&
result.sameTimer &&
result.sameRow &&
result.active &&
result.hud &&
result.drawn,
JSON.stringify(result),
);
assert.equal(result.id, aircraftId);
assert.equal(result.mode, 'thermal');
assert.ok(
result.sourcePolls <= result.timerTicks,
'switch must not add a source request',
);
if (result.timerTicks === 0) {
assert.equal(result.stamp, result.initialStamp);
}
await strict.passed(
page,
layerId,
'same manager entry, model, row, poll timer, cockpit, thermal mode, HUD and drawn aircraft',
result,
);
// Polling goes on after the switch: the same timer ticks and each tick
// reads the source once (no tick may be skipped, none added).
const polls = await page.evaluate(async (layerId) => {
const entry = window.__godsEyeView.dataManager.layers.get(layerId);
const every =
Number(entry.module.refreshInterval) ||
Number(entry.module.updateInterval);
const start = {
polls: window.__flightPolls,
ticks: window.__handoff.ticks,
stamp: entry.module.getStats().lastUpdate,
};
const deadline = Date.now() + 2.5 * every + 5000;
while (window.__handoff.ticks - start.ticks < 2 && Date.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 250));
// Let the last tick's request land.
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
every,
ticks: window.__handoff.ticks - start.ticks,
polls: window.__flightPolls - start.polls,
updated: entry.module.getStats().lastUpdate !== start.stamp,
sameTimer: entry.intervalId === window.__handoff.timer,
};
}, layerId);
await strict.check(
page,
layerId,
'polling continues after the switch: two timer ticks, one source read each, fresh data',
polls.every > 0 &&
polls.ticks >= 2 &&
polls.polls === polls.ticks &&
polls.updated &&
polls.sameTimer,
polls,
);
await page.waitForFunction(
() => Math.abs(window.__godsEyeView.engine.camera.height() - 9700) < 1000,
);
console.log(
`PASS ${kind} cockpit, aircraft, HUD, manager and timer (${result.sourcePolls} polls since entry)`,
);
}
// The other direction, warm: the parked Navara renderer fails to resume on
// the way back from Cesium. The session rolls back to Cesium, then an
// ordinary retry starts Navara cold.
await page.evaluate(() => {
window.__parkedNavara = window.__godsEyeView.engine;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await ready('cesium');
const lengthOnCesium = await page.evaluate(() => {
const request = window.__parkedNavara.render.request;
window.__parkedNavara.render.request = function (...args) {
window.__parkedNavara.render.request = request;
window.__refusedResume = true;
throw new Error('QA parked renderer failure');
};
return history.length;
});
await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
await page.waitForFunction(() => window.__refusedResume === true);
await ready('cesium');
assert.ok(
(await page.evaluate(
(length) => history.length === length,
lengthOnCesium,
)) &&
(await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
return (
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId &&
new URL(location.href).searchParams.get('engine') === 'cesium'
);
}),
'the retry restores the same session on Cesium',
);
await strict.passed(
page,
layerId,
'after both engines fail, the retry restores the same session',
);
console.log(
'PASS failed recovery offered the picker; the retry restored the session on Cesium',
);
await page.evaluate(() =>
window.__godsEyeView.styleManager.controlCockpit('exit'),
);
await page.waitForFunction(
() => !window.__godsEyeView.styleManager.getCockpitState().active,
);
// Out of the cockpit the dock shows the picker again: a switch with
// real input keeps the same entry and timer, the aircraft drawn, and the
// source polled on its timer.
})),
'a failed warm Navara resume must leave Cesium usable without a history entry',
);
await strict.passed(
page,
layerId,
'warm Navara failure rolls back to Cesium: same aircraft and cockpit, no history entry',
);
console.log(
'PASS warm Navara failure rolled back to Cesium with the same flight and cockpit',
);
await page.evaluate(() => {
window.__parkedCesium = window.__godsEyeView.engine;
});
await strict.selectEngine(page, 'navara', HIDDEN_PICKER);
await ready('navara');
assert.ok(
await page.evaluate(() => {
window.__handoff.ticks = 0;
window.__handoff.pollCount = window.__flightPolls;
window.__handoff.timer = window.__godsEyeView.dataManager.layers.get(
window.__qaAircraft.layerId,
).intervalId;
});
await strict.pick(page, 'navara');
await ready('navara');
const after = await page.evaluate(async () => {
const { dataManager } = window.__godsEyeView;
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const every =
Number(entry.module.refreshInterval) ||
Number(entry.module.updateInterval);
const deadline = Date.now() + 2.5 * every + 5000;
while (window.__handoff.ticks < 1 && Date.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 250));
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
same:
dataManager === window.__handoff.dataManager &&
entry === window.__handoff.entry,
timer: entry.intervalId === window.__handoff.timer,
drawn: entry.module
.qaDrawn()
.some((row) => row.id === window.__qaAircraft.aircraftId),
ticks: window.__handoff.ticks,
polls: window.__flightPolls - window.__handoff.pollCount,
const { engine, styleManager } = window.__godsEyeView;
return (
engine !== window.__parkedNavara &&
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId
);
}),
'the retry starts a new Navara renderer on the same session',
);
console.log('PASS retry after the warm failure started Navara cold');
// Both engines fail: the warm Cesium resume and the warm Navara recovery.
// No engine runs, but the error screen offers the engine picker, and a
// retry starts Cesium cold on the same session without a reload.
const lengthBeforeDoubleFailure = await page.evaluate(() => {
window.__failResume = true;
for (const engine of [window.__godsEyeView.engine, window.__parkedCesium]) {
const request = engine.render.request;
engine.render.request = function (reason, ...args) {
if (reason === 'engine-resume' && window.__failResume) {
window.__refusedResumes = (window.__refusedResumes ?? 0) + 1;
throw new Error('QA parked renderer failure');
}
return request.call(this, reason, ...args);
};
}
window.__startDocument = document;
return history.length;
});
await strict.selectEngine(page, 'cesium', HIDDEN_PICKER);
await page
.waitForSelector('#loading-screen .engine-switch-retry button', {
visible: true,
timeout: 90000,
})
.catch(async (error) => {
console.log(
'no retry picker',
JSON.stringify(
await page.evaluate(() => ({
refused: window.__refusedResumes,
engine: window.__godsEyeView?.engine?.kind ?? null,
retry: !!document.querySelector('.engine-switch-retry'),
loading: document.querySelector('#loading-screen')?.className,
status: document.querySelector('#loading-screen .loader-status')
?.textContent,
address: location.search,
})),
),
);
throw error;
});
await strict.check(
page,
layerId,
'picker switch keeps the entry, timer and drawn aircraft; polling continues',
after.same &&
after.timer &&
after.drawn &&
after.ticks >= 1 &&
after.polls === after.ticks,
after,
);
assert.deepEqual(errors, []);
},
);
assert.ok(
await page.evaluate(
(length) =>
window.__refusedResumes === 2 &&
// No globe runs (the page's debug object may stay, without one).
!window.__godsEyeView?.engine &&
history.length === length &&
!document
.querySelector('#loading-screen')
.classList.contains('hidden') &&
/QA parked renderer failure/.test(
document.querySelector('#loading-screen .loader-status').textContent,
),
lengthBeforeDoubleFailure,
),
'a failed recovery shows its error and the engine picker ' +
JSON.stringify(
await page.evaluate(() => ({
refused: window.__refusedResumes,
app: !!window.__godsEyeView,
engine: window.__godsEyeView?.engine?.kind ?? null,
history: history.length,
loading: document.querySelector('#loading-screen')?.className,
status: document.querySelector('#loading-screen .loader-status')
?.textContent,
})),
),
);
await page.evaluate(() => {
window.__failResume = false;
});
await page.click(
'#loading-screen .engine-switch-retry [data-engine="cesium"]',
);
await ready('cesium');
assert.ok(
await page.evaluate(() => {
const { dataManager, styleManager } = window.__godsEyeView;
return (
window.__startDocument === document &&
!document.querySelector('.engine-switch-retry') &&
dataManager === window.__handoff.dataManager &&
dataManager.layers.get(window.__qaAircraft.layerId) ===
window.__handoff.entry &&
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id ===
window.__qaAircraft.aircraftId &&
new URL(location.href).searchParams.get('engine') === 'cesium'
);
}),
'the retry restores the same session on Cesium',
);
await strict.passed(
page,
layerId,
'after both engines fail, the retry restores the same session',
);
console.log(
'PASS failed recovery offered the picker; the retry restored the session on Cesium',
);
await page.evaluate(() =>
window.__godsEyeView.styleManager.controlCockpit('exit'),
);
await page.waitForFunction(
() => !window.__godsEyeView.styleManager.getCockpitState().active,
);
// Out of the cockpit the dock shows the picker again: a switch with
// real input keeps the same entry and timer, the aircraft drawn, and the
// source polled on its timer.
await page.evaluate(() => {
window.__handoff.ticks = 0;
window.__handoff.pollCount = window.__flightPolls;
window.__handoff.timer = window.__godsEyeView.dataManager.layers.get(
window.__qaAircraft.layerId,
).intervalId;
});
await strict.pick(page, 'navara');
await ready('navara');
const after = await page.evaluate(async () => {
const { dataManager } = window.__godsEyeView;
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const every =
Number(entry.module.refreshInterval) ||
Number(entry.module.updateInterval);
const deadline = Date.now() + 2.5 * every + 5000;
while (window.__handoff.ticks < 1 && Date.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 250));
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
same:
dataManager === window.__handoff.dataManager &&
entry === window.__handoff.entry,
timer: entry.intervalId === window.__handoff.timer,
drawn: entry.module
.qaDrawn()
.some((row) => row.id === window.__qaAircraft.aircraftId),
ticks: window.__handoff.ticks,
polls: window.__flightPolls - window.__handoff.pollCount,
};
});
await strict.check(
page,
layerId,
'picker switch keeps the entry, timer and drawn aircraft; polling continues',
after.same &&
after.timer &&
after.drawn &&
after.ticks >= 1 &&
after.polls === after.ticks,
after,
);
assert.deepEqual(errors, []);
});
+10 -26
View File
@@ -1,19 +1,18 @@
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
base.hash =
'v=2&lat=41.5340&lon=-90.5070&alt=20000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
const strict = createStrict({ origin: base.origin, layers: ['local-adsb'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
await p.evaluateOnNewDocument(() => {
const raw = window.fetch;
window.fetch = function (input, ...args) {
@@ -53,21 +52,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
);
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -107,8 +92,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.evaluate(() => {
window.qaSelections = [];
});
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
await p.waitForFunction(() =>
window.qaSelections.includes('local-adsb:abc123'),
);
+7 -20
View File
@@ -9,7 +9,11 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import sharp from 'sharp';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
const LAT = 37.7749;
const LON = -122.4194;
@@ -31,8 +35,6 @@ const RING = [
await strict.run(
{
args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
@@ -43,21 +45,7 @@ await strict.run(
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base.href);
const ready = async (kind) => {
await page.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 120000 },
kind,
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(page, 'navara');
assert.equal(
await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
@@ -172,8 +160,7 @@ await strict.run(
if (
(await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind
) {
await strict.pick(page, kind);
await ready(kind);
await strict.switchTo(page, kind);
}
await wait(2500);
const state = await page.evaluate(() => ({
+140 -143
View File
@@ -1,4 +1,4 @@
// The app's Bloom on Navara (Stage 6 B2): the Display bloom glows the
// The app's Bloom on Navara: the Display bloom glows the
// Navara frame (brighter, more at a higher slider), it sits after the
// overlays and before the visual styles, Cesium's dead zone draws nothing,
// turning it off restores the frame exactly, and on Cesium the scene's own
@@ -53,153 +53,150 @@ const setBloom = (page, enabled, intensityPct) =>
intensityPct,
);
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
// The same sun on every run: the frame's brightness decides what blooms
// and what each style's signature reads.
await pinDaytime(page);
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
});
await page.setViewport({ width: 1280, height: 800 });
await page.goto(visualPageUrl(base.origin, 'navara'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
await strict.run({}, async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
// The same sun on every run: the frame's brightness decides what blooms
// and what each style's signature reads.
await pinDaytime(page);
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
});
await page.setViewport({ width: 1280, height: 800 });
await page.goto(visualPageUrl(base.origin, 'navara'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
const initial = await page.evaluate(() => {
const { engine } = window.__godsEyeView;
return { bloom: engine.render.bloom(), pipeline: engine.pipeline() };
});
check(
'Navara blooms itself and starts with bloom off',
initial.bloom.enabled === false && !initial.pipeline?.bloomShown,
initial,
);
const normal = await capture(page);
save('normal', normal);
const normalStats = stats(normal);
const initial = await page.evaluate(() => {
const { engine } = window.__godsEyeView;
return { bloom: engine.render.bloom(), pipeline: engine.pipeline() };
});
check(
'Navara blooms itself and starts with bloom off',
initial.bloom.enabled === false && !initial.pipeline?.bloomShown,
initial,
);
const normal = await capture(page);
save('normal', normal);
const normalStats = stats(normal);
const full = await setBloom(page, true, 200);
check(
'the slider at 200 reaches the engine at full strength',
full.engine.enabled === true &&
full.engine.strength === 1 &&
full.pipeline.bloomShown === true,
full,
);
const order = full.pipeline.passes ?? [];
const at = order.indexOf('gevBloom');
check(
'bloom runs after the scene and overlays, just before the final copy',
at >= 0 &&
order[at + 1] === 'final' &&
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
(key) => !order.includes(key) || order.indexOf(key) < at,
),
order,
);
const glowFull = await capture(page);
save('bloom-200', glowFull);
const fullChange = diff(normal, glowFull);
const fullStats = stats(glowFull);
check(
'bloom at 200 glows the frame brighter',
fullChange.mean > 3 && luma(fullStats) > luma(normalStats) + 2,
{
...fullChange,
normalLuma: luma(normalStats),
bloomLuma: luma(fullStats),
},
);
const full = await setBloom(page, true, 200);
check(
'the slider at 200 reaches the engine at full strength',
full.engine.enabled === true &&
full.engine.strength === 1 &&
full.pipeline.bloomShown === true,
full,
);
const order = full.pipeline.passes ?? [];
const at = order.indexOf('gevBloom');
check(
'bloom runs after the scene and overlays, just before the final copy',
at >= 0 &&
order[at + 1] === 'final' &&
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
(key) => !order.includes(key) || order.indexOf(key) < at,
),
order,
);
const glowFull = await capture(page);
save('bloom-200', glowFull);
const fullChange = diff(normal, glowFull);
const fullStats = stats(glowFull);
check(
'bloom at 200 glows the frame brighter',
fullChange.mean > 3 && luma(fullStats) > luma(normalStats) + 2,
{
...fullChange,
normalLuma: luma(normalStats),
bloomLuma: luma(fullStats),
},
);
await setBloom(page, true, 100);
const glowHalf = await capture(page);
save('bloom-100', glowHalf);
const halfChange = diff(normal, glowHalf);
check(
'a lower slider glows less',
halfChange.mean > 0.5 && halfChange.mean < fullChange.mean,
{ half: halfChange.mean, full: fullChange.mean },
);
await setBloom(page, true, 100);
const glowHalf = await capture(page);
save('bloom-100', glowHalf);
const halfChange = diff(normal, glowHalf);
check(
'a lower slider glows less',
halfChange.mean > 0.5 && halfChange.mean < fullChange.mean,
{ half: halfChange.mean, full: fullChange.mean },
);
const dead = await setBloom(page, true, 10);
const deadFrame = await capture(page);
check(
"Cesium's dead zone: the toggle on, nothing glows",
dead.engine.enabled === false &&
dead.pipeline.bloomShown === false &&
diff(normal, deadFrame).mean === 0,
{ engine: dead.engine, diff: diff(normal, deadFrame) },
);
const dead = await setBloom(page, true, 10);
const deadFrame = await capture(page);
check(
"Cesium's dead zone: the toggle on, nothing glows",
dead.engine.enabled === false &&
dead.pipeline.bloomShown === false &&
diff(normal, deadFrame).mean === 0,
{ engine: dead.engine, diff: diff(normal, deadFrame) },
);
// With a style: bloom first, then the style, then the final copy.
await setBloom(page, true, 200);
const styled = await pickStyle(page, 'noir', 'normal');
const styledOrder = styled.pipeline?.passes ?? [];
check(
'bloom runs before the visual styles',
styledOrder.indexOf('gevBloom') >= 0 &&
styledOrder.indexOf('gevBloom') + 1 ===
styledOrder.indexOf('gevStyles') &&
styledOrder.indexOf('gevStyles') + 1 === styledOrder.indexOf('final'),
styledOrder,
);
await pickStyle(page, 'normal', 'noir');
// With a style: bloom first, then the style, then the final copy.
await setBloom(page, true, 200);
const styled = await pickStyle(page, 'noir', 'normal');
const styledOrder = styled.pipeline?.passes ?? [];
check(
'bloom runs before the visual styles',
styledOrder.indexOf('gevBloom') >= 0 &&
styledOrder.indexOf('gevBloom') + 1 ===
styledOrder.indexOf('gevStyles') &&
styledOrder.indexOf('gevStyles') + 1 === styledOrder.indexOf('final'),
styledOrder,
);
await pickStyle(page, 'normal', 'noir');
const off = await setBloom(page, false, 200);
const offFrame = await capture(page);
check(
'turning bloom off restores the frame',
off.engine.enabled === false &&
off.pipeline.bloomShown === false &&
diff(normal, offFrame).mean === 0,
{ engine: off.engine, diff: diff(normal, offFrame) },
);
const off = await setBloom(page, false, 200);
const offFrame = await capture(page);
check(
'turning bloom off restores the frame',
off.engine.enabled === false &&
off.pipeline.bloomShown === false &&
diff(normal, offFrame).mean === 0,
{ engine: off.engine, diff: diff(normal, offFrame) },
);
// Cesium keeps its scene bloom stage; its engine has no bloom.
await strict.switchTo(page, 'cesium');
await freeze(page);
await setBloom(page, false, 200);
const cesiumNormal = await capture(page);
const cesium = await setBloom(page, true, 200);
const cesiumStage = await page.evaluate(() => {
const a = window.__godsEyeView;
const stage = a.viewer?.scene?.postProcessStages?.bloom;
return {
setBloom: typeof a.engine.render.setBloom,
enabled: stage?.enabled ?? null,
contrast: stage?.uniforms?.contrast ?? null,
};
});
const cesiumGlow = await capture(page);
save('cesium-bloom-200', cesiumGlow);
check(
"Cesium: its scene's bloom stage glows, the engine draws none",
cesium.engine === null &&
cesiumStage.setBloom === 'undefined' &&
cesiumStage.enabled !== false &&
diff(cesiumNormal, cesiumGlow).mean > 1,
{ stage: cesiumStage, diff: diff(cesiumNormal, cesiumGlow) },
);
await setBloom(page, false, 200);
const cesiumBack = diff(cesiumNormal, await capture(page));
check(
'Cesium: bloom off restores its frame',
cesiumBack.mean < 0.5,
cesiumBack,
);
// Cesium keeps its scene bloom stage; its engine has no bloom.
await strict.switchTo(page, 'cesium');
await freeze(page);
await setBloom(page, false, 200);
const cesiumNormal = await capture(page);
const cesium = await setBloom(page, true, 200);
const cesiumStage = await page.evaluate(() => {
const a = window.__godsEyeView;
const stage = a.viewer?.scene?.postProcessStages?.bloom;
return {
setBloom: typeof a.engine.render.setBloom,
enabled: stage?.enabled ?? null,
contrast: stage?.uniforms?.contrast ?? null,
};
});
const cesiumGlow = await capture(page);
save('cesium-bloom-200', cesiumGlow);
check(
"Cesium: its scene's bloom stage glows, the engine draws none",
cesium.engine === null &&
cesiumStage.setBloom === 'undefined' &&
cesiumStage.enabled !== false &&
diff(cesiumNormal, cesiumGlow).mean > 1,
{ stage: cesiumStage, diff: diff(cesiumNormal, cesiumGlow) },
);
await setBloom(page, false, 200);
const cesiumBack = diff(cesiumNormal, await capture(page));
check(
'Cesium: bloom off restores its frame',
cesiumBack.mean < 0.5,
cesiumBack,
);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'bloom.json'),
JSON.stringify(report, null, 2),
);
},
);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'bloom.json'),
JSON.stringify(report, null, 2),
);
});
+137 -142
View File
@@ -1,4 +1,4 @@
// The cockpit weather clouds on Navara (Stage 6 C3): in cockpit mode with
// The cockpit weather clouds on Navara: in cockpit mode with
// the cockpit weather on, Navara's own volumetric clouds take their cover
// from the weather under the camera (clear: none; overcast: a heavy deck),
// with no second canvas or WebGL context; leaving the cockpit gives the look
@@ -101,152 +101,147 @@ const coverage = (page) =>
() => window.__godsEyeView.engine.render.cloudCover?.().coverage,
);
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.evaluateOnNewDocument((utc) => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
const offset = Date.parse(utc) - Date.now();
const RealDate = Date;
class ShiftedDate extends RealDate {
constructor(...args) {
if (args.length === 0) super(RealDate.now() + offset);
else super(...args);
}
static now() {
return RealDate.now() + offset;
}
await strict.run({}, async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.evaluateOnNewDocument((utc) => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
const offset = Date.parse(utc) - Date.now();
const RealDate = Date;
class ShiftedDate extends RealDate {
constructor(...args) {
if (args.length === 0) super(RealDate.now() + offset);
else super(...args);
}
globalThis.Date = ShiftedDate;
}, MIDDAY_UTC);
await page.setViewport({ width: 1280, height: 800 });
await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
static now() {
return RealDate.now() + offset;
}
}
globalThis.Date = ShiftedDate;
}, MIDDAY_UTC);
await page.setViewport({ width: 1280, height: 800 });
await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
const setup = await page.evaluate(() => {
const a = window.__godsEyeView;
return {
cover: a.engine.render.cloudCover?.() ?? null,
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length,
};
});
check(
'Navara takes the cockpit cloud cover itself, no cloud canvas',
setup.cover?.coverage === null && !setup.canvas,
setup,
);
const lookOwn = await converged(page, 'look-own');
await weatherToggle(page, true);
await cockpit(page, true);
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage === 0,
{ timeout: 10000, polling: 100 },
);
check('clear weather in the cockpit: no cloud cover', asked.length >= 1, {
requests: asked.length,
});
const clear = await converged(page, 'cockpit-clear');
// Overcast 30 km east: the movement makes the weather stale.
weather = WEATHER.overcast;
await page.evaluate(() => {
const { engine } = window.__godsEyeView;
const place = engine.camera.pose().cartographic;
const east =
place.longitude + 30000 / (6378137 * Math.cos(place.latitude));
const { sin, cos } = Math;
// WGS84 Earth-fixed position of (east, latitude, height).
const a = 6378137;
const e2 = 0.00669437999014;
const n = a / Math.sqrt(1 - e2 * sin(place.latitude) ** 2);
const h = place.height;
engine.camera.setView({
destination: {
x: (n + h) * cos(place.latitude) * cos(east),
y: (n + h) * cos(place.latitude) * sin(east),
z: (n * (1 - e2) + h) * sin(place.latitude),
},
orientation: { heading: Math.PI / 2, pitch: -0.14, roll: 0 },
});
});
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage > 0.4,
{ timeout: 15000, polling: 100 },
);
const overcastCover = await coverage(page);
const overcast = await converged(page, 'cockpit-overcast');
const greyed = {
lookOwn: saturation(lookOwn),
clear: saturation(clear),
overcast: saturation(overcast),
clearToOvercast: diff(clear, overcast).mean,
};
check(
'overcast weather covers the view with Navara clouds, clear leaves it open',
overcastCover > 0.4 &&
greyed.overcast < greyed.clear * 0.6 &&
greyed.lookOwn < greyed.clear &&
greyed.clearToOvercast > 15,
{ overcastCover, greyed, requests: asked.length },
);
const contexts = await page.evaluate(() => ({
const setup = await page.evaluate(() => {
const a = window.__godsEyeView;
return {
cover: a.engine.render.cloudCover?.() ?? null,
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length,
}));
check(
'no second canvas or WebGL context in the cockpit',
!contexts.canvas && contexts.canvases === setup.canvases,
{ before: setup.canvases, after: contexts },
);
};
});
check(
'Navara takes the cockpit cloud cover itself, no cloud canvas',
setup.cover?.coverage === null && !setup.canvas,
setup,
);
const lookOwn = await converged(page, 'look-own');
await cockpit(page, false);
check(
'leaving the cockpit gives the look its own clouds back',
(await coverage(page)) === null,
);
await weatherToggle(page, true);
await cockpit(page, true);
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage === 0,
{ timeout: 10000, polling: 100 },
);
check('clear weather in the cockpit: no cloud cover', asked.length >= 1, {
requests: asked.length,
});
const clear = await converged(page, 'cockpit-clear');
// Cesium keeps its own cockpit cloud canvas; its engine draws none.
await strict.switchTo(page, 'cesium');
await freeze(page);
weather = WEATHER.overcast;
await cockpit(page, true);
await page.waitForFunction(
() =>
Number(
document.getElementById('cockpit-cloud-effects')?.dataset
.cloudStrength,
) > 0.3,
{ timeout: 15000, polling: 100 },
);
const cesium = await page.evaluate(() => {
const canvas = document.getElementById('cockpit-cloud-effects');
return {
setCloudCover: typeof window.__godsEyeView.engine.render.setCloudCover,
strength: Number(canvas.dataset.cloudStrength),
cockpit: canvas.dataset.cockpit,
};
// Overcast 30 km east: the movement makes the weather stale.
weather = WEATHER.overcast;
await page.evaluate(() => {
const { engine } = window.__godsEyeView;
const place = engine.camera.pose().cartographic;
const east = place.longitude + 30000 / (6378137 * Math.cos(place.latitude));
const { sin, cos } = Math;
// WGS84 Earth-fixed position of (east, latitude, height).
const a = 6378137;
const e2 = 0.00669437999014;
const n = a / Math.sqrt(1 - e2 * sin(place.latitude) ** 2);
const h = place.height;
engine.camera.setView({
destination: {
x: (n + h) * cos(place.latitude) * cos(east),
y: (n + h) * cos(place.latitude) * sin(east),
z: (n * (1 - e2) + h) * sin(place.latitude),
},
orientation: { heading: Math.PI / 2, pitch: -0.14, roll: 0 },
});
check(
'Cesium: the cockpit draws its own cloud canvas, the engine none',
cesium.setCloudCover === 'undefined' && cesium.cockpit === 'true',
cesium,
);
await cockpit(page, false);
await weatherToggle(page, false);
});
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage > 0.4,
{ timeout: 15000, polling: 100 },
);
const overcastCover = await coverage(page);
const overcast = await converged(page, 'cockpit-overcast');
const greyed = {
lookOwn: saturation(lookOwn),
clear: saturation(clear),
overcast: saturation(overcast),
clearToOvercast: diff(clear, overcast).mean,
};
check(
'overcast weather covers the view with Navara clouds, clear leaves it open',
overcastCover > 0.4 &&
greyed.overcast < greyed.clear * 0.6 &&
greyed.lookOwn < greyed.clear &&
greyed.clearToOvercast > 15,
{ overcastCover, greyed, requests: asked.length },
);
const contexts = await page.evaluate(() => ({
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length,
}));
check(
'no second canvas or WebGL context in the cockpit',
!contexts.canvas && contexts.canvases === setup.canvases,
{ before: setup.canvases, after: contexts },
);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'cockpit-clouds.json'),
JSON.stringify(report, null, 2),
);
},
);
await cockpit(page, false);
check(
'leaving the cockpit gives the look its own clouds back',
(await coverage(page)) === null,
);
// Cesium keeps its own cockpit cloud canvas; its engine draws none.
await strict.switchTo(page, 'cesium');
await freeze(page);
weather = WEATHER.overcast;
await cockpit(page, true);
await page.waitForFunction(
() =>
Number(
document.getElementById('cockpit-cloud-effects')?.dataset.cloudStrength,
) > 0.3,
{ timeout: 15000, polling: 100 },
);
const cesium = await page.evaluate(() => {
const canvas = document.getElementById('cockpit-cloud-effects');
return {
setCloudCover: typeof window.__godsEyeView.engine.render.setCloudCover,
strength: Number(canvas.dataset.cloudStrength),
cockpit: canvas.dataset.cockpit,
};
});
check(
'Cesium: the cockpit draws its own cloud canvas, the engine none',
cesium.setCloudCover === 'undefined' && cesium.cockpit === 'true',
cesium,
);
await cockpit(page, false);
await weatherToggle(page, false);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'cockpit-clouds.json'),
JSON.stringify(report, null, 2),
);
});
-2
View File
@@ -32,8 +32,6 @@ const AREA = [
await strict.run(
{
args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
-2
View File
@@ -136,8 +136,6 @@ const FILES = [
await strict.run(
{
args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
+7 -20
View File
@@ -10,7 +10,11 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import sharp from 'sharp';
import { fromDegrees } from '../src/geo/cartesian3.js';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0';
@@ -31,8 +35,6 @@ const EDGE = { x: 1250, y: 560, width: 26, height: 90 };
await strict.run(
{
args: [
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
@@ -43,21 +45,7 @@ await strict.run(
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base.href);
const ready = async (kind) => {
await page.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 120000 },
kind,
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(page, 'navara');
assert.equal(
await page.evaluate(() =>
Boolean(window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__),
@@ -92,8 +80,7 @@ await strict.run(
if (
(await page.evaluate(() => window.__godsEyeView.engine.kind)) !== kind
) {
await strict.pick(page, kind);
await ready(kind);
await strict.switchTo(page, kind);
}
await wait(2500);
const mask = await page.evaluate(() => {
+175 -179
View File
@@ -1,4 +1,4 @@
// The Cyber HUD sonar on Navara (Stage 6 C2): with the Cyber HUD on, the
// The Cyber HUD sonar on Navara: with the Cyber HUD on, the
// sweep lights the contacts it passes and dims the rest to the opacity
// floor, by each contact's place on screen exactly as the CPU sampler of
// src/cyberSonar.js rates it; the sweep turns; with the sweep off every
@@ -98,193 +98,189 @@ const framesIn = (page, ms) =>
const mean = (values) =>
values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length);
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
await strict.run({}, async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
});
await page.setViewport({ width: WIDTH, height: HEIGHT });
await page.goto(visualPageUrl(base.origin, 'navara'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
const places = contactPlaces();
const placed = await page.evaluate((places) => {
const { engine } = window.__godsEyeView;
const set = engine.movers.createSet({
id: 'qa-sonar-contacts',
kind: 'point',
capacity: places.length,
depthTest: 'always',
point: { sizePx: 14, outlinePx: 0 },
});
await page.setViewport({ width: WIDTH, height: HEIGHT });
await page.goto(visualPageUrl(base.origin, 'navara'), {
waitUntil: 'domcontentloaded',
window.__qaSonarSet = set;
let missing = 0;
places.forEach((place, index) => {
const position = engine.camera.pickEllipsoid(place);
if (!position) missing++;
set.set(index, { position, color: [1, 1, 1, 1] });
});
await waitForEngine(page, 'navara');
await freeze(page);
set.setCount(places.length);
set.commit();
return { missing, sonar: engine.render.contactSonar?.() ?? null };
}, places);
check(
'Navara treats contacts itself; the sonar starts off',
placed.missing === 0 && placed.sonar?.enabled === false,
placed,
);
const baseline = await read(page, places);
const places = contactPlaces();
const placed = await page.evaluate((places) => {
const { engine } = window.__godsEyeView;
const set = engine.movers.createSet({
id: 'qa-sonar-contacts',
kind: 'point',
capacity: places.length,
depthTest: 'always',
point: { sizePx: 14, outlinePx: 0 },
});
window.__qaSonarSet = set;
let missing = 0;
places.forEach((place, index) => {
const position = engine.camera.pickEllipsoid(place);
if (!position) missing++;
set.set(index, { position, color: [1, 1, 1, 1] });
});
set.setCount(places.length);
set.commit();
return { missing, sonar: engine.render.contactSonar?.() ?? null };
}, places);
check(
'Navara treats contacts itself; the sonar starts off',
placed.missing === 0 && placed.sonar?.enabled === false,
placed,
await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
styleManager._setHudVariant('cyber');
// Normal keeps the contacts' own colours; the HUD stays on (in Normal
// it would hide itself, and the sweep with it).
styleManager.setStyle('normal');
styleManager.hud.setMode('on');
styleManager._setCyberSonarEnabled(true);
styleManager._setCyberSonarSetting('opacity', 35);
});
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
const theme = await page.evaluate(() => ({
theme: document.documentElement.dataset.uiTheme,
gpu: document.documentElement.dataset.cyberSonarGpu,
hud: document.getElementById('intel-hud')?.classList.contains('active'),
}));
check(
'the Cyber HUD runs with the contact adapter',
theme.theme === 'cyber' && theme.gpu === 'supported' && theme.hud,
theme,
);
const sweeps = [];
for (const wait of [0, 1300, 1300]) {
await new Promise((resolve) => setTimeout(resolve, wait));
const { luma, sonar } = await read(page, places);
const expected = places.map(({ x, y }) =>
sonarFactorAt(x, y, WIDTH, HEIGHT, sonar),
);
const baseline = await read(page, places);
await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
styleManager._setHudVariant('cyber');
// Normal keeps the contacts' own colours; the HUD stays on (in Normal
// it would hide itself, and the sweep with it).
styleManager.setStyle('normal');
styleManager.hud.setMode('on');
styleManager._setCyberSonarEnabled(true);
styleManager._setCyberSonarSetting('opacity', 35);
const lit = luma.filter((_, i) => expected[i] > 0.9);
const dim = luma.filter((_, i) => expected[i] <= sonar.opacity / 100);
sweeps.push({
spread: Math.max(...luma.slice(0, 32)) - Math.min(...luma.slice(0, 32)),
angle: sonar.angleDeg,
lit: lit.length,
litLuma: mean(lit),
dimLuma: mean(dim),
brightest: luma.indexOf(Math.max(...luma)),
expectedBrightest: expected.indexOf(Math.max(...expected)),
sonar,
});
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
const theme = await page.evaluate(() => ({
theme: document.documentElement.dataset.uiTheme,
gpu: document.documentElement.dataset.cyberSonarGpu,
hud: document.getElementById('intel-hud')?.classList.contains('active'),
}));
check(
'the Cyber HUD runs with the contact adapter',
theme.theme === 'cyber' && theme.gpu === 'supported' && theme.hud,
theme,
`the sweep at ${Math.round(sonar.angleDeg)}° lights the contacts it passes, the rest sit at the floor`,
sonar.enabled &&
sonar.active &&
sonar.opacity === 35 &&
lit.length >= 1 &&
dim.length >= 8 &&
mean(lit) > mean(dim) + 20 &&
expected[places.length - 1] === 0.35,
sweeps.at(-1),
);
}
check(
'the sweep turns: another contact is lit',
new Set(sweeps.map((s) => s.expectedBrightest)).size > 1 &&
new Set(sweeps.map((s) => s.brightest)).size > 1,
sweeps.map(({ angle, brightest, expectedBrightest }) => ({
angle,
brightest,
expectedBrightest,
})),
);
const cadence = await framesIn(page, 1500);
check(
'a parked scene draws at the fleet cadence while the sweep runs over contacts',
cadence >= 8,
{ frames: cadence },
);
const sweeps = [];
for (const wait of [0, 1300, 1300]) {
await new Promise((resolve) => setTimeout(resolve, wait));
const { luma, sonar } = await read(page, places);
const expected = places.map(({ x, y }) =>
sonarFactorAt(x, y, WIDTH, HEIGHT, sonar),
);
const lit = luma.filter((_, i) => expected[i] > 0.9);
const dim = luma.filter((_, i) => expected[i] <= sonar.opacity / 100);
sweeps.push({
spread: Math.max(...luma.slice(0, 32)) - Math.min(...luma.slice(0, 32)),
angle: sonar.angleDeg,
lit: lit.length,
litLuma: mean(lit),
dimLuma: mean(dim),
brightest: luma.indexOf(Math.max(...luma)),
expectedBrightest: expected.indexOf(Math.max(...expected)),
sonar,
});
check(
`the sweep at ${Math.round(sonar.angleDeg)}° lights the contacts it passes, the rest sit at the floor`,
sonar.enabled &&
sonar.active &&
sonar.opacity === 35 &&
lit.length >= 1 &&
dim.length >= 8 &&
mean(lit) > mean(dim) + 20 &&
expected[places.length - 1] === 0.35,
sweeps.at(-1),
);
}
check(
'the sweep turns: another contact is lit',
new Set(sweeps.map((s) => s.expectedBrightest)).size > 1 &&
new Set(sweeps.map((s) => s.brightest)).size > 1,
sweeps.map(({ angle, brightest, expectedBrightest }) => ({
angle,
brightest,
expectedBrightest,
})),
);
const cadence = await framesIn(page, 1500);
check(
'a parked scene draws at the fleet cadence while the sweep runs over contacts',
cadence >= 8,
{ frames: cadence },
);
await page.evaluate(() =>
window.__godsEyeView.styleManager._setCyberSonarEnabled(false),
);
const neutral = await read(page, places);
const spread =
Math.max(...neutral.luma.slice(0, 32)) -
Math.min(...neutral.luma.slice(0, 32));
await new Promise((resolve) => setTimeout(resolve, 300));
const idle = await framesIn(page, 1500);
check(
'with the sweep off every contact takes the neutral factor and the scene rests',
neutral.sonar.enabled &&
!neutral.sonar.active &&
// What still differs is the map under the translucent contacts.
spread < Math.min(...sweeps.map((sweep) => sweep.spread)) * 0.6 &&
idle <= 3,
{ spread, sweeping: sweeps.map((sweep) => sweep.spread), frames: idle },
);
await page.evaluate(() =>
window.__godsEyeView.styleManager._setCyberSonarEnabled(false),
);
const neutral = await read(page, places);
const spread =
Math.max(...neutral.luma.slice(0, 32)) -
Math.min(...neutral.luma.slice(0, 32));
await new Promise((resolve) => setTimeout(resolve, 300));
const idle = await framesIn(page, 1500);
check(
'with the sweep off every contact takes the neutral factor and the scene rests',
neutral.sonar.enabled &&
!neutral.sonar.active &&
// What still differs is the map under the translucent contacts.
spread < Math.min(...sweeps.map((sweep) => sweep.spread)) * 0.6 &&
idle <= 3,
{ spread, sweeping: sweeps.map((sweep) => sweep.spread), frames: idle },
);
await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
styleManager._setCyberSonarEnabled(true);
styleManager._setHudVariant('tactical');
styleManager.setStyle('normal');
styleManager.hud.setMode('auto');
});
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
const after = await read(page, places);
const drift = Math.max(
...after.luma.map((value, i) => Math.abs(value - baseline.luma[i])),
);
check(
'leaving the Cyber HUD gives the contacts back unchanged',
after.sonar.enabled === false && drift < 3,
{ drift },
);
await page.evaluate(() => window.__qaSonarSet.destroy());
await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
styleManager._setCyberSonarEnabled(true);
styleManager._setHudVariant('tactical');
styleManager.setStyle('normal');
styleManager.hud.setMode('auto');
});
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
const after = await read(page, places);
const drift = Math.max(
...after.luma.map((value, i) => Math.abs(value - baseline.luma[i])),
);
check(
'leaving the Cyber HUD gives the contacts back unchanged',
after.sonar.enabled === false && drift < 3,
{ drift },
);
await page.evaluate(() => window.__qaSonarSet.destroy());
// Cesium keeps its command adapter; its engine draws no sonar.
await strict.switchTo(page, 'cesium');
await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
styleManager._setHudVariant('cyber');
});
await page.waitForFunction(
() => document.documentElement.dataset.cyberSonarGpu,
{ timeout: 10000 },
);
const cesium = await page.evaluate(() => ({
setContactSonar: typeof window.__godsEyeView.engine.render.setContactSonar,
gpu: document.documentElement.dataset.cyberSonarGpu,
}));
check(
'Cesium: the command adapter treats contacts, the engine none',
cesium.setContactSonar === 'undefined' && cesium.gpu === 'supported',
cesium,
);
await page.evaluate(() =>
window.__godsEyeView.styleManager._setHudVariant('tactical'),
);
// Cesium keeps its command adapter; its engine draws no sonar.
await strict.switchTo(page, 'cesium');
await page.evaluate(() => {
const { styleManager } = window.__godsEyeView;
styleManager._setHudVariant('cyber');
});
await page.waitForFunction(
() => document.documentElement.dataset.cyberSonarGpu,
{ timeout: 10000 },
check('no page errors', errors.length === 0, errors);
if (outDir) {
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
path.join(outDir, 'sonar.json'),
JSON.stringify(report, null, 2),
);
const cesium = await page.evaluate(() => ({
setContactSonar:
typeof window.__godsEyeView.engine.render.setContactSonar,
gpu: document.documentElement.dataset.cyberSonarGpu,
}));
check(
'Cesium: the command adapter treats contacts, the engine none',
cesium.setContactSonar === 'undefined' && cesium.gpu === 'supported',
cesium,
);
await page.evaluate(() =>
window.__godsEyeView.styleManager._setHudVariant('tactical'),
);
check('no page errors', errors.length === 0, errors);
if (outDir) {
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
path.join(outDir, 'sonar.json'),
JSON.stringify(report, null, 2),
);
}
},
);
}
});
+253 -270
View File
@@ -1,4 +1,4 @@
// The app's visual styles on Navara (Stage 6 B1): each of CRT, NVG, FLIR,
// The app's visual styles on Navara: each of CRT, NVG, FLIR,
// Anime and Noir filters the Navara frame (pixels and a style-specific
// signature against Normal), switching styles crossfades (both styles drawn
// at once mid-way, the fade completes), Normal restores the frame, the
@@ -45,291 +45,274 @@ const check = checker(report, assert);
const save = (name, pixels) => saveCapture(outDir, name, pixels);
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
// The same sun on every run: the frame's brightness decides what blooms
// and what each style's signature reads.
await pinDaytime(page);
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
});
await page.setViewport({ width: 1280, height: 800 });
await page.goto(pageUrl('navara'), { waitUntil: 'domcontentloaded' });
await waitForEngine(page, 'navara');
await freeze(page);
await strict.run({}, async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
// The same sun on every run: the frame's brightness decides what blooms
// and what each style's signature reads.
await pinDaytime(page);
await page.evaluateOnNewDocument(() => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
});
await page.setViewport({ width: 1280, height: 800 });
await page.goto(pageUrl('navara'), { waitUntil: 'domcontentloaded' });
await waitForEngine(page, 'navara');
await freeze(page);
const capabilities = await page.evaluate(() => {
const { engine } = window.__godsEyeView;
return {
styles: engine.render.visualStyles,
pipeline: engine.pipeline(),
};
});
const capabilities = await page.evaluate(() => {
const { engine } = window.__godsEyeView;
return {
styles: engine.render.visualStyles,
pipeline: engine.pipeline(),
};
});
check(
'Navara draws every app style itself',
JSON.stringify(capabilities.styles) === JSON.stringify(STYLES),
capabilities,
);
check(
'no style pass shows on Normal',
capabilities.pipeline?.shown === false,
capabilities.pipeline,
);
// The military styles' presets set sharpening and detection (as on
// Cesium); Normal keeps them, so they are put back before comparing.
const display = await page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
const normal = await capture(page);
const normalAgain = await capture(page);
const noise = diff(normal, normalAgain);
check('Normal is a still frame to compare against', noise.mean < 0.5, noise);
save('normal', normal);
const normalStats = stats(normal);
let previous = 'normal';
for (const name of STYLES) {
const fade = await pickStyle(page, name, previous);
check(
'Navara draws every app style itself',
JSON.stringify(capabilities.styles) === JSON.stringify(STYLES),
capabilities,
`${name}: the crossfade completes`,
fade.pending === 0 && fade.ms < 1500 && fade.active === name,
{ ms: fade.ms, pending: fade.pending },
);
const between = fade.samples.filter(
([, next]) => next > 0.02 && next < 0.98,
);
check(
'no style pass shows on Normal',
capabilities.pipeline?.shown === false,
capabilities.pipeline,
`${name}: the engine fades it in over several frames`,
between.length >= 1,
{ frames: fade.samples.length, between: between.length },
);
// The military styles' presets set sharpening and detection (as on
// Cesium); Normal keeps them, so they are put back before comparing.
const display = await page.evaluate(
() => window.__godsEyeView.styleManager.getControlState().sharpen,
);
const normal = await capture(page);
const normalAgain = await capture(page);
const noise = diff(normal, normalAgain);
check(
'Normal is a still frame to compare against',
noise.mean < 0.5,
noise,
);
save('normal', normal);
const normalStats = stats(normal);
let previous = 'normal';
for (const name of STYLES) {
const fade = await pickStyle(page, name, previous);
check(
`${name}: the crossfade completes`,
fade.pending === 0 && fade.ms < 1500 && fade.active === name,
{ ms: fade.ms, pending: fade.pending },
);
const between = fade.samples.filter(
([, next]) => next > 0.02 && next < 0.98,
if (previous !== 'normal') {
const both = fade.samples.filter(
([out, next]) => out > 0.05 && out < 0.95 && next > 0.05 && next < 0.95,
);
check(
`${name}: the engine fades it in over several frames`,
between.length >= 1,
{ frames: fade.samples.length, between: between.length },
`${name}: ${previous} and ${name} draw together mid-crossfade`,
both.length >= 1,
{ both: both.length },
);
if (previous !== 'normal') {
const both = fade.samples.filter(
([out, next]) =>
out > 0.05 && out < 0.95 && next > 0.05 && next < 0.95,
);
check(
`${name}: ${previous} and ${name} draw together mid-crossfade`,
both.length >= 1,
{ both: both.length },
);
}
const others = Object.entries(fade.intensities).filter(
([style, value]) => style !== name && value !== 0,
);
check(
`${name}: alone at full intensity once settled`,
fade.intensities[name] === 1 && others.length === 0,
fade.intensities,
);
const order = fade.pipeline?.passes ?? [];
const at = order.indexOf('gevStyles');
check(
`${name}: the style pass shows, last before the final copy and after the overlays`,
fade.pipeline?.shown === true &&
at >= 0 &&
order[at + 1] === 'final' &&
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
(key) => !order.includes(key) || order.indexOf(key) < at,
),
fade.pipeline,
);
const pixels = await capture(page);
save(name, pixels);
const styled = stats(pixels);
const change = diff(normal, pixels);
const shift = histogramShift(normalStats, styled);
check(
`${name}: the frame changes against Normal`,
change.mean > 6 && change.changedShare > 0.25,
{ ...change, histogramShift: shift },
);
const signature = {
// Night vision: phosphor green over red and blue.
surveillance: styled.g > styled.r * 1.3 && styled.g > styled.b * 1.3,
// FLIR white-hot (palette 0): grey.
thermal: styled.saturation < normalStats.saturation * 0.5,
// Noir: greyer, with a slight sepia, and vignetted corners.
noir:
styled.saturation < normalStats.saturation &&
styled.vignette < normalStats.vignette * 0.85,
// Anime: cel bands, more saturated.
anime:
styled.saturation > normalStats.saturation * 1.05 || shift > 0.05,
// CRT: pixelated, dithered, scanlined.
retro: shift > 0.05 || change.mean > 12,
}[name];
check(`${name}: the style's own look`, signature, {
normal: {
r: normalStats.r,
g: normalStats.g,
b: normalStats.b,
saturation: normalStats.saturation,
vignette: normalStats.vignette,
},
styled: {
r: styled.r,
g: styled.g,
b: styled.b,
saturation: styled.saturation,
vignette: styled.vignette,
},
histogramShift: shift,
});
previous = name;
}
// A style parameter reaches the Navara pass (FLIR's Ironbow palette).
await pickStyle(page, 'thermal', previous);
previous = 'thermal';
const grey = await capture(page);
const ironbow = await page.evaluate(() => {
const a = window.__godsEyeView;
a.styleManager.stages.thermal.uniforms.palette = 1;
return a.engine.render.style('thermal').uniforms.palette;
});
const coloured = await capture(page);
check(
'FLIR Ironbow reaches the engine and colours the frame',
ironbow === 1 && stats(coloured).saturation > stats(grey).saturation + 10,
{
grey: stats(grey).saturation,
ironbow: stats(coloured).saturation,
},
const others = Object.entries(fade.intensities).filter(
([style, value]) => style !== name && value !== 0,
);
await page.evaluate(() => {
window.__godsEyeView.styleManager.stages.thermal.uniforms.palette = 0;
});
const back = await pickStyle(page, 'normal', previous);
check(
'Normal: every style back at 0 and the pass hidden',
Object.values(back.intensities).every((v) => v === 0) &&
back.pipeline?.shown === false,
back,
`${name}: alone at full intensity once settled`,
fade.intensities[name] === 1 && others.length === 0,
fade.intensities,
);
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const restored = await capture(page);
save('normal-restored', restored);
const residue = diff(normal, restored);
const order = fade.pipeline?.passes ?? [];
const at = order.indexOf('gevStyles');
check(
'Normal restores the frame',
residue.mean < 1 && residue.changedShare < 0.01,
residue,
);
// The retired Snow opens on Normal; a link's style opens on Navara.
await page.goto(pageUrl('navara', '&style=snow'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
const snow = await page.evaluate(() => {
const a = window.__godsEyeView;
return {
active: a.styleManager.activeStyle,
intensities: a.engine.render.visualStyles.map(
(s) => a.engine.render.style(s).intensity,
`${name}: the style pass shows, last before the final copy and after the overlays`,
fade.pipeline?.shown === true &&
at >= 0 &&
order[at + 1] === 'final' &&
['toneMapping', 'gevSharpen', 'gevOverlay', 'smaa', 'fxaa'].every(
(key) => !order.includes(key) || order.indexOf(key) < at,
),
};
});
fade.pipeline,
);
const pixels = await capture(page);
save(name, pixels);
const styled = stats(pixels);
const change = diff(normal, pixels);
const shift = histogramShift(normalStats, styled);
check(
'style=snow opens on Normal',
snow.active === 'normal' && snow.intensities.every((v) => v === 0),
snow,
`${name}: the frame changes against Normal`,
change.mean > 6 && change.changedShare > 0.25,
{ ...change, histogramShift: shift },
);
await page.goto(pageUrl('navara', `&style=${URL_NAMES.thermal}`), {
waitUntil: 'domcontentloaded',
const signature = {
// Night vision: phosphor green over red and blue.
surveillance: styled.g > styled.r * 1.3 && styled.g > styled.b * 1.3,
// FLIR white-hot (palette 0): grey.
thermal: styled.saturation < normalStats.saturation * 0.5,
// Noir: greyer, with a slight sepia, and vignetted corners.
noir:
styled.saturation < normalStats.saturation &&
styled.vignette < normalStats.vignette * 0.85,
// Anime: cel bands, more saturated.
anime: styled.saturation > normalStats.saturation * 1.05 || shift > 0.05,
// CRT: pixelated, dithered, scanlined.
retro: shift > 0.05 || change.mean > 12,
}[name];
check(`${name}: the style's own look`, signature, {
normal: {
r: normalStats.r,
g: normalStats.g,
b: normalStats.b,
saturation: normalStats.saturation,
vignette: normalStats.vignette,
},
styled: {
r: styled.r,
g: styled.g,
b: styled.b,
saturation: styled.saturation,
vignette: styled.vignette,
},
histogramShift: shift,
});
await waitForEngine(page, 'navara');
await page.waitForFunction(
() => window.__godsEyeView.engine.render.style('thermal').intensity === 1,
{ timeout: 10000, polling: 50 },
);
check('a FLIR link opens filtered on Navara', true);
previous = name;
}
// Cesium keeps its own post stages; its engine draws no styles. The
// switch is real input on the picker, in the same document.
await page.evaluate(() =>
window.__godsEyeView.styleManager.setStyle('normal'),
);
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
await strict.switchTo(page, 'cesium');
await freeze(page);
// The address may still carry the FLIR link: Normal first.
await pickStyle(page, 'normal', 'thermal');
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const cesiumNormal = await capture(page);
const cesium = await pickStyle(page, 'thermal', 'normal');
const cesiumState = await page.evaluate(() => {
const a = window.__godsEyeView;
const stage = a.styleManager.stages.thermal;
return {
setStyle: typeof a.engine.render.setStyle,
stageName: stage.name,
enabled: stage.enabled,
intensity: stage.uniforms.intensity,
inScene: a.viewer?.scene?.postProcessStages?.contains?.(stage) ?? null,
};
});
const cesiumThermal = await capture(page);
save('cesium-normal', cesiumNormal);
save('cesium-thermal', cesiumThermal);
if (outDir)
await page.screenshot({
path: path.join(outDir, 'cesium-thermal-page.png'),
});
check(
'Cesium: FLIR is its own post stage, the engine draws none',
cesium.pending === 0 &&
cesiumState.setStyle === 'undefined' &&
cesiumState.enabled === true &&
cesiumState.intensity === 1 &&
cesiumState.inScene !== false,
cesiumState,
);
const cesiumChange = diff(cesiumNormal, cesiumThermal);
check(
'Cesium: FLIR changes its frame',
cesiumChange.mean > 6,
cesiumChange,
);
await pickStyle(page, 'normal', 'thermal');
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const cesiumBack = diff(cesiumNormal, await capture(page));
check(
'Cesium: Normal restores its frame',
cesiumBack.mean < 1.5,
cesiumBack,
);
// A style parameter reaches the Navara pass (FLIR's Ironbow palette).
await pickStyle(page, 'thermal', previous);
previous = 'thermal';
const grey = await capture(page);
const ironbow = await page.evaluate(() => {
const a = window.__godsEyeView;
a.styleManager.stages.thermal.uniforms.palette = 1;
return a.engine.render.style('thermal').uniforms.palette;
});
const coloured = await capture(page);
check(
'FLIR Ironbow reaches the engine and colours the frame',
ironbow === 1 && stats(coloured).saturation > stats(grey).saturation + 10,
{
grey: stats(grey).saturation,
ironbow: stats(coloured).saturation,
},
);
await page.evaluate(() => {
window.__godsEyeView.styleManager.stages.thermal.uniforms.palette = 0;
});
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'visual-styles.json'),
JSON.stringify(report, null, 2),
);
},
);
const back = await pickStyle(page, 'normal', previous);
check(
'Normal: every style back at 0 and the pass hidden',
Object.values(back.intensities).every((v) => v === 0) &&
back.pipeline?.shown === false,
back,
);
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const restored = await capture(page);
save('normal-restored', restored);
const residue = diff(normal, restored);
check(
'Normal restores the frame',
residue.mean < 1 && residue.changedShare < 0.01,
residue,
);
// The retired Snow opens on Normal; a link's style opens on Navara.
await page.goto(pageUrl('navara', '&style=snow'), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
const snow = await page.evaluate(() => {
const a = window.__godsEyeView;
return {
active: a.styleManager.activeStyle,
intensities: a.engine.render.visualStyles.map(
(s) => a.engine.render.style(s).intensity,
),
};
});
check(
'style=snow opens on Normal',
snow.active === 'normal' && snow.intensities.every((v) => v === 0),
snow,
);
await page.goto(pageUrl('navara', `&style=${URL_NAMES.thermal}`), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await page.waitForFunction(
() => window.__godsEyeView.engine.render.style('thermal').intensity === 1,
{ timeout: 10000, polling: 50 },
);
check('a FLIR link opens filtered on Navara', true);
// Cesium keeps its own post stages; its engine draws no styles. The
// switch is real input on the picker, in the same document.
await page.evaluate(() =>
window.__godsEyeView.styleManager.setStyle('normal'),
);
await page.waitForFunction(
() => window.__godsEyeView.styleManager.transitions.size === 0,
);
await strict.switchTo(page, 'cesium');
await freeze(page);
// The address may still carry the FLIR link: Normal first.
await pickStyle(page, 'normal', 'thermal');
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const cesiumNormal = await capture(page);
const cesium = await pickStyle(page, 'thermal', 'normal');
const cesiumState = await page.evaluate(() => {
const a = window.__godsEyeView;
const stage = a.styleManager.stages.thermal;
return {
setStyle: typeof a.engine.render.setStyle,
stageName: stage.name,
enabled: stage.enabled,
intensity: stage.uniforms.intensity,
inScene: a.viewer?.scene?.postProcessStages?.contains?.(stage) ?? null,
};
});
const cesiumThermal = await capture(page);
save('cesium-normal', cesiumNormal);
save('cesium-thermal', cesiumThermal);
if (outDir)
await page.screenshot({
path: path.join(outDir, 'cesium-thermal-page.png'),
});
check(
'Cesium: FLIR is its own post stage, the engine draws none',
cesium.pending === 0 &&
cesiumState.setStyle === 'undefined' &&
cesiumState.enabled === true &&
cesiumState.intensity === 1 &&
cesiumState.inScene !== false,
cesiumState,
);
const cesiumChange = diff(cesiumNormal, cesiumThermal);
check('Cesium: FLIR changes its frame', cesiumChange.mean > 6, cesiumChange);
await pickStyle(page, 'normal', 'thermal');
await page.evaluate((sharpen) => {
const { styleManager } = window.__godsEyeView;
styleManager._setDetectionMode?.('OFF');
styleManager.setSharpen(sharpen);
}, display);
const cesiumBack = diff(cesiumNormal, await capture(page));
check('Cesium: Normal restores its frame', cesiumBack.mean < 1.5, cesiumBack);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'visual-styles.json'),
JSON.stringify(report, null, 2),
);
});
+11 -32
View File
@@ -1,6 +1,11 @@
import sharp from 'sharp';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -54,18 +59,8 @@ const strict = createStrict({
},
],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b, { shaderErrors: true });
const assets = [];
p.on('request', (req) => {
const path = new URL(req.url()).pathname;
@@ -77,21 +72,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
assets.push(path);
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { tileset, dataManager, sceneDirector } = window.__godsEyeView;
if (!sceneDirector?.listScenes().length)
@@ -118,8 +99,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
};
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
}
const state = await p.evaluate(() => {
const { dataManager } = window.__godsEyeView;
@@ -184,8 +164,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await p.waitForFunction(
() => window.qaEvent.getPlaybackState().progress > 0.53,
);
await strict.pick(p, 'cesium');
await ready('cesium');
await strict.switchTo(p, 'cesium');
const live = await p.evaluate(() => window.qaEvent.getPlaybackState());
await strict.check(
p,
+10 -30
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js';
import sharp from 'sharp';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -11,34 +16,10 @@ const strict = createStrict({
origin: base.origin,
layers: ['bhote-koshi-locator'],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b, { shaderErrors: true });
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -51,8 +32,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
});
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
}
const { same, stats, params, at } = await p.evaluate(
(world) => {
+10 -26
View File
@@ -1,6 +1,11 @@
import sharp from 'sharp';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
// Drone airspace, OSM places, sea marks and ski pistes on both engines, over
// Navara → Cesium → Navara: each draws over the view centre, and the switch
@@ -57,14 +62,8 @@ const strict = createStrict({
tileRoute('https://www.opensnowmap.org/*', 'ski-pistes'),
],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
await p.evaluateOnNewDocument((ring) => {
const fetch = window.fetch.bind(window);
window.qaZoneReads = 0;
@@ -101,20 +100,6 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
};
}, RING);
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
const setEnabled = (id, on) =>
p.evaluate(
(layerId, show) =>
@@ -177,7 +162,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const greener = (on, off) =>
2 * on[1] - on[0] - on[2] > 2 * off[1] - off[0] - off[2] + 10;
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(() => {
const { dataManager } = window.__godsEyeView;
window.qaModels = Object.fromEntries(
@@ -190,8 +175,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
for (const [leg, kind] of ['navara', 'cesium', 'navara'].entries()) {
const readsBeforeSwitch = await p.evaluate(() => window.qaZoneReads);
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
const same = await p.evaluate(() =>
Object.entries(window.qaModels).every(
([id, model]) =>
+77 -80
View File
@@ -6,86 +6,83 @@ const url = localPreview(process.argv[2]);
url.search = '?engine=navara&welcome=0';
url.hash = 'v=2&lat=30.27&lon=-97.74&alt=12000&pitch=-90&map=sentinel2&l=';
const strict = createStrict({ origin: url.origin, layers: ['radio'] });
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await installRadioFixture(page);
await page.goto(url.href);
await page.waitForFunction(
() =>
window.__godsEyeView?.engine?.kind === 'navara' &&
document.querySelector('#loading-screen.hidden'),
);
await page.evaluate(async (id) => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
await styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Expected keyless preview');
window.qaLayer = dataManager.layers.get('radio').module;
await dataManager.setEnabled('radio', true, { origin: 'user' });
}, stations[0].id);
await page.waitForFunction(() => window.qaLayer?.getStats().count === 750);
await page.evaluate((id) => {
window.qaLayer.selectStation(id, {
autoplay: true,
focus: false,
origin: 'user',
});
window.qaLayer.setRadioVolume(0.36);
}, stations[10].id);
await page.waitForFunction(
() =>
window.__qaRadio.plays.length === 1 &&
window.qaLayer.getUIState().audioState === 'playing',
);
const first = await page.evaluate(() => {
window.qaAudio = window.qaPlayer;
window.qaSnapshot = window.qaLayer.getAcceptedCatalogSnapshot();
return window.__qaRadio.reads;
await strict.run({}, async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await installRadioFixture(page);
await page.goto(url.href);
await page.waitForFunction(
() =>
window.__godsEyeView?.engine?.kind === 'navara' &&
document.querySelector('#loading-screen.hidden'),
);
await page.evaluate(async (id) => {
const { dataManager, styleManager, tileset } = window.__godsEyeView;
await styleManager.initialRestorePromise;
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Expected keyless preview');
window.qaLayer = dataManager.layers.get('radio').module;
await dataManager.setEnabled('radio', true, { origin: 'user' });
}, stations[0].id);
await page.waitForFunction(() => window.qaLayer?.getStats().count === 750);
await page.evaluate((id) => {
window.qaLayer.selectStation(id, {
autoplay: true,
focus: false,
origin: 'user',
});
for (const kind of ['cesium', 'navara']) {
await strict.switchTo(page, kind);
const state = await page.evaluate(() => ({
same:
window.qaLayer ===
window.__godsEyeView.dataManager.layers.get('radio').module,
player: window.qaAudio === window.qaPlayer,
snapshot:
window.qaSnapshot === window.qaLayer.getAcceptedCatalogSnapshot(),
plays: window.__qaRadio.plays.length,
reads: window.__qaRadio.reads,
state: window.qaLayer.getUIState(),
count: window.qaLayer.getStats().count,
}));
console.log(
kind,
state.count,
state.state.selected?.id,
state.state.audioState,
state.plays,
state.reads,
);
assert.ok(state.same && state.player && state.snapshot);
assert.equal(state.plays, 1);
assert.equal(state.reads, first);
assert.equal(state.count, 750);
assert.equal(state.state.audioState, 'playing');
assert.equal(state.state.selected.id, stations[10].id);
assert.equal(state.state.volume, 0.36);
// The fixture's HTMLMediaElement.prototype.play counts play() calls: the
// switch must not start the stream again.
await strict.passed(
page,
'radio',
'same model, player element and catalog snapshot; 750 stations, one catalog read, no second play(), same station playing at volume 0.36',
);
}
assert.deepEqual(errors, []);
window.qaLayer.setRadioVolume(0.36);
}, stations[10].id);
await page.waitForFunction(
() =>
window.__qaRadio.plays.length === 1 &&
window.qaLayer.getUIState().audioState === 'playing',
);
const first = await page.evaluate(() => {
window.qaAudio = window.qaPlayer;
window.qaSnapshot = window.qaLayer.getAcceptedCatalogSnapshot();
return window.__qaRadio.reads;
});
for (const kind of ['cesium', 'navara']) {
await strict.switchTo(page, kind);
const state = await page.evaluate(() => ({
same:
window.qaLayer ===
window.__godsEyeView.dataManager.layers.get('radio').module,
player: window.qaAudio === window.qaPlayer,
snapshot:
window.qaSnapshot === window.qaLayer.getAcceptedCatalogSnapshot(),
plays: window.__qaRadio.plays.length,
reads: window.__qaRadio.reads,
state: window.qaLayer.getUIState(),
count: window.qaLayer.getStats().count,
}));
console.log(
'PASS one live radio player, one station directory and the same selection',
kind,
state.count,
state.state.selected?.id,
state.state.audioState,
state.plays,
state.reads,
);
},
);
assert.ok(state.same && state.player && state.snapshot);
assert.equal(state.plays, 1);
assert.equal(state.reads, first);
assert.equal(state.count, 750);
assert.equal(state.state.audioState, 'playing');
assert.equal(state.state.selected.id, stations[10].id);
assert.equal(state.state.volume, 0.36);
// The fixture's HTMLMediaElement.prototype.play counts play() calls: the
// switch must not start the stream again.
await strict.passed(
page,
'radio',
'same model, player element and catalog snapshot; 750 stations, one catalog read, no second play(), same station playing at volume 0.36',
);
}
assert.deepEqual(errors, []);
console.log(
'PASS one live radio player, one station directory and the same selection',
);
});
+1 -1
View File
@@ -14,7 +14,7 @@ const strict = createStrict({
origin: url.origin,
layers: [missions ? 'rocket-launches' : 'satellites'],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
const p = await b.newPage();
const errors = [];
p.on('pageerror', (e) => {
+1 -1
View File
@@ -54,7 +54,7 @@ const getOpt = (name, dflt) => {
};
const BASE_URL = getOpt('--url', 'http://localhost:4173');
// --engine=cesium|navara (phase 3): without a viewer the layer is read through
// --engine=cesium|navara: without a viewer the layer is read through
// the QA probe (window.__godsEyeView.layers.drawn) and the military checks are
// skipped where the engine has no military layer.
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
+155 -171
View File
@@ -1,5 +1,9 @@
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
import { serveFixtures, newFixture } from './qa-street-level.mjs';
import { KV_LINE, KV_SEQUENCE_ID } from './fixtures/street-level/kartaView.mjs';
import {
@@ -28,186 +32,166 @@ const strict = createStrict({
'https://panoramax.openstreetmap.fr/*',
],
});
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const p = await browser.newPage();
await p.setViewport({ width: 1280, height: 900 });
const errors = [],
fixture = newFixture({ configured: true });
p.on('pageerror', (e) => {
errors.push(e.message);
console.error(e.message.split('?')[0]);
});
p.on('console', (m) => {
if (/shader|registerMeshPicker/i.test(m.text()) && m.type() === 'error')
errors.push(m.text());
});
await serveFixtures(p, fixture);
let photoRequests = 0;
p.on('request', (req) => {
if (
/\.jpg(?:$|\?)/i.test(new URL(req.url()).pathname) &&
/openstreetcam|kartaview|panoramax/.test(new URL(req.url()).hostname)
)
photoRequests++;
});
await p.goto(base.href);
const ready = async (k) => {
await p.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
k,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await p.evaluate(async (provider) => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
window.qaStreet = dataManager.layers.get('street-level').module;
await dataManager.setEnabled('street-level', true, { origin: 'user' });
window.qaStreet.setProviderEnabled(provider, true);
}, provider);
const until = async (fn) => {
try {
await p.waitForFunction(fn, { timeout: 20000 });
} catch (e) {
console.log(await p.evaluate(() => window.qaStreet.getUIState()));
throw e;
}
};
await until(() => window.qaStreet.getUIState().coverage.count > 0);
// The fixture drive passes under the middle of the canvas. Click its actual picked line.
const pickLine = async () => {
const xy = await p.evaluate(
(prefix) => {
const { engine } = window.__godsEyeView,
canvas = engine.host.canvas,
rect = canvas.getBoundingClientRect();
for (let dy = -30; dy <= 30; dy += 3)
for (let dx = -40; dx <= 40; dx++) {
const at = { x: rect.width / 2 + dx, y: rect.height / 2 + dy },
pick = engine.picker.pick(at),
id = pick?.id?.id ?? pick?.id;
if (typeof id === 'string' && id.startsWith(prefix))
return { x: at.x + rect.x, y: at.y + rect.y, id };
}
return null;
},
panorama ? 'pnx:seq:' : 'kv:line:',
);
assert.ok(xy, 'native ground line was pickable');
console.log('pick', xy);
await p.mouse.click(xy.x, xy.y);
await until(
() =>
window.qaStreet.getUIState().street.open &&
window.qaStreet.getUIState().sequence.images > 0 &&
(document.querySelector('#sl-viewer img')?.naturalWidth > 0 ||
document.querySelector('#sl-viewer canvas:not([hidden])')?.width >
0),
);
await strict.passed(
p,
'street-level',
'a click on the coverage line opens its sequence and photo',
xy,
);
return xy;
};
const xy = await pickLine();
if (panorama) {
const expected = panoramaxPictures().filter(
(p) => p.sequenceId === xy.id.slice('pnx:seq:'.length),
).length;
await p.waitForFunction(
(n) => window.qaStreet.getUIState().sequence.images === n,
{ timeout: 20000 },
expected,
);
await strict.run({}, async (browser) => {
const p = await browser.newPage();
await p.setViewport({ width: 1280, height: 900 });
const errors = [],
fixture = newFixture({ configured: true });
p.on('pageerror', (e) => {
errors.push(e.message);
console.error(e.message.split('?')[0]);
});
p.on('console', (m) => {
if (/shader|registerMeshPicker/i.test(m.text()) && m.type() === 'error')
errors.push(m.text());
});
await serveFixtures(p, fixture);
let photoRequests = 0;
p.on('request', (req) => {
if (
/\.jpg(?:$|\?)/i.test(new URL(req.url()).pathname) &&
/openstreetcam|kartaview|panoramax/.test(new URL(req.url()).hostname)
)
photoRequests++;
});
await p.goto(base.href);
await waitForEngine(p, 'navara');
await p.evaluate(async (provider) => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
throw Error('Expected keyless preview');
window.qaStreet = dataManager.layers.get('street-level').module;
await dataManager.setEnabled('street-level', true, { origin: 'user' });
window.qaStreet.setProviderEnabled(provider, true);
}, provider);
const until = async (fn) => {
try {
await p.waitForFunction(fn, { timeout: 20000 });
} catch (e) {
console.log(await p.evaluate(() => window.qaStreet.getUIState()));
throw e;
}
const first = await p.evaluate(() => {
window.streetPhoto =
document.querySelector('#sl-viewer canvas:not([hidden])') ??
document.querySelector('#sl-viewer img');
return window.qaStreet.getUIState();
});
assert.ok(
(panorama ? PNX_SEQUENCES : [KV_SEQUENCE_ID]).includes(
first.street.sequenceId,
),
};
await until(() => window.qaStreet.getUIState().coverage.count > 0);
// The fixture drive passes under the middle of the canvas. Click its actual picked line.
const pickLine = async () => {
const xy = await p.evaluate(
(prefix) => {
const { engine } = window.__godsEyeView,
canvas = engine.host.canvas,
rect = canvas.getBoundingClientRect();
for (let dy = -30; dy <= 30; dy += 3)
for (let dx = -40; dx <= 40; dx++) {
const at = { x: rect.width / 2 + dx, y: rect.height / 2 + dy },
pick = engine.picker.pick(at),
id = pick?.id?.id ?? pick?.id;
if (typeof id === 'string' && id.startsWith(prefix))
return { x: at.x + rect.x, y: at.y + rect.y, id };
}
return null;
},
panorama ? 'pnx:seq:' : 'kv:line:',
);
assert.ok(xy, 'native ground line was pickable');
console.log('pick', xy);
await p.mouse.click(xy.x, xy.y);
await until(
() =>
window.qaStreet.getUIState().street.open &&
window.qaStreet.getUIState().sequence.images > 0 &&
(document.querySelector('#sl-viewer img')?.naturalWidth > 0 ||
document.querySelector('#sl-viewer canvas:not([hidden])')?.width > 0),
);
const reads = photoRequests;
for (const kind of ['cesium', 'navara']) {
await strict.pick(p, kind);
await ready(kind);
// The new globe reloads coverage right after it attaches: compare once
// that load is over.
await until(
() =>
!window.qaStreet.getUIState().coverage.loading &&
window.qaStreet.getUIState().coverage.count > 0,
);
const state = await p.evaluate(() => ({
model:
window.__godsEyeView.dataManager.layers.get('street-level').module ===
window.qaStreet,
samePhoto:
document.querySelector('#sl-viewer')?.contains(window.streetPhoto) &&
window.streetPhoto.isConnected,
ui: window.qaStreet.getUIState(),
}));
console.log(
kind,
state.model,
state.samePhoto,
state.ui.street.imageId,
state.ui.sequence.images,
state.ui.coverage.count,
);
assert.equal(state.model, true);
assert.equal(state.samePhoto, true);
assert.ok(state.ui.street.open);
assert.equal(state.ui.street.imageId, first.street.imageId);
assert.equal(state.ui.sequence.images, first.sequence.images);
assert.deepEqual(state.ui.filter, first.filter);
assert.ok(state.ui.coverage.count > 0);
await strict.passed(
p,
'street-level',
'same model, the same mounted photo, image and sequence; coverage kept',
);
}
assert.equal(photoRequests, reads);
await strict.passed(
p,
'street-level',
'no photo requested again by the switches',
'a click on the coverage line opens its sequence and photo',
xy,
);
// Click-picking on Cesium: close the viewer, switch, click the line again.
await p.evaluate(() => window.qaStreet.closeViewer());
await until(() => !window.qaStreet.getUIState().street.open);
await strict.pick(p, 'cesium');
await ready('cesium');
return xy;
};
const xy = await pickLine();
if (panorama) {
const expected = panoramaxPictures().filter(
(p) => p.sequenceId === xy.id.slice('pnx:seq:'.length),
).length;
await p.waitForFunction(
(n) => window.qaStreet.getUIState().sequence.images === n,
{ timeout: 20000 },
expected,
);
}
const first = await p.evaluate(() => {
window.streetPhoto =
document.querySelector('#sl-viewer canvas:not([hidden])') ??
document.querySelector('#sl-viewer img');
return window.qaStreet.getUIState();
});
assert.ok(
(panorama ? PNX_SEQUENCES : [KV_SEQUENCE_ID]).includes(
first.street.sequenceId,
),
);
const reads = photoRequests;
for (const kind of ['cesium', 'navara']) {
await strict.switchTo(p, kind);
// The new globe reloads coverage right after it attaches: compare once
// that load is over.
await until(
() =>
!window.qaStreet.getUIState().coverage.loading &&
window.qaStreet.getUIState().coverage.count > 0,
);
await new Promise((resolve) => setTimeout(resolve, 1000));
await pickLine();
assert.deepEqual(errors, []);
assert.deepEqual(fixture.unknown, []);
const state = await p.evaluate(() => ({
model:
window.__godsEyeView.dataManager.layers.get('street-level').module ===
window.qaStreet,
samePhoto:
document.querySelector('#sl-viewer')?.contains(window.streetPhoto) &&
window.streetPhoto.isConnected,
ui: window.qaStreet.getUIState(),
}));
console.log(
'PASS native line pick, same mounted photo and sequence across two switches',
kind,
state.model,
state.samePhoto,
state.ui.street.imageId,
state.ui.sequence.images,
state.ui.coverage.count,
);
},
);
assert.equal(state.model, true);
assert.equal(state.samePhoto, true);
assert.ok(state.ui.street.open);
assert.equal(state.ui.street.imageId, first.street.imageId);
assert.equal(state.ui.sequence.images, first.sequence.images);
assert.deepEqual(state.ui.filter, first.filter);
assert.ok(state.ui.coverage.count > 0);
await strict.passed(
p,
'street-level',
'same model, the same mounted photo, image and sequence; coverage kept',
);
}
assert.equal(photoRequests, reads);
await strict.passed(
p,
'street-level',
'no photo requested again by the switches',
);
// Click-picking on Cesium: close the viewer, switch, click the line again.
await p.evaluate(() => window.qaStreet.closeViewer());
await until(() => !window.qaStreet.getUIState().street.open);
await strict.switchTo(p, 'cesium');
await until(
() =>
!window.qaStreet.getUIState().coverage.loading &&
window.qaStreet.getUIState().coverage.count > 0,
);
await new Promise((resolve) => setTimeout(resolve, 1000));
await pickLine();
assert.deepEqual(errors, []);
assert.deepEqual(fixture.unknown, []);
console.log(
'PASS native line pick, same mounted photo and sequence across two switches',
);
});
+1 -1
View File
@@ -41,7 +41,7 @@ const navaraCode = async (url) => {
};
const results = [];
strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
for (const start of ['navara', 'cesium']) {
const p = await b.newPage();
await p.setViewport({ width: 1280, height: 800 });
+1 -1
View File
@@ -32,7 +32,7 @@ const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
const strict = createStrict({ origin: base.origin, layers: [] });
const results = [];
strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
let p = null;
const errors = [];
// A fresh page on `from`; warm visits the other engine first.
+1 -1
View File
@@ -37,7 +37,7 @@ const other = (kind) => (kind === 'navara' ? 'cesium' : 'navara');
const strict = createStrict({ origin: base.origin, layers: [] });
const results = [];
strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
// A fresh page on `start` with the session; `warm` visits the other
// engine first so it is parked.
const open = async (start, warm) => {
+65 -79
View File
@@ -29,8 +29,21 @@ import {
import { createMissLoader, tileCacheFile } from './qa/tileCache.mjs';
export const ENGINES = Object.freeze(['cesium', 'navara']);
const RESERVED_PORTS = ['', '4173', '4185', '4186', '4187', '4188', '4189'];
const RESERVED = [...RESERVED_PORTS, '4194', '4196'];
/** Local ports that belong to the user's own servers: never a QA preview. */
export const RESERVED_PORTS = Object.freeze([
'',
'4173',
'4174',
'4185',
'4186',
'4187',
'4188',
'4189',
'4194',
'4196',
]);
/** Chrome flags every scenario's browser starts with (the GPU path). */
const CHROME_ARGS = Object.freeze(['--use-angle=metal', '--enable-gpu']);
/** The values of --layers=a,b and --evidence=FILE (null when absent). */
export function strictArguments(argv = process.argv) {
@@ -48,7 +61,7 @@ export function localPreview(href) {
const url = new URL(href);
if (
!['localhost', '127.0.0.1'].includes(url.hostname) ||
RESERVED.includes(url.port)
RESERVED_PORTS.includes(url.port)
)
throw Error('Supply an unreserved local keyless preview URL');
return url;
@@ -89,11 +102,18 @@ export async function installStrictNetwork(
// workers' own requests are never held.
const intercept = async (session, type) => {
session.on('Fetch.requestPaused', async ({ requestId, request }) => {
const fulfil = (status, headers, body) =>
// Every answer allows any origin; `noStore` keeps it out of the cache.
const fulfil = (status, contentType, body, { noStore = false } = {}) =>
session.send('Fetch.fulfillRequest', {
requestId,
responseCode: status,
responseHeaders: headers,
responseHeaders: [
...(contentType
? [{ name: 'Content-Type', value: contentType }]
: []),
...(noStore ? [{ name: 'Cache-Control', value: 'no-store' }] : []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
...(body == null
? {}
: { body: Buffer.from(body).toString('base64') }),
@@ -106,12 +126,7 @@ export async function installStrictNetwork(
const answer = await route.respond(request);
return await fulfil(
answer.status ?? 200,
[
...(answer.contentType
? [{ name: 'Content-Type', value: answer.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
answer.contentType,
answer.body ?? '',
);
}
@@ -128,14 +143,9 @@ export async function installStrictNetwork(
(ledger.probes[probe.pattern] ?? 0) + 1;
return await fulfil(
probe.status,
[
...(probe.contentType
? [{ name: 'Content-Type', value: probe.contentType }]
: []),
{ name: 'Cache-Control', value: 'no-store' },
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
probe.contentType,
probe.body ?? null,
{ noStore: true },
);
}
const accept = Object.entries(request.headers ?? {}).find(
@@ -172,16 +182,7 @@ export async function installStrictNetwork(
const host = new URL(request.url).host;
tiles.fetchedByHost[host] = (tiles.fetchedByHost[host] ?? 0) + 1;
}
await fulfil(
entry.status,
[
...(entry.contentType
? [{ name: 'Content-Type', value: entry.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
entry.body,
);
await fulfil(entry.status, entry.contentType, entry.body);
} catch (error) {
if (
/Invalid InterceptionId|closed|detached/i.test(String(error?.message))
@@ -326,21 +327,15 @@ export async function installStrictNetwork(
* each page gets an accessor in place of the global: reads before the script
* has assigned it are logged with their stack in `ledger.cesiumBeforeLoad`
* and answer `undefined`, as production would.
*
* GEV_QA_CESIUM_TRAP=record (a survey, not a check) answers such a read with
* the SDK instead, loaded synchronously on the first one, and logs every
* distinct read site until the page itself loads Cesium.js, so one run lists
* all the Navara paths that still need Cesium.
*/
async function trapCesiumGlobal(session, ledger) {
const binding = '__gevStrictCesiumRead';
const record = process.env.GEV_QA_CESIUM_TRAP === 'record';
session.on('Runtime.bindingCalled', (event) => {
if (event.name !== binding) return;
const stack = String(event.payload).slice(0, 1500);
const known = ledger.cesiumBeforeLoad.find((read) => read.stack === stack);
if (known) known.count++;
else if (ledger.cesiumBeforeLoad.length < (record ? 2000 : 50))
else if (ledger.cesiumBeforeLoad.length < 50)
ledger.cesiumBeforeLoad.push({ stack, count: 1 });
});
await session.send('Runtime.enable');
@@ -349,10 +344,8 @@ async function trapCesiumGlobal(session, ledger) {
await session.send('Page.addScriptToEvaluateOnNewDocument', {
source: `(() => {
if (window.top !== window) return;
const record = ${record};
const seen = new Set();
let sdk;
let pageLoaded = false;
const report = () => {
const limit = Error.stackTraceLimit;
Error.stackTraceLimit = 12;
@@ -366,20 +359,7 @@ async function trapCesiumGlobal(session, ledger) {
Object.defineProperty(window, 'Cesium', {
configurable: true,
get() {
if (!record) {
if (sdk === undefined) report();
return sdk;
}
if (!pageLoaded) {
pageLoaded = !!document.querySelector('script[src$="/Cesium.js"]');
if (!pageLoaded) report();
}
if (sdk === undefined) {
const request = new XMLHttpRequest();
request.open('GET', '/cesium/Cesium.js', false);
request.send();
(0, eval)(request.responseText);
}
if (sdk === undefined) report();
return sdk;
},
set(value) {
@@ -434,30 +414,8 @@ export async function pickEngine(
);
const opened = !(await expanded());
if (opened) {
await page.click('#control-panel-toggle').catch(async (error) => {
const state = await page.evaluate(() => {
const el = document.querySelector('#control-panel-toggle');
const box = el?.getBoundingClientRect();
const style = el && getComputedStyle(el);
const hit =
box &&
document.elementFromPoint(
box.x + box.width / 2,
box.y + box.height / 2,
);
return {
box: box && [box.x, box.y, box.width, box.height],
display: style?.display,
visibility: style?.visibility,
hidden: el?.closest('[hidden]')?.id ?? null,
hit: hit?.id || hit?.className || null,
body: document.body.className,
};
});
throw Error(
`cannot click the control panel toggle (${error.message}): ` +
JSON.stringify(state),
);
await page.click('#control-panel-toggle').catch((error) => {
throw Error(`cannot click the control panel toggle: ${error.message}`);
});
await page.waitForFunction(
() =>
@@ -592,6 +550,27 @@ export async function waitForEngine(page, kind, { timeout = 120000 } = {}) {
);
}
/**
* A new 1024×768 page of `browser` whose uncaught errors (and, with
* `shaderErrors`, shader and WebGL program errors on the console) land in
* `errors`; each uncaught error is also logged without its query string.
*/
export async function openPage(browser, { shaderErrors = false } = {}) {
const page = await browser.newPage();
await page.setViewport({ width: 1024, height: 768 });
const errors = [];
page.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
if (shaderErrors)
page.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
return { page, errors };
}
/**
* The strict context of one scenario run.
*
@@ -653,9 +632,16 @@ export function createStrict({
ledger,
extra,
timings,
/** Launch Chrome with the policy on every target. */
async launch(options = {}) {
const browser = await puppeteer.launch({ headless: true, ...options });
/**
* Launch headless Chrome with the policy on every target; `args` adds
* flags to CHROME_ARGS.
*/
async launch({ args = [], ...options } = {}) {
const browser = await puppeteer.launch({
headless: true,
...options,
args: [...CHROME_ARGS, ...args],
});
await installStrictNetwork(browser, {
origin,
fixtures,
@@ -665,7 +651,7 @@ export function createStrict({
});
return browser;
},
/** Launch, run `body(browser)`, close, then finish(). */
/** Launch with `options`, run `body(browser)`, close, then finish(). */
async run(options, body) {
const browser = await strict.launch(options);
let failed = null;
+1 -1
View File
@@ -20,7 +20,7 @@ import {
const base = localPreview(process.argv[2]);
const strict = createStrict({ origin: base.origin, layers: [] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1280, height: 800 });
const errors = [];
+1 -1
View File
@@ -20,7 +20,7 @@ const base = localPreview(process.argv[2]);
const strict = createStrict({ origin: base.origin, layers: [] });
const results = [];
strict.extra.contracts = results;
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
for (const start of ENGINES) {
const page = await b.newPage();
await page.setViewport({ width: 1280, height: 800 });
+1 -1
View File
@@ -24,7 +24,7 @@ import {
const base = localPreview(process.argv[2]);
const strict = createStrict({ origin: base.origin, layers: [] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1280, height: 800 });
const errors = [];
+1 -3
View File
@@ -156,9 +156,7 @@ strict.extra.soak = report;
for (let run = 1; run <= runs; run++) {
const start = run % 2 ? 'navara' : 'cesium';
const browser = await strict.launch({
args: ['--use-angle=metal', '--enable-gpu'],
});
const browser = await strict.launch();
const row = { run, start, swaps: [], checkpoints: [], errors: [] };
report.runs.push(row);
try {
+10 -30
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -27,39 +32,15 @@ const strict = createStrict({
origin: base.origin,
layers: ['telegeography-submarine-cables'],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b, { shaderErrors: true });
const reads = [];
p.on('request', (req) => {
const path = new URL(req.url()).pathname;
if (/\/(cable-geo|landing-point-geo).*\.json$/.test(path)) reads.push(path);
});
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -71,8 +52,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
});
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
}
await p.waitForFunction(
() => window.qaCables.getStats().referenceLabelCount > 0,
+10 -30
View File
@@ -1,7 +1,12 @@
import { trafficFixtureResponse } from './traffic-fixtures.mjs';
import sharp from 'sharp';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
import { STRICT_TILE_HOSTS } from './qa-map-policy.mjs';
const base = localPreview(process.argv[2]);
@@ -22,14 +27,8 @@ const strict = createStrict({
fixtures: ['https://tiles.maps.eox.at/*', 'https://tiles.openfreemap.org/*'],
tileHosts: STRICT_TILE_HOSTS.filter((host) => host !== 'tiles.maps.eox.at'),
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b, { shaderErrors: true });
const keyless = process.argv.includes('--simulation');
const reads = new Map();
@@ -73,26 +72,8 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}
return req.continue();
});
p.on('console', (msg) => {
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
errors.push(msg.text());
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { tileset, dataManager } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -199,8 +180,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
);
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
}
await waitTraffic();
const check = await p.evaluate(() => {
+1 -1
View File
@@ -31,7 +31,7 @@ const strict = createStrict({
'*://*.earthdata.nasa.gov/*',
],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
await strict.run({}, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1280, height: 900 });
const errors = [];
+1 -1
View File
@@ -38,7 +38,7 @@ const getOpt = (name, dflt) => {
};
const APP_URL = getOpt('--url', 'http://localhost:4173');
// --engine cesium|navara (phase 3): frame, settle and read the renderer
// --engine cesium|navara: frame, settle and read the renderer
// through window.__godsEyeView.engine (either engine) instead of the Cesium
// viewer; the URL gets ?engine=. Without it, as before.
const ENGINE = getOpt('--engine', null);
+1 -1
View File
@@ -19,7 +19,7 @@
* node scripts/qa-vessel-datum.mjs --ports rotterdam --url http://localhost:4173
* node scripts/qa-vessel-datum.mjs --engine navara --url 'http://localhost:PORT/?qaFeed=vessels:fixture'
*
* --engine cesium|navara (phase 3): the engine-neutral path on either engine,
* --engine cesium|navara: the engine-neutral path on either engine,
* through window.__godsEyeView.engine and the QA probe (layers.drawn: each
* chevron's drawn position and depth-test flag) instead of the Cesium
* viewer's primitives; the URL gets ?engine=. Without it, as before.
+52 -47
View File
@@ -1,19 +1,28 @@
// Recent imagery on a keyless preview, starting on Navara: the NASA catalog
// and imagery tiles are fixtures, and the image, A/B, swap, divider, opacity,
// basemap, toggle, failure, box-drag and zoom-to-fit checks read the rendered
// pixels; both pinned dates survive a switch to Cesium and back. The report
// and its screenshots go next to the evidence file (or to the temp directory).
//
// node scripts/qa-w2-imagery.mjs http://127.0.0.1:<PORT>
// [--layers=recent-imagery] [--evidence=FILE]
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import sharp from 'sharp';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
strictArguments,
} from './qa-strict-harness.mjs';
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
const args = process.argv.slice(2);
const option = (name, fallback) =>
args.includes(name) ? args[args.indexOf(name) + 1] : fallback;
const url = option('--url', 'http://127.0.0.1:4193');
const output = option('--report', '/private/tmp/gev-imagery.json');
if (
['4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
new URL(url).port,
)
)
throw new Error('Reserved QA port');
const { origin } = localPreview(process.argv[2]);
const url = origin + '/?engine=navara';
const { evidence } = strictArguments();
const output = evidence
? path.join(path.dirname(evidence), 'imagery-report.json')
: path.join(os.tmpdir(), 'gev-imagery.json');
const report = {
url,
checks: [],
@@ -26,7 +35,7 @@ let failLandsat = false,
// The engine the page runs, for the strict assertion manifest.
let engine = null;
const strict = createStrict({
origin: localPreview(url).origin,
origin,
layers: ['recent-imagery'],
// The scenario answers the NASA catalog and imagery tiles itself.
fixtures: ['*://*.earthdata.nasa.gov/*'],
@@ -48,8 +57,6 @@ const browser = await strict.launch({
headless: true,
args: [
'--no-sandbox',
'--use-angle=metal',
'--enable-gpu',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
@@ -63,7 +70,7 @@ try {
const text = redact(message.text());
const expected =
isExpectedPreviewSetupError(message, {
preview: args.includes('--preview'),
preview: true,
baseUrl: url,
}) ||
(failedTiles > 0 &&
@@ -231,37 +238,35 @@ try {
await sleep(2500);
image = await capture('ab');
check('A/B dates on opposite sides', red(image.left) && green(image.right));
if (args.includes('--switch')) {
const first = await page.evaluate(() => {
window.qaImageryOwner = window.__gevRecentImagery.layer;
return window.qaImageryOwner.diagnostics().pins;
});
const reads = () =>
report.requests.filter((r) => r.host === 'cmr.earthdata.nasa.gov').length;
const expectedReads = reads();
const initial = await page.evaluate(() => window.__godsEyeView.engine.kind);
for (const kind of [initial === 'navara' ? 'cesium' : 'navara', initial]) {
await strict.switchTo(page, kind);
engine = kind;
const state = await page.evaluate(() => ({
same: window.__gevRecentImagery.layer === window.qaImageryOwner,
...window.qaImageryOwner.diagnostics(),
}));
check(
kind + ' holds both original pins',
state.same &&
state.mode === 'ab' &&
state.pins.a === first.a &&
state.pins.b === first.b,
);
await sleep(1800);
const pixels = await capture('switch-' + kind);
check(
kind + ' renders both pinned dates',
red(pixels.left) && green(pixels.right),
);
check(kind + ' reuses the STAC search', reads() === expectedReads);
}
const first = await page.evaluate(() => {
window.qaImageryOwner = window.__gevRecentImagery.layer;
return window.qaImageryOwner.diagnostics().pins;
});
const reads = () =>
report.requests.filter((r) => r.host === 'cmr.earthdata.nasa.gov').length;
const expectedReads = reads();
const initial = await page.evaluate(() => window.__godsEyeView.engine.kind);
for (const kind of [initial === 'navara' ? 'cesium' : 'navara', initial]) {
await strict.switchTo(page, kind);
engine = kind;
const state = await page.evaluate(() => ({
same: window.__gevRecentImagery.layer === window.qaImageryOwner,
...window.qaImageryOwner.diagnostics(),
}));
check(
kind + ' holds both original pins',
state.same &&
state.mode === 'ab' &&
state.pins.a === first.a &&
state.pins.b === first.b,
);
await sleep(1800);
const pixels = await capture('switch-' + kind);
check(
kind + ' renders both pinned dates',
red(pixels.left) && green(pixels.right),
);
check(kind + ' reuses the STAC search', reads() === expectedReads);
}
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
image = await capture('swap');
+11 -27
View File
@@ -1,7 +1,12 @@
import { fromDegrees } from '../src/geo/cartesian3.js';
import sharp from 'sharp';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -16,14 +21,8 @@ const strict = createStrict({
'weather-cyclones',
],
});
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
const times = ['2026-11-02T12:00:00.000Z', '2026-11-02T12:05:00.000Z'];
const ids = ['weather-radar', 'weather-satellite', 'weather-lightning'];
const images = new Map(),
@@ -168,21 +167,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
console.log('GPU', msg.text().slice(0, 3000));
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
const waitTime = (ids, time) =>
p.waitForFunction(
(ids, time) =>
@@ -251,8 +236,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const polls = [...reads.entries()];
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
}
await waitTime(ids, times[0]);
const state = await p.evaluate(
@@ -364,7 +348,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await p.select('#globe-engine', kind);
await ready(kind);
await waitForEngine(p, kind);
}
await p.waitForFunction(
() =>
+10 -26
View File
@@ -1,6 +1,11 @@
import sharp from 'sharp';
import assert from 'node:assert/strict';
import { createStrict, localPreview } from './qa-strict-harness.mjs';
import {
createStrict,
localPreview,
openPage,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
base.search = '?engine=navara&welcome=0&look=cinematic';
@@ -9,14 +14,8 @@ base.hash =
// The fixture field is uniform, so the GPU seeds a fixed particle count.
const WIND_PATHS = 7200;
const strict = createStrict({ origin: base.origin, layers: ['wind'] });
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const p = await b.newPage();
await p.setViewport({ width: 1024, height: 768 });
const errors = [];
p.on('pageerror', (e) => {
errors.push(e.message);
console.log('error', e.message.split('?')[0]);
});
await strict.run({}, async (b) => {
const { page: p, errors } = await openPage(b);
const reads = [];
await p.setRequestInterception(true);
p.on('request', (req) => {
@@ -70,21 +69,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
}
});
await p.goto(base.href);
const ready = async (kind) => {
await p.waitForFunction(
(k) =>
window.__godsEyeView?.engine?.kind === k &&
!document.querySelector('#globe-engine')?.disabled &&
!document.querySelector('.engine-switch-frame') &&
document.querySelector('#loading-screen.hidden'),
{ timeout: 90000 },
kind,
);
await p.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
};
await ready('navara');
await waitForEngine(p, 'navara');
await p.evaluate(async () => {
const { dataManager, tileset } = window.__godsEyeView;
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
@@ -103,8 +88,7 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
const baseline = reads.length;
for (const kind of ['navara', 'cesium', 'navara']) {
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
await strict.pick(p, kind);
await ready(kind);
await strict.switchTo(p, kind);
}
await p.waitForFunction(
() =>
+4 -34
View File
@@ -5,19 +5,7 @@
// the session ids, listener counts and transport counters.
import puppeteer from 'puppeteer';
import { blockPaidQaMaps } from '../qa-map-policy.mjs';
const RESERVED_PORTS = new Set([
'',
'4173',
'4174',
'4185',
'4186',
'4187',
'4188',
'4189',
'4194',
'4196',
]);
import { localPreview, waitForEngine } from '../qa-strict-harness.mjs';
/** Paid or live model endpoints no switch gate may reach. */
export const BLOCKED_URLS = Object.freeze([
@@ -36,13 +24,7 @@ export const BLOCKED_URLS = Object.freeze([
/** The preview origin from argv, refusing the ports that belong to the user. */
export function previewBase(argument) {
const base = new URL(argument ?? '');
if (
!['localhost', '127.0.0.1'].includes(base.hostname) ||
RESERVED_PORTS.has(base.port)
)
throw new Error('Supply an unreserved local preview URL');
return base;
return localPreview(argument ?? '');
}
/** The page address: Boston on OSM, no welcome, on the given engine. */
@@ -247,20 +229,8 @@ export async function openSessionPage(browser, address) {
}
/** The engine runs, its picker is idle and the loading screen is gone. */
export async function waitReady(page, kind) {
await page.waitForFunction(
(kind) =>
window.__godsEyeView?.engine?.kind === kind &&
document.querySelector('#globe-engine')?.value === kind &&
!document.querySelector('#globe-engine').disabled &&
document.getElementById('loading-screen')?.classList.contains('hidden') &&
!document.querySelector('.engine-switch-frame'),
{ timeout: 90_000, polling: 100 },
kind,
);
await page.evaluate(
() => window.__godsEyeView.styleManager.initialRestorePromise,
);
export function waitReady(page, kind) {
return waitForEngine(page, kind, { timeout: 90_000 });
}
/**
+7 -88
View File
@@ -1,14 +1,12 @@
// Map tiles for headless QA: a disk cache shared by every run (in the system
// temp directory) and one fetch per missing tile, with backoff on HTTP 429,
// for the free tile hosts the QA pages load (OpenStreetMap imagery, the
// keyless re:earth terrain). Back-to-back QA runs otherwise reload the same
// tiles until those hosts rate-limit them. Only requests to these hosts are
// paused (CDP Fetch with URL patterns); page-wide request interception also
// held the Navara workers' requests and kept workers alive past their globe.
// Map tiles for headless QA: the disk cache entry of a tile request and one
// fetch per missing tile, with backoff on HTTP 429, for the free tile hosts
// the QA pages load (OpenStreetMap imagery, the keyless re:earth terrain).
// Back-to-back QA runs otherwise reload the same tiles until those hosts
// rate-limit them. The strict harness (scripts/qa-strict-harness.mjs) pauses
// only the requests to these hosts (CDP Fetch with URL patterns) and answers
// them from the cache in the system temp directory.
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
export const CACHED_TILE_HOSTS = Object.freeze([
@@ -119,82 +117,3 @@ export function createMissLoader({
},
};
}
/**
* Serves the page's requests to CACHED_TILE_HOSTS (or `hosts`) from the
* cache (filling it on a miss). Returns the counts.
*/
export async function installTileCache(
page,
{
dir = path.join(os.tmpdir(), 'gev-qa-tiles'),
hosts = CACHED_TILE_HOSTS,
} = {},
) {
fs.mkdirSync(dir, { recursive: true });
const stats = { hits: 0, fetched: 0, cancelled: 0, failed: 0, errors: {} };
const client = await page.createCDPSession();
const misses = createMissLoader();
client.on('Fetch.requestPaused', async ({ requestId, request }) => {
const accept = Object.entries(request.headers ?? {}).find(
([name]) => name.toLowerCase() === 'accept',
)?.[1];
const file =
request.method === 'GET'
? tileCacheFile(request.url, accept, dir, hosts)
: null;
try {
if (!file)
return await client.send('Fetch.continueRequest', { requestId });
let entry = null;
if (fs.existsSync(`${file}.json`)) {
entry = {
...JSON.parse(fs.readFileSync(`${file}.json`, 'utf8')),
body: fs.readFileSync(file),
};
stats.hits++;
} else {
const tile = await misses.load(request.url, accept);
// Only successes are cached; a 404 (no tile there) is passed on.
if (tile.status === 200) {
fs.writeFileSync(file, tile.body);
fs.writeFileSync(
`${file}.json`,
JSON.stringify({ status: 200, contentType: tile.contentType }),
);
stats.fetched++;
} else if (tile.status === 429) stats.failed++;
entry = tile;
}
await client.send('Fetch.fulfillRequest', {
requestId,
responseCode: entry.status,
responseHeaders: [
...(entry.contentType
? [{ name: 'Content-Type', value: entry.contentType }]
: []),
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
body: entry.body.toString('base64'),
});
} catch (error) {
// The page dropped the request meanwhile (a tile no longer in view).
if (/Invalid InterceptionId/.test(String(error?.message))) {
stats.cancelled++;
return;
}
stats.failed++;
const reason = String(error?.message ?? error).slice(0, 80);
stats.errors[reason] = (stats.errors[reason] ?? 0) + 1;
await client
.send('Fetch.failRequest', { requestId, errorReason: 'Failed' })
.catch(() => {});
}
});
await client.send('Fetch.enable', {
patterns: hosts.map((host) => ({
urlPattern: `https://${host}/*`,
})),
});
return stats;
}
+1 -1
View File
@@ -14,7 +14,7 @@ const LINE_ID = 'gev-draw-preview';
const DOTS_ID = 'gev-draw-vertices';
/** '#rrggbb' → rgba in 0..1. */
export function cssRgba(css, alpha = 1) {
function cssRgba(css, alpha = 1) {
const hex = Number.parseInt(String(css).slice(1), 16);
return [
((hex >> 16) & 255) / 255,
-11
View File
@@ -68,14 +68,3 @@ export function catalogControlServices(catalog) {
}),
);
}
/**
* Layer roles for controls on a page without data layers: one empty object
* per role, so the shell's optional calls
* (`layer.method?.()`) do nothing.
*/
export function absentLayerControlServices() {
return Object.fromEntries(
Object.keys(CONTROL_LAYER_IDS).map((role) => [role, {}]),
);
}
+1 -39
View File
@@ -1,10 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
absentLayerControlServices,
createLayerCatalog,
catalogControlServices,
} from './catalog.js';
import { createLayerCatalog, catalogControlServices } from './catalog.js';
import { createApplicationData } from './data.js';
const metadata = (layers) =>
@@ -158,40 +154,6 @@ test('controls bind catalog instances rather than similarly named defaults', ()
);
});
test('without data layers the controls get one empty stand-in per layer role', () => {
const ids = [
'traffic',
'flights',
'military',
'satellites',
'cctv',
'street-level',
'radio',
'bikeshare',
'transit',
'ais-live-vessels',
'military-awareness',
'military-installations',
'rocket-launches',
'local-adsb',
];
const layers = ids.map((id) => ({ id }));
const roles = Object.keys(
catalogControlServices(createLayerCatalog(layers, metadata(layers))),
);
const absent = absentLayerControlServices();
assert.deepEqual(Object.keys(absent), roles);
for (const role of roles) {
assert.deepEqual(Object.keys(absent[role]), [], role);
assert.equal(absent[role].subscribe, undefined);
}
assert.notEqual(absent.flightsLayer, absent.trafficLayer);
assert.notEqual(
absentLayerControlServices().flightsLayer,
absent.flightsLayer,
);
});
test('data setup without a viewer registers an empty catalog and no sonar scene', async (t) => {
const previous = globalThis.document;
globalThis.document = { getElementById: () => null };
+9 -9
View File
@@ -6,17 +6,17 @@ import { createAirQualityCesiumRenderer } from '../../layers/airQuality/render/c
import { createFirmsCesiumRenderer } from '../../layers/firms/render/cesium.js';
import { createEarthquakesCesiumRenderer } from '../../layers/earthquakes/render/cesium.js';
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
// Which engine draws which layer: the layer renderer registry (plan, "layer
// renderer interface") and the layer availability API built on it.
// Which engine draws which layer: the layer renderer registry and the layer
// availability API built on it.
//
// A renderer factory `(engine, options) => renderer` draws one layer on one
// engine kind (`render/cesium.js`, `render/navara.js` of a layer). Cesium
// layers still draw themselves until their render/cesium.js is extracted
// (the *a tasks): they are registered as CESIUM_BUILTIN. Cesium entries are
// registered here, statically (main chunk); Navara factories are registered
// by the Navara mode after its import() (src/app/navaraMode.js).
// layers without a render/cesium.js draw themselves: they are registered as
// CESIUM_BUILTIN. Cesium entries are registered here, statically (main
// chunk); Navara factories are registered by the Navara mode after its
// import() (src/app/navaraMode.js).
//
// ## Layer availability (stable API, used by phase 4's layer list)
// ## Layer availability (stable API, used by the layer list)
//
// import { createLayerAvailability } from './app/layers/renderers.js';
// const availability = createLayerAvailability(engine.kind);
@@ -28,8 +28,8 @@ import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesiu
// The object is frozen; available() and unavailable() return fresh arrays.
// It reads the registry when called, so a Navara factory registered later
// (after the Navara mode loads) shows up. `name` is the layer's display name
// (src/app/layers/directory.js). Phase 4 injects it as its `availability`
// provider (`{ engine, unavailable() }` is the part it reads).
// (src/app/layers/directory.js). The layer list takes it as its
// `availability` provider (`{ engine, unavailable() }` is the part it reads).
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js';
+2 -2
View File
@@ -14,7 +14,7 @@
// drawn(id) what the layer draws per object (glyph, model),
// as plain data, or null (layers with qaDrawn)
// following() { label, native } of engine.camera.following()
// movers() engine.movers.list()
// movers() engine.movers.list() ([] without movers: Cesium)
// framesDrawn() frames drawn since the probe started
const plain = (position) =>
@@ -83,7 +83,7 @@ export function createQaProbe({ engine, dataManager, availability }) {
native: handle.native?.id ?? null,
};
},
movers: () => engine.movers.list(),
movers: () => engine.movers?.list() ?? [],
framesDrawn: () => frames,
destroy: () => removeFrames(),
};
+5 -5
View File
@@ -217,11 +217,11 @@ async function createRenderer({
googleTileset: tileset,
cesiumToken,
initialStack: tileset ? 'photoreal' : 'esri-imagery',
// Task 5 (height-datum fix): rebroadcast stack changes as a window
// CustomEvent so data layers (CCTV per-regime ground resolution) can
// react without coupling MapStackController to layer modules. Fires on
// 'switching'/'ready'/'error'; listeners derive the surface regime from
// live scene state, so intermediate emissions are harmless.
// Rebroadcast stack changes as a window CustomEvent so data layers (CCTV
// per-regime ground resolution) can react without coupling
// MapStackController to layer modules. Fires on 'switching'/'ready'/
// 'error'; listeners derive the surface regime from live scene state, so
// intermediate emissions are harmless.
onChange: (state) => {
window.dispatchEvent(
new CustomEvent('gev:map-stack-changed', { detail: state }),
-9
View File
@@ -9,13 +9,6 @@
// a release that runs, in reverse, when the part detaches (before its
// detachScene).
const STATES = Object.freeze([
'detached',
'attaching',
'attached',
'detaching',
]);
export class SceneSwitchingError extends Error {
constructor(message = 'The globe is switching engines') {
super(message);
@@ -208,5 +201,3 @@ export function createSceneHost({ onError = null } = {}) {
},
});
}
export const SCENE_HOST_STATES = STATES;
+1 -1
View File
@@ -327,7 +327,7 @@ function attachViewerTools(
);
// Install the explicit scope mask used by the DISPLAY controls.
installScopeMask(viewer);
installScopeMask(engine);
defer(() => destroyScopeMask());
// The follow camera recomputes the tracked target's dead-reckon position
+1 -1
View File
@@ -9,7 +9,7 @@ export const PITCH_CLAMP_MIN_HEIGHT_M = 300_000;
// with the first Cesium globe, after this module (src/app/loadCesium.js).
const RADIANS_PER_DEGREE = Math.PI / 180;
/** Look this far below the Earth's limb, so the globe stays on screen. */
export const LIMB_MARGIN_RAD = 8 * RADIANS_PER_DEGREE;
const LIMB_MARGIN_RAD = 8 * RADIANS_PER_DEGREE;
const PITCH_EASE = 0.35;
const PITCH_SNAP_RAD = 0.1 * RADIANS_PER_DEGREE;
const HEIGHT_EPSILON_M = 0.0001;
+1 -1
View File
@@ -1,6 +1,6 @@
// Deterministic Launch Library 2 records for phase-3 mission/replay acceptance.
import { SATELLITE_FIXTURE_EPOCH } from './satelliteFixture.js';
export const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH;
const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH;
export function launchFixture() {
const definitions = [
[
+1 -3
View File
@@ -1,4 +1,4 @@
// Deterministic TLE catalogs for phase 3 browser QA; no live feed requests.
// Deterministic TLE catalogs for browser QA; no live feed requests.
import { W1_FROZEN_MS } from './w1Feeds.js';
const checksum = (line) => {
@@ -30,8 +30,6 @@ const GROUPS = {
starlink: [50000, 6000, 53, 15.2, 'QA STARLINK'],
};
export const SATELLITE_FIXTURE_EPOCH = W1_FROZEN_MS;
export const SATELLITE_FIXTURE_CORE_COUNT = 839;
export const SATELLITE_FIXTURE_DENSE_COUNT = 6839;
export function satelliteFixture(group) {
const spec = GROUPS[group];
if (!spec) throw new TypeError('Unknown fixture group');
+1
View File
@@ -57,6 +57,7 @@ function fakeViewer({ cameraHeightM, surfaceM, canvas = null }) {
const position = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, cameraHeightM);
const viewer = {
flights,
canvas,
scene: {
canvas,
sampleHeightSupported: true,
+1 -1
View File
@@ -400,7 +400,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
assert.doesNotMatch(source, /camera\.flyTo/);
}
// Vessels' click rule is a function the engine's selection dispatcher
// calls (phase 3, 13a): the same body, out of the old LEFT_CLICK handler.
// calls: the same body, out of the old LEFT_CLICK handler.
const vesselClick = body(
vessels,
/function handleClick\(click\) \{([\s\S]*?)\n \}/,
-14
View File
@@ -2370,17 +2370,3 @@ export function flyRoute(
};
return typeof runNavigation === 'function' ? runNavigation(start) : start();
}
// QA (development builds only; production builds drop it): the verbs and
// their diagnostics, so a browser scenario can drive them on an engine whose
// voice tools do not offer them yet.
if (import.meta.env?.DEV && typeof window !== 'undefined')
window.__gevCameraVerbs = Object.freeze({
cameraVerbsSupported,
flyRoute,
getActiveCameraMotion,
initCameraVerbs,
interruptCameraMotion,
moveCamera,
releaseCameraVerbs,
});
+1 -3
View File
@@ -261,10 +261,8 @@ const TRACKED_RASTER_PX = 192;
* Default size serves the fleet; pass `aircraftIcon(kind, TRACKED_ICON_PX)`
* (re-exported below) for the tracked billboard. */
export const TRACKED_ICON_PX = TRACKED_RASTER_PX;
/** The fleet raster size (aircraftIcon's default). */
export const FLEET_ICON_PX = FLEET_RASTER_PX;
/** Every silhouette kind aircraftIcon draws (an engine's sprite atlas). */
export const AIRCRAFT_ICON_KINDS = Object.freeze(Object.keys(BODIES));
const AIRCRAFT_ICON_KINDS = Object.freeze(Object.keys(BODIES));
export function aircraftIcon(kind, px = FLEET_RASTER_PX) {
const k = BODIES[kind] ? kind : 'airliner';
const key = `${k}@${px}`;
+2 -2
View File
@@ -2,8 +2,8 @@ import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
/**
* Arrival corrections belong to one globe engine and yield to its next camera
* owner. Callers may name the engine or (layers until phase 1c) the viewer it
* is registered for; both reach the same correction.
* owner. Callers may name the engine or (layers that still hold one) the
* viewer it is registered for; both reach the same correction.
*/
const arrivals = new WeakMap();
+1 -1
View File
@@ -29,7 +29,7 @@ for (const layer of LAYERS) {
test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => {
// The aircraft renderer loads through its `loadModel`, Cesium.Model.fromGltfAsync
// unless a test injects a loader (phase 3).
// unless a test injects a loader.
assert.match(source, /loadModel = \(options\) => Cesium\.Model\.fromGltfAsync\(options\)/);
const calls = [...source.matchAll(/(?:Cesium\.Model\.fromGltfAsync|\bloadModel)\(\{([\s\S]*?)\}\)/g)];
assert.ok(calls.length >= 3, `expected fleet, tracked, and preload model calls; found ${calls.length}`);
+2 -2
View File
@@ -34,7 +34,7 @@ export const OPENMAPTILES_CREDIT = {
key: 'openfreemap',
html: 'Vector tiles: <a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> © <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>',
};
export const GOOGLE_PLACES_CREDIT = {
const GOOGLE_PLACES_CREDIT = {
key: 'google-maps-places',
html: 'Google Maps Places: <a href="https://developers.google.com/maps/documentation/places/web-service/policies" target="_blank" rel="noopener">Google Maps</a>',
};
@@ -45,7 +45,7 @@ const viewerCredits = new WeakMap();
/**
* Credits of a globe engine, or of a Cesium viewer's credit display for
* callers that still hold a viewer (layers until phase 1c); null without one.
* callers that still hold a viewer (Cesium layers); null without one.
* @returns {{ addStatic: Function, request: () => void } | null}
*/
function creditsOf(target) {
+1 -1
View File
@@ -143,7 +143,7 @@ test('every scene click handler consults ownership before it picks', () => {
'src/layers/submarineCables/interaction.js',
'viewer.scene.pick(click.position)',
],
// Vessels take the engine dispatcher's pick (phase 3, 13a): the guard
// Vessels take the engine dispatcher's pick: the guard
// comes before the click rule reads it.
['src/layers/vessels/selection.js', 'resolvePickId(picked)'],
];
+1 -1
View File
@@ -140,7 +140,7 @@ test('sampler on a globe engine: samples engine.surface and latches against the
assert.equal(cachedMeshFloor(cell.lat, cell.lon), 137.0);
const [call] = engine.fake.callsOf('surface.sampleHeightDetailed');
assert.ok(Math.abs(Cesium.Math.toDegrees(call.args[0].latitude) - cell.lat) < 1e-9);
assert.deepEqual(call.args[1], { exclude: ['own'], lod: 'visible' });
assert.deepEqual(call.args[1], { exclude: ['own'] });
});
test('sampler: a coarse tile answer is a miss; a fine one latches', async () => {
+1 -2
View File
@@ -1,7 +1,6 @@
// Native constants shared by the W1 adapters; keep the SDK import here.
// Read on call: the production build's Cesium global loads with the first
// Cesium globe, after this module has loaded on a Navara start.
import { Axis, ScreenSpaceEventType } from 'cesium';
import { ScreenSpaceEventType } from 'cesium';
export const modelAxis = () => Axis;
export const trackingEventTypes = () => ScreenSpaceEventType;
+3 -3
View File
@@ -1,6 +1,6 @@
import { nearbyContact } from './pickRegistry.js';
// One selection dispatcher for the layers that have moved onto the globe
// engine (phase 3 Task 4): one click subscription with picks
// One selection dispatcher for the layers that draw through the globe
// engine: one click subscription with picks
// (engine.input.on('click', …, { pick: true, drill: 8 })), the press
// gesture and one Escape listener, instead of a ScreenSpaceEventHandler
// per layer. Owners register as with pickRegistry (registerPickOwner, so
@@ -38,7 +38,7 @@ import {
isTrackingSelectionGesture,
} from './trackingClickGesture.js';
export const SELECTION_DRILL = 8;
const SELECTION_DRILL = 8;
/**
* @param {import('../globe/contract.js').GlobeEngine} engine
+1 -1
View File
@@ -109,7 +109,7 @@ test('flights, AIS, and FIRMS enable paths are wired through the shared sprite r
assert.deepEqual(calls, [viewer, viewer, viewer]);
const firmsLayer = createFirmsHeatmapLayer({ id: 'firms', name: 'FIRMS' });
// Flights draws through its renderer (phase 3): enable asks the fleet to
// Flights draws through its renderer: enable asks the fleet to
// restore its order, which the Cesium aircraft renderer wires to the shared
// restorer under the flights' sprite slot.
assert.match(flightsLayer.enable.toString(), /fleet\.restoreOrderOnEnable\(\)/);
+1 -1
View File
@@ -114,7 +114,7 @@ test('civilian and military click handlers apply duration only at the deselect b
readLayerSource(new URL('./flights.js', import.meta.url)),
readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
];
// Flights and military moved to the selection dispatcher (phase 3), whose
// Flights and military use the selection dispatcher, whose
// source applies the same two rules: the selection gesture before a
// select, the full click classifier before the empty-space deselect, which
// each layer answers with a user-origin clear.
+7 -9
View File
@@ -1,12 +1,15 @@
// Seeded, deterministic bit-for-bit comparison of src/geo against Cesium.
// Seeded, deterministic bit-for-bit comparison of src/geo against Cesium's
// release build (Build/Cesium, debug checks stripped, as production ships it).
// Every number is compared with Object.is (so -0 and NaN count), a throw on one
// side must be a throw on the other, and `result` aliasing is exercised the
// same way on both sides.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createRequire } from 'node:module';
import * as geo from './index.js';
const Cesium = createRequire(import.meta.url)('cesium/Build/Cesium/index.cjs');
const {
cartesian3: c3,
cartographic,
@@ -108,7 +111,7 @@ const cesium = (v) => new C3(v.x, v.y, v.z);
test('vector operations, including -0 inputs and aliased results, equal Cesium bit for bit', () => {
const { scalar, vector } = inputs(1);
// Directionless vectors first: normalize must throw on both sides.
// Directionless vectors first: normalize returns NaN on both sides.
const DEGENERATE = [
{ x: 0, y: 0, z: 0 },
{ x: -0, y: 0, z: -0 },
@@ -397,12 +400,7 @@ test('matrices and quaternions, including aliased results and every fromRotation
][i % 4];
const angle =
i % 8 < 4 ? Math.PI - 0.5 * random() : (random() - 0.5) * 2 * Math.PI;
let m;
try {
m = rotation(axis, angle);
} catch {
continue; // a zero axis: Cesium throws, nothing to compare
}
const m = rotation(axis, angle);
const trace = m[0] + m[4] + m[8];
branches.add(
trace > 0
-7
View File
@@ -1,8 +1,6 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — BoundingSphere.js subset.
// Exact mission framing: Ritter/box fit and the union of two spheres.
import * as Cartesian3 from './cartesian3.js';
import { Check } from './check.js';
import { DEBUG } from './debug.js';
const defined = (value) => value !== null && value !== undefined;
export function create(center = Cartesian3.ZERO, radius = 0) {
return { center: Cartesian3.clone(center), radius };
@@ -184,11 +182,6 @@ export function fromPoints(positions, result) {
return result;
}
export function union(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
if (!defined(result)) {
result = create();
}
+1 -11
View File
@@ -1,17 +1,12 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartesian2.js
//
// 2D vectors (screen coordinates) as plain-data {x, y} objects. With DEBUG on
// (development and tests; see debug.js) invalid arguments throw the same
// DeveloperError as Cesium's debug checks.
// 2D vectors (screen coordinates) as plain-data {x, y} objects.
//
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
// copyState and src/director/documentFields.js jsonTree reject them: copy
// the fields into a literal before putting one into product state or a
// scene document.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
// Values are instances of a module-private constructor rather than object
// literals: every `{x, y}` literal in the process shares one V8 hidden
// class, and code elsewhere that builds such literals would make geo's hot
@@ -38,11 +33,6 @@ export function clone(cartesian, result) {
/** The distance between two points. */
export function distance(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
const x = left.x - right.x;
const y = left.y - right.y;
return Math.sqrt(x * x + y * y);
+3 -144
View File
@@ -3,19 +3,14 @@
// 3D vectors as plain-data {x, y, z} objects. Arithmetic takes a required `result`
// (as Cesium does) that may be one of the inputs; creators take an optional
// one. The operation order matches Cesium so the outputs are bit-identical.
// With DEBUG on (development and tests; see debug.js) invalid arguments throw
// the same DeveloperError as Cesium's debug checks, and `normalize` throws on a
// zero or non-finite vector; production builds skip the checks and return NaN
// components there, as release Cesium does.
// As in release Cesium, arguments are not checked and `normalize` returns NaN
// components for a zero or non-finite vector.
//
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
// copyState and src/director/documentFields.js jsonTree reject them: copy
// the fields into a literal before putting one into product state or a
// scene document.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
import { DeveloperError } from './developerError.js';
import { equalsEpsilon as scalarEqualsEpsilon, toRadians } from './math.js';
// Values are instances of a module-private constructor rather than object
@@ -59,10 +54,6 @@ export const UNIT_Y = Object.freeze(create(0.0, 1.0, 0.0));
export const UNIT_Z = Object.freeze(create(0.0, 0.0, 1.0));
export function magnitudeSquared(cartesian) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
}
return (
cartesian.x * cartesian.x +
cartesian.y * cartesian.y +
@@ -77,60 +68,29 @@ export function magnitude(cartesian) {
const distanceScratch = create();
export function distance(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
subtract(left, right, distanceScratch);
return magnitude(distanceScratch);
}
export function distanceSquared(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
subtract(left, right, distanceScratch);
return magnitudeSquared(distanceScratch);
}
/** The unit vector; with DEBUG on, throws when the vector has no direction (zero or non-finite). */
/** The unit vector; NaN components when the vector has no direction (zero or non-finite). */
export function normalize(cartesian, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.object('result', result);
}
const length = magnitude(cartesian);
result.x = cartesian.x / length;
result.y = cartesian.y / length;
result.z = cartesian.z / length;
if (DEBUG) {
if (isNaN(result.x) || isNaN(result.y) || isNaN(result.z)) {
throw new DeveloperError('normalized result is not a number');
}
}
return result;
}
export function dot(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
return left.x * right.x + left.y * right.y + left.z * right.z;
}
export function multiplyComponents(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = left.x * right.x;
result.y = left.y * right.y;
result.z = left.z * right.z;
@@ -138,12 +98,6 @@ export function multiplyComponents(left, right, result) {
}
export function add(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = left.x + right.x;
result.y = left.y + right.y;
result.z = left.z + right.z;
@@ -151,12 +105,6 @@ export function add(left, right, result) {
}
export function subtract(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = left.x - right.x;
result.y = left.y - right.y;
result.z = left.z - right.z;
@@ -164,12 +112,6 @@ export function subtract(left, right, result) {
}
export function multiplyByScalar(cartesian, scalar, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.number('scalar', scalar);
Check.typeOf.object('result', result);
}
result.x = cartesian.x * scalar;
result.y = cartesian.y * scalar;
result.z = cartesian.z * scalar;
@@ -177,12 +119,6 @@ export function multiplyByScalar(cartesian, scalar, result) {
}
export function divideByScalar(cartesian, scalar, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.number('scalar', scalar);
Check.typeOf.object('result', result);
}
result.x = cartesian.x / scalar;
result.y = cartesian.y / scalar;
result.z = cartesian.z / scalar;
@@ -190,11 +126,6 @@ export function divideByScalar(cartesian, scalar, result) {
}
export function negate(cartesian, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
Check.typeOf.object('result', result);
}
result.x = -cartesian.x;
result.y = -cartesian.y;
result.z = -cartesian.z;
@@ -204,25 +135,12 @@ export function negate(cartesian, result) {
const lerpScratch = create();
export function lerp(start, end, t, result) {
if (DEBUG) {
Check.typeOf.object('start', start);
Check.typeOf.object('end', end);
Check.typeOf.number('t', t);
Check.typeOf.object('result', result);
}
multiplyByScalar(end, t, lerpScratch);
result = multiplyByScalar(start, 1.0 - t, result);
return add(lerpScratch, result, result);
}
export function cross(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
const leftX = left.x;
const leftY = left.y;
const leftZ = left.z;
@@ -241,12 +159,6 @@ export function cross(left, right, result) {
}
export function midpoint(left, right, result) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
Check.typeOf.object('result', result);
}
result.x = (left.x + right.x) * 0.5;
result.y = (left.y + right.y) * 0.5;
result.z = (left.z + right.z) * 0.5;
@@ -292,11 +204,6 @@ let scratchK = create();
/** The Earth-fixed position of a geodetic one in radians on the ellipsoid (WGS84 by default). */
export function fromRadians(longitude, latitude, height, ellipsoid, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
height = height ?? 0.0;
const radiiSquared =
ellipsoid == null ? WGS84_RADII_SQUARED : ellipsoid.radiiSquared;
@@ -318,11 +225,6 @@ export function fromRadians(longitude, latitude, height, ellipsoid, result) {
/** The Earth-fixed position of a geodetic one in degrees on the ellipsoid (WGS84 by default). */
export function fromDegrees(longitude, latitude, height, ellipsoid, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
longitude = toRadians(longitude);
latitude = toRadians(latitude);
return fromRadians(longitude, latitude, height, ellipsoid, result);
@@ -330,15 +232,6 @@ export function fromDegrees(longitude, latitude, height, ellipsoid, result) {
/** Positions from a flat [lon, lat, lon, lat, …] list in degrees, at height 0. */
export function fromDegreesArray(coordinates, ellipsoid, result) {
if (DEBUG) {
Check.defined('coordinates', coordinates);
if (coordinates.length < 2 || coordinates.length % 2 !== 0) {
throw new DeveloperError(
'the number of coordinates must be a multiple of 2 and at least 2',
);
}
}
const length = coordinates.length;
if (result == null) {
result = new Array(length / 2);
@@ -358,43 +251,9 @@ export function fromDegreesArray(coordinates, ellipsoid, result) {
return result;
}
/** Positions from a flat [lon, lat, height, …] list in degrees and metres. */
export function fromDegreesArrayHeights(coordinates, ellipsoid, result) {
if (DEBUG) {
Check.defined('coordinates', coordinates);
if (coordinates.length < 3 || coordinates.length % 3 !== 0) {
throw new DeveloperError(
'the number of coordinates must be a multiple of 3 and at least 3',
);
}
}
const length = coordinates.length;
if (result == null) {
result = new Array(length / 3);
} else {
result.length = length / 3;
}
for (let i = 0; i < length; i += 3) {
const index = i / 3;
result[index] = fromDegrees(
coordinates[i],
coordinates[i + 1],
coordinates[i + 2],
ellipsoid,
result[index],
);
}
return result;
}
const angleBetweenScratch = create();
const angleBetweenScratch2 = create();
export function angleBetween(left, right) {
if (DEBUG) {
Check.typeOf.object('left', left);
Check.typeOf.object('right', right);
}
normalize(left, angleBetweenScratch);
normalize(right, angleBetweenScratch2);
const cosine = dot(angleBetweenScratch, angleBetweenScratch2);
+2 -90
View File
@@ -21,78 +21,16 @@ const same = (ours, theirs, label) =>
label,
);
test('binary vector ops equal Cesium bit for bit', () => {
for (const a of SAMPLES)
for (const b of SAMPLES) {
for (const op of ['add', 'subtract', 'cross', 'multiplyComponents']) {
same(
c3[op](asPlain(a), asPlain(b), c3.create()),
C[op](asCesium(a), asCesium(b), new C()),
`${op}`,
);
}
assert.equal(
c3.dot(asPlain(a), asPlain(b)),
C.dot(asCesium(a), asCesium(b)),
);
assert.equal(
c3.distance(asPlain(a), asPlain(b)),
C.distance(asCesium(a), asCesium(b)),
);
assert.equal(
c3.distanceSquared(asPlain(a), asPlain(b)),
C.distanceSquared(asCesium(a), asCesium(b)),
);
same(
c3.lerp(asPlain(a), asPlain(b), 0.3, c3.create()),
C.lerp(asCesium(a), asCesium(b), 0.3, new C()),
'lerp',
);
same(
c3.midpoint(asPlain(a), asPlain(b), c3.create()),
C.midpoint(asCesium(a), asCesium(b), new C()),
'midpoint',
);
assert.equal(
c3.equalsEpsilon(asPlain(a), asPlain(b), 1e-7),
C.equalsEpsilon(asCesium(a), asCesium(b), 1e-7),
);
}
});
test('unary ops and scalars equal Cesium', () => {
test('magnitudeSquared equals Cesium', () => {
for (const a of SAMPLES) {
assert.equal(c3.magnitude(asPlain(a)), C.magnitude(asCesium(a)));
assert.equal(
c3.magnitudeSquared(asPlain(a)),
C.magnitudeSquared(asCesium(a)),
);
same(
c3.negate(asPlain(a), c3.create()),
C.negate(asCesium(a), new C()),
'negate',
);
same(
c3.multiplyByScalar(asPlain(a), 2.5, c3.create()),
C.multiplyByScalar(asCesium(a), 2.5, new C()),
'mul',
);
same(
c3.divideByScalar(asPlain(a), 4, c3.create()),
C.divideByScalar(asCesium(a), 4, new C()),
'div',
);
if (C.magnitude(asCesium(a)) !== 0) {
same(
c3.normalize(asPlain(a), c3.create()),
C.normalize(asCesium(a), new C()),
'normalize',
);
}
}
});
test('geodetic constructors equal Cesium', () => {
test('fromRadians and fromDegreesArray equal Cesium', () => {
for (const [lon, lat, h] of [
[0, 0, 0],
[-73.9857, 40.7484, 381],
@@ -100,11 +38,6 @@ test('geodetic constructors equal Cesium', () => {
[180, -90, 1e4],
[12.5, 89.999999, 1],
]) {
same(
c3.fromDegrees(lon, lat, h),
C.fromDegrees(lon, lat, h),
`fromDegrees ${lon},${lat}`,
);
const lr = [Cesium.Math.toRadians(lon), Cesium.Math.toRadians(lat), h];
same(c3.fromRadians(...lr), C.fromRadians(...lr), 'fromRadians');
}
@@ -112,10 +45,6 @@ test('geodetic constructors equal Cesium', () => {
c3.fromDegreesArray(flat).forEach((p, i) =>
same(p, C.fromDegreesArray(flat)[i], `array ${i}`),
);
const heights = [-74, 40.7, 10, -73.9, 40.8, 20];
c3.fromDegreesArrayHeights(heights).forEach((p, i) =>
same(p, C.fromDegreesArrayHeights(heights)[i], `heights ${i}`),
);
});
test('result aliasing works like Cesium and creators reuse result', () => {
@@ -138,20 +67,3 @@ test('plain results are accepted by Cesium functions (duck typing)', () => {
),
);
});
test('normalize throws on a zero or NaN vector, as Cesium does when run from source', () => {
for (const [x, y, z] of [
[0, 0, 0],
[-0, 0, -0],
[Number.NaN, 1, 2],
]) {
assert.throws(
() => C.normalize(new C(x, y, z), new C()),
/normalized result is not a number/,
);
assert.throws(
() => c3.normalize({ x, y, z }, c3.create()),
/normalized result is not a number/,
);
}
});
+1 -19
View File
@@ -1,17 +1,13 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartographic.js
//
// Geodetic positions as plain-data {longitude, latitude, height} objects (radians and
// metres). Every function takes an optional `result` and returns it. With
// DEBUG on (development and tests; see debug.js) invalid arguments throw the
// same DeveloperError as Cesium's debug checks.
// metres). Every function takes an optional `result` and returns it.
//
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
// copyState and src/director/documentFields.js jsonTree reject them: copy
// the fields into a literal before putting one into product state or a
// scene document.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
import { toRadians } from './math.js';
import * as cartesian3 from './cartesian3.js';
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
@@ -51,11 +47,6 @@ const WGS84_CENTER_TOLERANCE_SQUARED = 0.1;
/** A geodetic position from radians. */
export function fromRadians(longitude, latitude, height, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
height = height ?? 0.0;
if (result == null) return create(longitude, latitude, height);
result.longitude = longitude;
@@ -66,11 +57,6 @@ export function fromRadians(longitude, latitude, height, result) {
/** A geodetic position from degrees; the result is in radians. */
export function fromDegrees(longitude, latitude, height, result) {
if (DEBUG) {
Check.typeOf.number('longitude', longitude);
Check.typeOf.number('latitude', latitude);
}
longitude = toRadians(longitude);
latitude = toRadians(latitude);
return fromRadians(longitude, latitude, height, result);
@@ -133,10 +119,6 @@ export function fromCartesian(cartesian, ellipsoid, result) {
* where Cesium uses Ellipsoid.default; gev never changes it).
*/
export function toCartesian(cartographic, ellipsoid, result) {
if (DEBUG) {
Check.defined('cartographic', cartographic);
}
return cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
-29
View File
@@ -4,12 +4,6 @@ import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import * as cartographic from './cartographic.js';
const close = (a, b, tol, label) =>
assert.ok(
Math.abs(a - b) <= tol * Math.max(1, Math.abs(b)),
`${label}: ${a} vs ${b}`,
);
test('fromDegrees and fromRadians equal Cesium', () => {
for (const [lon, lat, h] of [
[0, 0, 0],
@@ -30,29 +24,6 @@ test('fromDegrees and fromRadians equal Cesium', () => {
}
});
test('fromCartesian and toCartesian equal Cesium, including the centre case', () => {
for (const [x, y, z] of [
[1334762.2, -4654869.1, 4137287.9],
[6378137, 0, 0],
[0.01, 0, 0],
]) {
const ours = cartographic.fromCartesian({ x, y, z });
const theirs = Cesium.Cartographic.fromCartesian(
new Cesium.Cartesian3(x, y, z),
);
if (theirs === undefined) {
assert.equal(ours, undefined);
continue;
}
close(ours.longitude, theirs.longitude, 1e-12, 'lon');
close(ours.latitude, theirs.latitude, 1e-12, 'lat');
close(ours.height, theirs.height, 1e-9, 'h');
const back = cartographic.toCartesian(ours);
const cBack = Cesium.Cartographic.toCartesian(theirs);
for (const k of ['x', 'y', 'z']) close(back[k], cBack[k], 1e-12, k);
}
});
test('result objects are reused', () => {
const out = cartographic.create();
assert.equal(cartographic.fromDegrees(1, 2, 3, out), out);
-61
View File
@@ -1,61 +0,0 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — Check.js
//
// Cesium's argument checks with the same conditions and message text. Only the
// checks src/geo needs are ported. Callers run them inside `if (DEBUG)` blocks
// (see debug.js), as Cesium runs them inside its debug pragmas, so production
// builds drop the calls and this module with them.
import { DeveloperError } from './developerError.js';
const undefinedMessage = (name) =>
`${name} is required, actual value was undefined`;
const typeMessage = (actual, expected, name) =>
`Expected ${name} to be typeof ${expected}, actual typeof was ${actual}`;
/** Throws unless `test` is neither undefined nor null. */
function defined(name, test) {
if (test === undefined || test === null) {
throw new DeveloperError(undefinedMessage(name));
}
}
/** Throws unless `typeof test` is 'number' (NaN passes, as in Cesium). */
function number(name, test) {
if (typeof test !== 'number') {
throw new DeveloperError(typeMessage(typeof test, 'number', name));
}
}
// Attached in a function marked pure, not by top-level assignments, so that a
// production build, where no caller runs a check, drops this module entirely.
function withBounds(check) {
check.lessThanOrEquals = function (name, test, limit) {
check(name, test);
if (test > limit) {
throw new DeveloperError(
`Expected ${name} to be less than or equal to ${limit}, actual value was ${test}`,
);
}
};
check.greaterThanOrEquals = function (name, test, limit) {
check(name, test);
if (test < limit) {
throw new DeveloperError(
`Expected ${name} to be greater than or equal to ${limit}, actual value was ${test}`,
);
}
};
return check;
}
/** Throws unless `typeof test` is 'object' (null passes, as in Cesium). */
function object(name, test) {
if (typeof test !== 'object') {
throw new DeveloperError(typeMessage(typeof test, 'object', name));
}
}
export const Check = /* @__PURE__ */ Object.freeze({
defined,
typeOf: Object.freeze({ number: /* @__PURE__ */ withBounds(number), object }),
});
-486
View File
@@ -1,486 +0,0 @@
// Argument-check parity with Cesium. gev runs Cesium from source, where its
// debug checks are live, so for any input both sides must throw an error with
// the same name and message, or neither may throw. The inputs are seeded and
// deterministic: each case builds its arguments twice from the same seed, once
// for geo and once for Cesium, so no object is shared between the two sides.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import * as geo from './index.js';
import { DeveloperError } from './developerError.js';
const TRIALS = 150;
function generator(seed) {
return () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Invalid values every argument may receive. NaN is a number to Check.typeOf.number
// and null an object to Check.typeOf.object, so both reach the arithmetic.
const INVALID = [undefined, null, Number.NaN, 'abc', 5, true, {}];
const vectorValues = { x: 1.5, y: -2, z: 3 };
const VALID = {
num: (side, random) => [0, -0, 0.7, -2.5, 1e6][Math.floor(random() * 5)],
index3: (side, random) => Math.floor(random() * 5) - 1,
index4: (side, random) => Math.floor(random() * 6) - 1,
nonNegative: (side, random) => [0, 3, 6378137, -1][Math.floor(random() * 4)],
vec: (side) =>
side === 'geo' ? { ...vectorValues } : new Cesium.Cartesian3(1.5, -2, 3),
zeroVec: (side) =>
side === 'geo' ? { x: 0, y: 0, z: 0 } : new Cesium.Cartesian3(),
vec4: (side) =>
side === 'geo' ? { x: 0, y: 0, z: 0, w: 0 } : new Cesium.Cartesian4(),
carto: (side) =>
side === 'geo'
? { longitude: 0.3, latitude: -0.2, height: 10 }
: new Cesium.Cartographic(0.3, -0.2, 10),
partialCarto: (side) =>
side === 'geo'
? { longitude: 0.3, latitude: -0.2 }
: { longitude: 0.3, latitude: -0.2 },
quat: (side) =>
side === 'geo'
? { x: 0.1, y: 0.2, z: 0.3, w: 0.9 }
: new Cesium.Quaternion(0.1, 0.2, 0.3, 0.9),
mat3: (side) =>
side === 'geo'
? geo.matrix3.fromRotationZ(0.4)
: Cesium.Matrix3.fromRotationZ(0.4, new Cesium.Matrix3()),
mat4: (side) =>
side === 'geo'
? geo.transforms.eastNorthUpToFixedFrame({ x: 6378137, y: 1, z: 2 })
: Cesium.Transforms.eastNorthUpToFixedFrame(
new Cesium.Cartesian3(6378137, 1, 2),
),
hpr: (side) =>
side === 'geo'
? { heading: 0.3, pitch: -0.2, roll: 0.1 }
: new Cesium.HeadingPitchRoll(0.3, -0.2, 0.1),
partialHpr: () => ({ heading: 0.3 }),
ellipsoid: (side) =>
side === 'geo' ? geo.ellipsoid.WGS84 : Cesium.Ellipsoid.WGS84,
coords2: (side, random) =>
[[1, 2], [1, 2, 3], [], [1, 2, 3, 4]][Math.floor(random() * 4)],
coords3: (side, random) =>
[[1, 2, 3], [1, 2], [], [1, 2, 3, 4, 5, 6], [1, 2, 'h']][
Math.floor(random() * 5)
],
nanVec: (side) =>
side === 'geo'
? { x: 1, y: Number.NaN, z: 2 }
: new Cesium.Cartesian3(1, Number.NaN, 2),
partialVec: () => ({ x: 1, y: 2 }),
stringHeight: () => '5',
};
/** One argument: usually a valid value of its kind, sometimes an invalid one. */
function argument(kinds, side, random) {
if (random() < 0.4) return INVALID[Math.floor(random() * INVALID.length)];
const kind = kinds[Math.floor(random() * kinds.length)];
return VALID[kind](side, random);
}
const W = Cesium.Ellipsoid.WGS84;
const C3 = Cesium.Cartesian3;
const V = ['vec', 'zeroVec', 'nanVec', 'partialVec'];
const N = ['num'];
const R = ['vec'];
// [name, geo function, Cesium function, argument kinds per position]
const CASES = [
['math.toRadians', geo.math.toRadians, Cesium.Math.toRadians, [N]],
['math.toDegrees', geo.math.toDegrees, Cesium.Math.toDegrees, [N]],
['math.clamp', geo.math.clamp, Cesium.Math.clamp, [N, N, N]],
['math.lerp', geo.math.lerp, Cesium.Math.lerp, [N, N, N]],
['math.mod', geo.math.mod, Cesium.Math.mod, [N, ['num', 'nonNegative']]],
['math.zeroToTwoPi', geo.math.zeroToTwoPi, Cesium.Math.zeroToTwoPi, [N]],
[
'math.negativePiToPi',
geo.math.negativePiToPi,
Cesium.Math.negativePiToPi,
[N],
],
[
'math.equalsEpsilon',
geo.math.equalsEpsilon,
Cesium.Math.equalsEpsilon,
[N, N, N, N],
],
['cartesian2.clone', geo.cartesian2.clone, Cesium.Cartesian2.clone, [R, R]],
[
'cartesian2.distance',
geo.cartesian2.distance,
Cesium.Cartesian2.distance,
[R, R],
],
['cartesian3.clone', geo.cartesian3.clone, C3.clone, [V, R]],
[
'cartesian3.fromElements',
geo.cartesian3.fromElements,
C3.fromElements,
[N, N, N, R],
],
['cartesian3.magnitude', geo.cartesian3.magnitude, C3.magnitude, [V]],
[
'cartesian3.magnitudeSquared',
geo.cartesian3.magnitudeSquared,
C3.magnitudeSquared,
[V],
],
['cartesian3.distance', geo.cartesian3.distance, C3.distance, [V, V]],
[
'cartesian3.distanceSquared',
geo.cartesian3.distanceSquared,
C3.distanceSquared,
[V, V],
],
['cartesian3.normalize', geo.cartesian3.normalize, C3.normalize, [V, R]],
['cartesian3.dot', geo.cartesian3.dot, C3.dot, [V, V]],
[
'cartesian3.multiplyComponents',
geo.cartesian3.multiplyComponents,
C3.multiplyComponents,
[V, V, R],
],
['cartesian3.add', geo.cartesian3.add, C3.add, [V, V, R]],
['cartesian3.subtract', geo.cartesian3.subtract, C3.subtract, [V, V, R]],
[
'cartesian3.multiplyByScalar',
geo.cartesian3.multiplyByScalar,
C3.multiplyByScalar,
[V, N, R],
],
[
'cartesian3.divideByScalar',
geo.cartesian3.divideByScalar,
C3.divideByScalar,
[V, N, R],
],
['cartesian3.negate', geo.cartesian3.negate, C3.negate, [V, R]],
['cartesian3.lerp', geo.cartesian3.lerp, C3.lerp, [V, V, N, R]],
['cartesian3.cross', geo.cartesian3.cross, C3.cross, [V, V, R]],
['cartesian3.midpoint', geo.cartesian3.midpoint, C3.midpoint, [V, V, R]],
['cartesian3.equals', geo.cartesian3.equals, C3.equals, [V, V]],
[
'cartesian3.equalsEpsilon',
geo.cartesian3.equalsEpsilon,
C3.equalsEpsilon,
[V, V, N, N],
],
[
'cartesian3.fromRadians',
geo.cartesian3.fromRadians,
C3.fromRadians,
[N, N, ['num', 'stringHeight'], ['ellipsoid'], R],
],
[
'cartesian3.fromDegrees',
geo.cartesian3.fromDegrees,
C3.fromDegrees,
[N, N, ['num', 'stringHeight'], ['ellipsoid'], R],
],
[
'cartesian3.fromDegreesArray',
geo.cartesian3.fromDegreesArray,
C3.fromDegreesArray,
[['coords2'], ['ellipsoid']],
],
[
'cartesian3.fromDegreesArrayHeights',
geo.cartesian3.fromDegreesArrayHeights,
C3.fromDegreesArrayHeights,
[['coords3'], ['ellipsoid']],
],
[
'cartographic.fromRadians',
geo.cartographic.fromRadians,
Cesium.Cartographic.fromRadians,
[N, N, N, ['carto']],
],
[
'cartographic.fromDegrees',
geo.cartographic.fromDegrees,
Cesium.Cartographic.fromDegrees,
[N, N, N, ['carto']],
],
[
'cartographic.fromCartesian',
geo.cartographic.fromCartesian,
Cesium.Cartographic.fromCartesian,
[V, ['ellipsoid'], ['carto']],
],
[
'cartographic.toCartesian',
geo.cartographic.toCartesian,
Cesium.Cartographic.toCartesian,
[['carto', 'partialCarto'], ['ellipsoid'], R],
],
[
'cartographic.clone',
geo.cartographic.clone,
Cesium.Cartographic.clone,
[['carto'], ['carto']],
],
[
'ellipsoid.create',
geo.ellipsoid.create,
(x, y, z) => new Cesium.Ellipsoid(x, y, z),
[['nonNegative'], ['nonNegative'], ['nonNegative']],
],
[
'WGS84.geodeticSurfaceNormal',
geo.ellipsoid.WGS84.geodeticSurfaceNormal,
(c, r) => W.geodeticSurfaceNormal(c, r),
[V, R],
],
[
'WGS84.geodeticSurfaceNormalCartographic',
geo.ellipsoid.WGS84.geodeticSurfaceNormalCartographic,
(c, r) => W.geodeticSurfaceNormalCartographic(c, r),
[['carto', 'partialCarto'], R],
],
[
'WGS84.scaleToGeodeticSurface',
geo.ellipsoid.WGS84.scaleToGeodeticSurface,
(c, r) => W.scaleToGeodeticSurface(c, r),
[V, R],
],
[
'WGS84.cartographicToCartesian',
geo.ellipsoid.WGS84.cartographicToCartesian,
(c, r) => W.cartographicToCartesian(c, r),
[['carto', 'partialCarto'], R],
],
[
'WGS84.cartesianToCartographic',
geo.ellipsoid.WGS84.cartesianToCartographic,
(c, r) => W.cartesianToCartographic(c, r),
[V, ['carto']],
],
[
'matrix3.fromQuaternion',
geo.matrix3.fromQuaternion,
Cesium.Matrix3.fromQuaternion,
[['quat'], ['mat3']],
],
[
'matrix3.fromRotationZ',
geo.matrix3.fromRotationZ,
Cesium.Matrix3.fromRotationZ,
[N, ['mat3']],
],
[
'matrix3.multiplyByVector',
geo.matrix3.multiplyByVector,
Cesium.Matrix3.multiplyByVector,
[['mat3'], V, R],
],
[
'matrix3.getColumn',
geo.matrix3.getColumn,
Cesium.Matrix3.getColumn,
[['mat3'], ['index3'], R],
],
[
'matrix4.clone',
geo.matrix4.clone,
Cesium.Matrix4.clone,
[['mat4'], ['mat4']],
],
[
'matrix4.equals',
geo.matrix4.equals,
Cesium.Matrix4.equals,
[['mat4'], ['mat4']],
],
[
'matrix4.multiply',
geo.matrix4.multiply,
Cesium.Matrix4.multiply,
[['mat4'], ['mat4'], ['mat4']],
],
[
'matrix4.multiplyTransformation',
geo.matrix4.multiplyTransformation,
Cesium.Matrix4.multiplyTransformation,
[['mat4'], ['mat4'], ['mat4']],
],
[
'matrix4.multiplyByPoint',
geo.matrix4.multiplyByPoint,
Cesium.Matrix4.multiplyByPoint,
[['mat4'], V, R],
],
[
'matrix4.multiplyByPointAsVector',
geo.matrix4.multiplyByPointAsVector,
Cesium.Matrix4.multiplyByPointAsVector,
[['mat4'], V, R],
],
[
'matrix4.getTranslation',
geo.matrix4.getTranslation,
Cesium.Matrix4.getTranslation,
[['mat4'], R],
],
[
'matrix4.getMatrix3',
geo.matrix4.getMatrix3,
Cesium.Matrix4.getMatrix3,
[['mat4'], ['mat3']],
],
[
'matrix4.getColumn',
geo.matrix4.getColumn,
Cesium.Matrix4.getColumn,
[['mat4'], ['index4'], ['vec4']],
],
[
'matrix4.inverseTransformation',
geo.matrix4.inverseTransformation,
Cesium.Matrix4.inverseTransformation,
[['mat4'], ['mat4']],
],
[
'matrix4.fromRotationTranslation',
geo.matrix4.fromRotationTranslation,
Cesium.Matrix4.fromRotationTranslation,
[['mat3'], R, ['mat4']],
],
[
'matrix4.fromUniformScale',
geo.matrix4.fromUniformScale,
Cesium.Matrix4.fromUniformScale,
[N, ['mat4']],
],
[
'quaternion.fromAxisAngle',
geo.quaternion.fromAxisAngle,
Cesium.Quaternion.fromAxisAngle,
[V, N, ['quat']],
],
[
'quaternion.fromRotationMatrix',
geo.quaternion.fromRotationMatrix,
Cesium.Quaternion.fromRotationMatrix,
[['mat3'], ['quat']],
],
[
'transforms.eastNorthUpToFixedFrame',
geo.transforms.eastNorthUpToFixedFrame,
Cesium.Transforms.eastNorthUpToFixedFrame,
[V, ['ellipsoid'], ['mat4']],
],
[
'transforms.headingPitchRollToFixedFrame',
geo.transforms.headingPitchRollToFixedFrame,
Cesium.Transforms.headingPitchRollToFixedFrame,
[V, ['hpr', 'partialHpr'], ['ellipsoid'], [], ['mat4']],
],
];
function outcome(fn, args) {
try {
fn(...args);
return 'returned';
} catch (error) {
return `${error?.name}: ${error?.message}`;
}
}
test('every ported function throws exactly where Cesium does, with the same name and message', () => {
let thrown = 0;
let checked = 0;
const mismatches = [];
CASES.forEach(([name, ours, theirs, kinds], caseIndex) => {
for (let trial = 0; trial < TRIALS; trial++) {
const seed = caseIndex * 100003 + trial;
// Trial 0 passes no arguments at all; the rest draw from the pools.
const build = (side) => {
if (trial === 0) return [];
const random = generator(seed);
return kinds.map((k) =>
k.length ? argument(k, side, random) : undefined,
);
};
const a = outcome(ours, build('geo'));
const b = outcome(theirs, build('cesium'));
if (a !== b)
mismatches.push(`${name} (seed ${seed}): geo ${a} | Cesium ${b}`);
checked++;
if (b !== 'returned') thrown++;
}
});
assert.deepEqual(mismatches, []);
// The pools must actually reach the checks.
assert.ok(thrown > checked / 4, `only ${thrown} of ${checked} cases threw`);
});
test('the checks throw a DeveloperError with Cesium message text', () => {
const cases = [
[() => geo.math.toRadians(undefined), 'degrees is required.'],
[() => geo.math.mod(5, 0), 'divisor cannot be 0.'],
[
() => geo.math.clamp('1', 0, 1),
'Expected value to be typeof number, actual typeof was string',
],
[
() => geo.cartesian3.fromDegrees(undefined, 1),
'Expected longitude to be typeof number, actual typeof was undefined',
],
[
() => geo.cartesian3.add({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }),
'Expected result to be typeof object, actual typeof was undefined',
],
[
() => geo.cartesian3.fromDegreesArray([1]),
'the number of coordinates must be a multiple of 2 and at least 2',
],
[
() => geo.cartesian3.fromDegreesArray(),
'coordinates is required, actual value was undefined',
],
[
() => geo.ellipsoid.create(-1, 1, 1),
'Expected x to be greater than or equal to 0, actual value was -1',
],
[
() => geo.matrix3.getColumn(geo.matrix3.create(), 3, {}),
'Expected index to be less than or equal to 2, actual value was 3',
],
[
() =>
geo.transforms.eastNorthUpToFixedFrame({ x: 1, y: Number.NaN, z: 0 }),
'origin has a NaN component',
],
[
() => geo.cartographic.fromCartesian({ x: 1, y: 2, z: 3 }, {}),
'oneOverRadii is required.',
],
];
for (const [fn, message] of cases) {
assert.throws(fn, (error) => {
assert.ok(error instanceof DeveloperError);
assert.equal(error.name, 'DeveloperError');
assert.equal(error.message, message);
return true;
});
}
// NaN is typeof number, so Check.typeOf.number lets it through (as in Cesium).
assert.ok(Number.isNaN(geo.math.clamp(Number.NaN, 0, 1)));
assert.ok(Number.isNaN(Cesium.Math.clamp(Number.NaN, 0, 1)));
});
-12
View File
@@ -1,12 +0,0 @@
// Whether src/geo runs Cesium's debug argument checks.
//
// Cesium wraps its argument checks in `//>>includeStart('debug')` pragmas: they
// are live in its Source modules (Node tests, the Vite dev server) and stripped
// from Build/Cesium, which vite-plugin-cesium copies into production builds.
// src/geo does the same: every Check call and every debug throw sits in an
// `if (DEBUG)` block. DEBUG is true in Node (no import.meta.env) and in Vite
// dev; `vite build` folds it to false and the minifier drops the blocks, so a
// production build returns what release Cesium returns (NaN, not a throw, for
// a degenerate input).
export const DEBUG = import.meta.env?.PROD !== true;
-13
View File
@@ -1,13 +0,0 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — DeveloperError.js
//
// The error Cesium throws for invalid arguments. Its debug argument checks are
// live in Cesium's Source modules (Node tests, the Vite dev server) and
// stripped from production builds; src/geo throws the same name and message
// wherever Cesium does while DEBUG is on (see debug.js).
export class DeveloperError extends Error {
constructor(message) {
super(message);
this.name = 'DeveloperError';
}
}
+1 -22
View File
@@ -3,14 +3,9 @@
// A quadratic surface x²/a² + y²/b² + z²/c² = 1 as a plain, frozen object.
// Every method takes an optional `result` and returns it (a new object when it
// is absent); `result` may be the input. The algorithms and the operation order
// match Cesium so the outputs are bit-identical. With DEBUG on (development
// and tests; see debug.js) invalid arguments throw the same DeveloperError as
// Cesium's debug checks.
// match Cesium so the outputs are bit-identical.
import { DEBUG } from './debug.js';
import { Check } from './check.js';
import { create as createCartographic } from './cartographic.js';
import { DeveloperError } from './developerError.js';
import { EPSILON14 } from './math.js';
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
import {
@@ -39,11 +34,6 @@ export function create(x, y, z) {
x = x ?? 0.0;
y = y ?? 0.0;
z = z ?? 0.0;
if (DEBUG) {
Check.typeOf.number.greaterThanOrEquals('x', x, 0.0);
Check.typeOf.number.greaterThanOrEquals('y', y, 0.0);
Check.typeOf.number.greaterThanOrEquals('z', z, 0.0);
}
const radii = Object.freeze(vec(x, y, z));
const radiiSquared = Object.freeze(vec(x * x, y * y, z * z));
@@ -68,10 +58,6 @@ export function create(x, y, z) {
/** The normal to the ellipsoid surface at a geodetic position. */
function geodeticSurfaceNormalCartographic(cartographic, result) {
if (DEBUG) {
Check.typeOf.object('cartographic', cartographic);
}
const longitude = cartographic.longitude;
const latitude = cartographic.latitude;
const cosLatitude = Math.cos(latitude);
@@ -89,13 +75,6 @@ export function create(x, y, z) {
/** The normal to the ellipsoid surface at a position, or `undefined` at the centre. */
function geodeticSurfaceNormal(cartesian, result) {
if (DEBUG) {
Check.typeOf.object('cartesian', cartesian);
if (isNaN(cartesian.x) || isNaN(cartesian.y) || isNaN(cartesian.z)) {
throw new DeveloperError('cartesian has a NaN component');
}
}
if (equalsEpsilon(cartesian, ZERO, EPSILON14)) {
return undefined;
}
-45
View File
@@ -12,18 +12,6 @@ const close = (a, b, tol, label) =>
const closeVec = (a, b, tol, label) => {
for (const k of ['x', 'y', 'z']) close(a[k], b[k], tol, `${label}.${k}`);
};
const POINTS = [
[6378137, 0, 0],
[0, 6378137, 0],
[0, 0, 6356752.314245],
[1334762.2, -4654869.1, 4137287.9],
[-2694045, -4293642, 3857878],
[1e3, 1e3, 1e3],
[0.01, 0.01, 0.01],
[7e6, -2e6, 1e6],
[4510000, 4510000, 1],
[1, 1, 6400000],
];
test('WGS84 constants equal Cesium', () => {
const c = Cesium.Ellipsoid.WGS84;
@@ -40,39 +28,6 @@ test('WGS84 constants equal Cesium', () => {
assert.equal(WGS84.maximumRadius, c.maximumRadius);
});
test('surface normals, scaling and conversions equal Cesium', () => {
const c = Cesium.Ellipsoid.WGS84;
for (const [x, y, z] of POINTS) {
const p = { x, y, z };
const cp = new Cesium.Cartesian3(x, y, z);
closeVec(
WGS84.geodeticSurfaceNormal(p),
c.geodeticSurfaceNormal(cp),
1e-12,
'normal',
);
const ours = WGS84.scaleToGeodeticSurface(p);
const theirs = c.scaleToGeodeticSurface(cp);
if (theirs === undefined) assert.equal(ours, undefined);
else closeVec(ours, theirs, 1e-12, 'scale');
const carto = WGS84.cartesianToCartographic(p);
const cCarto = c.cartesianToCartographic(cp);
if (cCarto === undefined) {
assert.equal(carto, undefined);
} else {
close(carto.longitude, cCarto.longitude, 1e-12, 'lon');
close(carto.latitude, cCarto.latitude, 1e-12, 'lat');
close(carto.height, cCarto.height, 1e-9, 'height');
closeVec(
WGS84.cartographicToCartesian(carto),
c.cartographicToCartesian(cCarto),
1e-12,
'roundtrip',
);
}
}
});
test('result parameters are filled in place and may alias the input', () => {
const p = { x: 1334762.2, y: -4654869.1, z: 4137287.9 };
const out = { x: 0, y: 0, z: 0 };
-48
View File
@@ -1,11 +1,8 @@
// Ported from CesiumJS 1.138 (Apache-2.0) — EllipsoidGeodesic.js.
// Vincenty inverse and surface interpolation, preserving operation order.
import * as Cartesian3 from './cartesian3.js';
import * as Cartographic from './cartographic.js';
import * as Ellipsoid from './ellipsoid.js';
import * as CesiumMath from './math.js';
import { Check } from './check.js';
import { DEBUG } from './debug.js';
const defined = (value) => value !== null && value !== undefined;
function setConstants(ellipsoidGeodesic) {
const uSquared = ellipsoidGeodesic._uSquared;
@@ -230,29 +227,7 @@ function vincentyInverseFormula(
ellipsoidGeodesic._uSquared = uSquared;
}
const scratchCart1 = Cartesian3.create();
const scratchCart2 = Cartesian3.create();
function computeProperties(ellipsoidGeodesic, start, end, ellipsoid) {
const firstCartesian = Cartesian3.normalize(
ellipsoid.cartographicToCartesian(start, scratchCart2),
scratchCart1,
);
const lastCartesian = Cartesian3.normalize(
ellipsoid.cartographicToCartesian(end, scratchCart2),
scratchCart2,
);
if (DEBUG) {
Check.typeOf.number.greaterThanOrEquals(
'value',
Math.abs(
Math.abs(Cartesian3.angleBetween(firstCartesian, lastCartesian)) -
Math.PI,
),
0.0125,
);
}
vincentyInverseFormula(
ellipsoidGeodesic,
ellipsoid.maximumRadius,
@@ -322,10 +297,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
*/
surfaceDistance: {
get: function () {
if (DEBUG) {
Check.defined('distance', this._distance);
}
return this._distance;
},
},
@@ -362,10 +333,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
*/
startHeading: {
get: function () {
if (DEBUG) {
Check.defined('distance', this._distance);
}
return this._startHeading;
},
},
@@ -378,10 +345,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
*/
endHeading: {
get: function () {
if (DEBUG) {
Check.defined('distance', this._distance);
}
return this._endHeading;
},
},
@@ -394,11 +357,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
* @param {Cartographic} end The final planetodetic point on the path.
*/
EllipsoidGeodesic.prototype.setEndPoints = function (start, end) {
if (DEBUG) {
Check.defined('start', start);
Check.defined('end', end);
}
computeProperties(this, start, end, this._ellipsoid);
};
@@ -425,17 +383,11 @@ EllipsoidGeodesic.prototype.interpolateUsingFraction = function (
* @param {number} distance The distance from the initial point to the point of interest along the geodesic
* @param {Cartographic} [result] The object in which to store the result.
* @returns {Cartographic} The location of the point along the geodesic.
*
* @exception {DeveloperError} start and end must be set before calling function interpolateUsingSurfaceDistance
*/
EllipsoidGeodesic.prototype.interpolateUsingSurfaceDistance = function (
distance,
result,
) {
if (DEBUG) {
Check.defined('distance', this._distance);
}
const constants = this._constants;
const s = constants.distanceRatio + distance / constants.b;
-14
View File
@@ -66,17 +66,3 @@ test('endpoint reset, result reuse, custom sphere and dateline/polar paths retai
);
}
});
test('unset and near-antipodal geodesics reject the same debug inputs', () => {
assert.throws(() => geo.ellipsoidGeodesic.create().surfaceDistance, {
name: 'DeveloperError',
});
const a = geo.cartographic.fromDegrees(0, 0),
b = geo.cartographic.fromDegrees(180, 0);
assert.throws(() => geo.ellipsoidGeodesic.create(a, b), {
name: 'DeveloperError',
});
assert.throws(
() => geo.ellipsoidGeodesic.create().setEndPoints(undefined, b),
{ name: 'DeveloperError' },
);
});
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff. Show more