mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
The probe exposes settled() and the first-frame outcome; scripts/qa/ready.mjs waits for drawn frames with the camera at rest and for a pick at the click position, with a deadline and its reason in the error. The strict scenarios use these instead of fixed pauses and loading-screen polling, and the hidden-switch scenario checks how the loading screen was lifted instead of how long it took. Each switch scenario now records a draw ledger per engine and runs a contract monitor; both write to the scenario's evidence file only, with an empty allowlist until browser runs show what differs on purpose.
457 lines
16 KiB
JavaScript
457 lines
16 KiB
JavaScript
import { waitForEngine } from './qa-strict-harness.mjs';
|
|
import { whenSettled } from './qa/ready.mjs';
|
|
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 waitForEngine(page, 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 whenSettled(page);
|
|
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 = () =>
|
|
page.evaluate(() => {
|
|
window.__godsEyeView.styleManager.shareLinkManager.flush();
|
|
return new URLSearchParams(location.hash.slice(1)).get('l');
|
|
});
|
|
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',
|
|
);
|
|
});
|