mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Navara now matches Cesium where the review found it diverging: a flight holds the camera inputs, camera.changed counts turns as Cesium does, render requests made during a frame survive it, follow pinches zoom, ground widths follow the pixel ratio and Cyber sonar reaches glyph caches and models. On Cesium, a staged imagery swap keeps loading tiles, setView replaces a running flight, and a cleared weather detail goes away. The contract gains MoverSet.failed, maps.terrainRevision, render.maxTextureSize and surface.sampleTerrainMostDetailed on every engine, so layers retry failed models, key road heights by terrain, size weather textures to the GPU and place Bhote Koshi cards on detailed terrain. Engine switches keep the live view, follows, focus, layer options and cards: a pick hands over the exact pose, applies no address options and pans no carried-over card; a history visit still applies its entry. Layers put GhostMaps pins, cable landings, OSM Places and directions on the measured surface without per-frame sampling, draw the Cockpit contact dot, hide disabled CCTV on attach, and keep ALPR running outside secure contexts. KML imports draw raised polygon fills, rotated and coloured ground overlays and north-referenced icon headings, and report files with nothing to draw. QA scripts read the contract instead of Cesium internals (test:track is 109/109 again), wait for the camera to rest before clicking, and give cold switches and parked engines realistic time. Docs follow the code.
471 lines
17 KiB
JavaScript
471 lines
17 KiB
JavaScript
import { assertKeylessPreview } from './qa-strict-harness.mjs';
|
|
import assert from 'node:assert/strict';
|
|
import sharp from 'sharp';
|
|
import { createStrict, localPreview } from './qa-strict-harness.mjs';
|
|
|
|
const base = localPreview(process.argv[2]);
|
|
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({}, 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:is([disabled], [aria-busy="true"])',
|
|
) &&
|
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
{ timeout: 90000 },
|
|
kind,
|
|
);
|
|
await assertKeylessPreview(page);
|
|
await page.evaluate(async () => {
|
|
await window.__godsEyeView.styleManager.initialRestorePromise;
|
|
});
|
|
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) {
|
|
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',
|
|
})
|
|
)
|
|
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',
|
|
);
|
|
});
|