mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Exercise every catalog layer on both planets and verify live switch continuity
This commit is contained in:
1 parent
05b646daa3
commit
62296a833c
6 files changed
+224
-15
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import assert from 'node:assert/strict';
|
||||
import { APPLICATION_LAYER_DIRECTORY } from '../src/app/layers/directory.js';
|
||||
|
||||
const base = new URL(process.argv[2]);
|
||||
if (
|
||||
!['127.0.0.1', 'localhost'].includes(base.hostname) ||
|
||||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||
base.port,
|
||||
)
|
||||
)
|
||||
throw Error('Supply an unreserved keyless local preview URL');
|
||||
const output = process.argv[3]
|
||||
? resolve(process.argv[3])
|
||||
: mkdtempSync(join(tmpdir(), 'gev-layer-switch-'));
|
||||
mkdirSync(output, { recursive: true });
|
||||
const runs = [
|
||||
['engine-switch', ['earthquakes']],
|
||||
['director-engine-switch', []],
|
||||
['layer-handoff', ['flights']],
|
||||
['layer-handoff', ['military'], '--military'],
|
||||
[
|
||||
'ground-layer-switch',
|
||||
['ais-live-vessels', 'air-quality', 'gnss-integrity', 'fire-perimeters'],
|
||||
],
|
||||
['contact-switch', ['military-awareness', 'military-installations']],
|
||||
['local-adsb-engine-switch', ['local-adsb']],
|
||||
['ghostmaps-switch', ['s2-ghostmaps']],
|
||||
[
|
||||
'weather-engine-switch',
|
||||
[
|
||||
'weather-radar',
|
||||
'weather-satellite',
|
||||
'weather-lightning',
|
||||
'weather-cyclones',
|
||||
],
|
||||
],
|
||||
['wind-engine-switch', ['wind']],
|
||||
['infrastructure-engine-switch', ['local-dams', 'local-datacenters']],
|
||||
['submarine-cables-engine-switch', ['telegeography-submarine-cables']],
|
||||
['directions-engine-switch', ['directions']],
|
||||
['nepal-locator-switch', ['bhote-koshi-locator']],
|
||||
['nepal-event-switch', ['bhote-koshi-2026']],
|
||||
['traffic-engine-switch', ['traffic']],
|
||||
['traffic-engine-switch', ['traffic'], '--simulation'],
|
||||
['street-level-engine-switch', ['street-level']],
|
||||
['street-level-engine-switch', ['street-level'], '--panoramax'],
|
||||
['radio-engine-switch', ['radio']],
|
||||
['w2-imagery', ['recent-imagery']],
|
||||
['fire-camera-switch', ['alpr-cameras']],
|
||||
['fire-camera-switch', ['local-firms'], '--fires'],
|
||||
['urban-engine-switch', ['bikeshare']],
|
||||
['urban-engine-switch', ['transit'], '--transit'],
|
||||
['cctv-engine-switch', ['cctv']],
|
||||
['space-engine-switch', ['satellites']],
|
||||
['space-engine-switch', ['rocket-launches'], '--missions'],
|
||||
];
|
||||
const ids = [...new Set(runs.flatMap(([, ids]) => ids))].sort();
|
||||
assert.deepEqual(
|
||||
ids,
|
||||
APPLICATION_LAYER_DIRECTORY.map((l) => l.id).sort(),
|
||||
'Every catalog layer must have a browser scenario',
|
||||
);
|
||||
const report = [];
|
||||
for (const [name, layers, flag] of runs) {
|
||||
const label = name + (flag ?? '');
|
||||
const args =
|
||||
name === 'w2-imagery'
|
||||
? [
|
||||
'--url',
|
||||
base.origin + '/?engine=navara',
|
||||
'--report',
|
||||
join(output, 'imagery.json'),
|
||||
'--switch',
|
||||
'--preview',
|
||||
]
|
||||
: [base.origin, ...(flag ? [flag] : [])];
|
||||
const start = Date.now();
|
||||
const run = spawnSync(
|
||||
process.execPath,
|
||||
[fileURLToPath(new URL('./qa-' + name + '.mjs', import.meta.url)), ...args],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
timeout: 240000,
|
||||
maxBuffer: 8388608,
|
||||
env: { ...process.env, GOOGLE_MAPS_API_KEY: '', CESIUM_ION_TOKEN: '' },
|
||||
},
|
||||
);
|
||||
const ok = run.status === 0;
|
||||
writeFileSync(
|
||||
join(output, label + '.log'),
|
||||
(run.stdout || '') + (run.stderr || '') + (run.error?.message || ''),
|
||||
);
|
||||
report.push({
|
||||
label,
|
||||
layers,
|
||||
ok,
|
||||
seconds: Math.round((Date.now() - start) / 1000),
|
||||
});
|
||||
writeFileSync(
|
||||
join(output, 'summary.json'),
|
||||
JSON.stringify({ ids, report }, null, 2) + '\n',
|
||||
);
|
||||
console.log(ok ? 'PASS' : 'FAIL', label, report.at(-1).seconds);
|
||||
}
|
||||
console.log(
|
||||
ids.length +
|
||||
' layers, ' +
|
||||
report.filter((row) => row.ok).length +
|
||||
'/' +
|
||||
runs.length +
|
||||
' browser scenarios. ' +
|
||||
output,
|
||||
);
|
||||
if (report.some((row) => !row.ok)) process.exitCode = 1;
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { installTileCache } from './qa/tileCache.mjs';
|
||||
|
||||
@@ -29,7 +30,14 @@ try {
|
||||
window.fetch = (url, options) =>
|
||||
String(url).startsWith('https://earthquake.usgs.gov/earthquakes/feed/')
|
||||
? Promise.resolve(
|
||||
Response.json({ type: 'FeatureCollection', features: [] }),
|
||||
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' },
|
||||
}],
|
||||
}),
|
||||
)
|
||||
: fetch(url, options);
|
||||
});
|
||||
@@ -53,6 +61,24 @@ try {
|
||||
});
|
||||
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);
|
||||
console.log('PASS', kind, 'earthquake footprint red minus green:', off, '->', on);
|
||||
}
|
||||
const stored = () => page.evaluate(() => localStorage.getItem('gev.engine'));
|
||||
async function switchTo(kind) {
|
||||
if (
|
||||
@@ -151,6 +177,7 @@ try {
|
||||
}
|
||||
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
|
||||
await ready('cesium');
|
||||
await verifyQuakeSurface('cesium');
|
||||
const sharpen = () =>
|
||||
page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.getControlState().sharpen,
|
||||
@@ -162,6 +189,7 @@ try {
|
||||
});
|
||||
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(
|
||||
|
||||
@@ -5,6 +5,11 @@ import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { switchGlobe } from './qa-switch-globe.mjs';
|
||||
const url = new URL(process.argv[2]);
|
||||
const soak = process.argv.includes('--soak');
|
||||
const requested = process.argv.find((arg) => arg.startsWith('--swaps='));
|
||||
const swaps = requested ? Number(requested.slice(8)) : soak ? 50 : 2;
|
||||
if (requested && (!soak || !Number.isInteger(swaps) || swaps % 2 || swaps < 4 || swaps > 1000))
|
||||
throw Error('Use --soak --swaps=N with an even count from 4 to 1000');
|
||||
|
||||
if (
|
||||
!['localhost', '127.0.0.1'].includes(url.hostname) ||
|
||||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||
@@ -78,7 +83,7 @@ try {
|
||||
window.qaModels = new Map([...dataManager.layers].map(([id, entry]) => [id, entry.module]));
|
||||
window.qaEngines = new Map([[engine.kind, engine]]);
|
||||
});
|
||||
const swaps = soak ? 50 : 2, measurements = [], memory = [];
|
||||
const measurements = [], memory = [];
|
||||
let contextListeners;
|
||||
for (let i = 1; i <= swaps; i++) {
|
||||
const kind = i % 2 ? 'cesium' : 'navara';
|
||||
@@ -96,7 +101,7 @@ try {
|
||||
window.qaModels.size === 33 && window.qaQuakeReads >= 1 &&
|
||||
dataManager.getEnabledLayerIds().has('earthquakes');
|
||||
}));
|
||||
if ([2, 10, 50].includes(i)) {
|
||||
if ([2, 10, 50, 100, 250, swaps].includes(i)) {
|
||||
const { result } = await client.send('Runtime.evaluate', {
|
||||
includeCommandLineAPI: true,
|
||||
returnByValue: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {switchGlobe} from './qa-switch-globe.mjs';
|
||||
import fs from 'node:fs/promises';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
import { installTileCache } from './qa/tileCache.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
|
||||
|
||||
@@ -51,6 +52,7 @@ try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
|
||||
await blockPaidQaMaps(page);
|
||||
await installTileCache(page);
|
||||
page.on('pageerror', (e) => report.errors.push(redact(e.message)));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() !== 'error') return;
|
||||
@@ -64,7 +66,10 @@ try {
|
||||
((/\b503\b/.test(text) &&
|
||||
String(message.location()?.url).includes('HLS_L30')) ||
|
||||
(failLandsat && /Failed to obtain image tile/.test(text))));
|
||||
(expected ? report.expectedErrors : report.errors).push(text);
|
||||
const source = message.location()?.url;
|
||||
(expected ? report.expectedErrors : report.errors).push(
|
||||
source ? text + ' (' + new URL(source, url).hostname + ')' : text,
|
||||
);
|
||||
});
|
||||
const session = await page.createCDPSession();
|
||||
await session.send('Fetch.enable', {
|
||||
|
||||
@@ -266,14 +266,41 @@ try {
|
||||
await manager.setEnabled('weather-cyclones', true, {origin:'user'});
|
||||
window.qaStorm = manager.layers.get('weather-cyclones').module;
|
||||
},ids);
|
||||
const click = async (lon,lat) => {
|
||||
const at = await p.evaluate(position => {
|
||||
const {engine} = window.__godsEyeView;
|
||||
const xy = engine.projector.worldToWindow(position,{});
|
||||
const rect = engine.host.canvas.getBoundingClientRect();
|
||||
return [rect.left+xy.x,rect.top+xy.y];
|
||||
},fromDegrees(lon,lat));
|
||||
await p.mouse.click(...at);
|
||||
const click = async (lon, lat, insideCone) => {
|
||||
const hit = await p.waitForFunction(
|
||||
({ lon, lat, base, insideCone }) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const longitude = (lon * Math.PI) / 180,
|
||||
latitude = (lat * Math.PI) / 180;
|
||||
const height =
|
||||
engine.surface.terrainHeight({ longitude, latitude, height: 0 }) ?? 0;
|
||||
const xy = engine.projector.worldToWindow(
|
||||
{
|
||||
x: base.x + height * Math.cos(latitude) * Math.cos(longitude),
|
||||
y: base.y + height * Math.cos(latitude) * Math.sin(longitude),
|
||||
z: base.z + height * Math.sin(latitude),
|
||||
},
|
||||
{},
|
||||
);
|
||||
if (!xy) return false;
|
||||
const canvas = engine.host.canvas,
|
||||
rect = canvas.getBoundingClientRect();
|
||||
const x = rect.left + xy.x,
|
||||
y = rect.top + xy.y;
|
||||
if (document.elementFromPoint(x, y) !== canvas) return false;
|
||||
const id = engine.picker.pick(xy)?.id;
|
||||
if (
|
||||
String(id?.id ?? id).startsWith('cyclone:al012026:cone:') !==
|
||||
insideCone
|
||||
)
|
||||
return false;
|
||||
return [x, y];
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
{ lon, lat, base: fromDegrees(lon, lat), insideCone },
|
||||
);
|
||||
await p.mouse.click(...(await hit.jsonValue()));
|
||||
await hit.dispose();
|
||||
};
|
||||
const before = cycloneReads;
|
||||
for (const kind of ['navara','cesium','navara']) {
|
||||
@@ -285,10 +312,9 @@ try {
|
||||
window.qaStorm.getDiagnostics().selectedId === 'al012026');
|
||||
const d = await p.evaluate(()=>window.qaStorm.getDiagnostics());
|
||||
assert.deepEqual([d.storms,d.tracks,d.cones,d.forecastPoints],[1,1,1,1],kind);
|
||||
await new Promise(r => setTimeout(r,1200));
|
||||
await click(-79.43,43.59);
|
||||
await click(-79.43,43.59,false);
|
||||
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === null);
|
||||
await click(-79.30,43.643);
|
||||
await click(-79.30,43.643,true);
|
||||
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === 'al012026');
|
||||
assert.equal(cycloneReads,before);
|
||||
console.log('PASS',kind,'pick advisory cone and retain selected storm');
|
||||
|
||||
@@ -245,6 +245,32 @@ test('the catalog per engine: Cesium as before, Navara only what it can show', a
|
||||
assert.equal(subset.get('military'), undefined);
|
||||
});
|
||||
|
||||
test('both production registries attach to the same 33 session models', async (t) => {
|
||||
const { registerNavaraLayerRenderers } = await import('./navaraMode.js');
|
||||
const lifetime = new AbortController();
|
||||
t.after(() => lifetime.abort());
|
||||
const options = {
|
||||
layerSession: { lifetime },
|
||||
sources: createStandaloneLayerSources(),
|
||||
signal: lifetime.signal,
|
||||
surface: fixtureSurface(lifetime.signal),
|
||||
};
|
||||
const cesium = createApplicationCatalog(options);
|
||||
await registerNavaraLayerRenderers();
|
||||
const navara = createApplicationCatalog({ ...options, engineKind: 'navara' });
|
||||
assert.equal(cesium.layers.length, 33);
|
||||
assert.deepEqual(
|
||||
navara.layers.map(({ id }) => id),
|
||||
cesium.layers.map(({ id }) => id),
|
||||
);
|
||||
assert.deepEqual(navara.metadata, cesium.metadata);
|
||||
for (const layer of cesium.layers) {
|
||||
assert.equal(navara.get(layer.id), layer, layer.id);
|
||||
assert.equal(typeof layer.attachRenderer, 'function', layer.id);
|
||||
assert.equal(typeof layer.detachRenderer, 'function', layer.id);
|
||||
}
|
||||
});
|
||||
|
||||
test('a GNSS integrity source can set the layer evidence window', (t) => {
|
||||
const lifetime = new AbortController();
|
||||
t.after(() => lifetime.abort());
|
||||
|
||||
Reference in new issue
Block a user