Files
gods-eye-view/scripts/qa-imagery-contract.mjs
T
German Khokhlov 60f9de8511 Wait for the application, not the clock, in the strict QA set
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.
2026-11-10 15:46:26 +04:00

479 lines
16 KiB
JavaScript

import { whenSettled } from './qa/ready.mjs';
// 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-imagery-contract.mjs http://127.0.0.1:<PORT>
// [--layers=recent-imagery] [--evidence=FILE]
import { closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import sharp from 'sharp';
import {
createStrict,
localPreview,
strictArguments,
} from './qa-strict-harness.mjs';
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
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: [],
errors: [],
expectedErrors: [],
requests: [],
};
let failLandsat = false,
failedTiles = 0;
// The engine the page runs, for the strict assertion manifest.
let engine = null;
const strict = createStrict({
origin,
layers: ['recent-imagery'],
// The scenario answers the NASA catalog and imagery tiles itself.
fixtures: ['*://*.earthdata.nasa.gov/*'],
});
const check = (name, value) => {
report.checks.push({ name, pass: Boolean(value) });
if (value && engine) strict.record('recent-imagery', engine, name);
console.log(`${value ? 'PASS' : 'FAIL'} ${name}`);
};
const colors = await Promise.all(
['#ed3838', '#35c969'].map((background) =>
sharp({ create: { width: 256, height: 256, channels: 4, background } })
.png()
.toBuffer(),
),
);
const browser = await strict.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
page.on('pageerror', (e) => report.errors.push(redact(e.message)));
page.on('console', (message) => {
if (message.type() !== 'error') return;
const text = redact(message.text());
const expected =
isExpectedPreviewSetupError(message, {
preview: true,
baseUrl: url,
}) ||
(failedTiles > 0 &&
((/\b503\b/.test(text) &&
String(message.location()?.url).includes('HLS_L30')) ||
(failLandsat && /Failed to obtain image tile/.test(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', {
patterns: [{ urlPattern: '*://*.earthdata.nasa.gov/*' }],
});
session.on('Fetch.requestPaused', async (event) => {
try {
const request = new URL(event.request.url);
const catalog = request.hostname === 'cmr.earthdata.nasa.gov';
const green = request.href.includes('2026-10-27');
const tileFailure =
failLandsat &&
request.pathname.includes('HLS_L30') &&
request.hostname.startsWith('gibs');
if (tileFailure) failedTiles++;
report.requests.push({ host: request.hostname, path: request.pathname });
const body = catalog
? Buffer.from(
JSON.stringify({
hits: 2,
items: ['2026-10-28', '2026-10-27'].map((day) => ({
umm: {
TemporalExtent: {
RangeDateTime: { BeginningDateTime: `${day}T12:00:00Z` },
},
AdditionalAttributes: [
{ Name: 'CLOUD_COVERAGE', Values: ['0'] },
],
SpatialExtent: {
HorizontalSpatialDomain: {
Geometry: {
BoundingRectangles: [
{
WestBoundingCoordinate: -98,
EastBoundingCoordinate: -97,
SouthBoundingCoordinate: 30,
NorthBoundingCoordinate: 31,
},
],
},
},
},
},
})),
}),
)
: colors[green ? 1 : 0];
await session.send('Fetch.fulfillRequest', {
requestId: event.requestId,
responseCode: tileFailure ? 503 : 200,
responseHeaders: [
{
name: 'Content-Type',
value: catalog ? 'application/json' : 'image/png',
},
{ name: 'Access-Control-Allow-Origin', value: '*' },
{ name: 'Access-Control-Expose-Headers', value: 'Data-Present' },
{ name: 'Data-Present', value: 'true' },
{ name: 'Cache-Control', value: 'no-store' },
],
body: body.toString('base64'),
});
} catch (e) {
if (
String(e.message).includes(
'Fetch.fulfillRequest): Invalid InterceptionId',
)
)
report.expectedErrors.push(
'Fixture response cancelled before fulfillment',
);
else if (!page.isClosed()) report.errors.push(redact(e.message));
}
});
const target = new URL(url);
target.searchParams.set('welcome', '0');
target.hash = 'lat=30.27&lon=-97.74&alt=12000&heading=0&pitch=-90&map=osm';
await page.goto(target.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() => window.__gevRecentImagery && window.__godsEyeView,
{ timeout: 90000 },
);
engine = await page.evaluate(() => window.__godsEyeView.engine.kind);
await page.evaluate(async () => {
const app = window.__godsEyeView;
await app.styleManager.initialRestorePromise;
app.styleManager.orbitController.stop();
for (const [id, entry] of app.dataManager.layers)
if (entry.enabled) await app.dataManager.setEnabled(id, false);
app.styleManager._setDetectionMode('OFF');
await app.dataManager.setEnabled('recent-imagery', true);
window.__gevRecentImagery.layer.setBox({
west: -97.77,
east: -97.71,
south: 30.25,
north: 30.29,
});
});
await page.waitForFunction(
() =>
window.__gevRecentImagery.layer
.getSnapshot()
.candidates.filter(
(c) => c.product === 'S30' && c.thumbnail?.status === 'present',
).length >= 2,
{ timeout: 30000 },
);
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
const c = l
.getSnapshot()
.candidates.filter((c) => c.product === 'S30')
.sort((a, b) => b.day.localeCompare(a.day));
l.setAssignment('a', c[0].key);
l.setAssignment('b', c[1].key);
l.setMode('image');
});
await page.waitForFunction(
() => window.__godsEyeView.engine.surface.tilesLoaded(),
{ timeout: 90000 },
);
await whenSettled(page);
const capture = async (name) => {
await page.evaluate(() => window.__godsEyeView.requestRender('imagery-qa'));
await whenSettled(page);
const buffer = await page.screenshot();
await fs.writeFile(output.replace(/\.json$/, `-${name}.png`), buffer);
const pixels = await sharp(buffer)
.raw()
.toBuffer({ resolveWithObject: true });
const sample = (x, y) =>
Array.from(
pixels.data.subarray(
(y * pixels.info.width + x) * pixels.info.channels,
(y * pixels.info.width + x) * pixels.info.channels + 3,
),
);
const result = {
left: sample(565, 400),
right: sample(715, 400),
outside: sample(340, 400),
};
report[name] = result;
return result;
};
const red = (p) => p[0] > p[1] * 1.6 && p[0] > 100;
const green = (p) => p[1] > p[0] * 1.6 && p[1] > 100;
let image = await capture('image');
check(
'image pixels inside selected box',
red(image.left) && red(image.right),
);
check('geographic clip outside box', !red(image.outside));
await page.evaluate(() => window.__gevRecentImagery.layer.setMode('ab'));
await whenSettled(page);
image = await capture('ab');
check('A/B dates on opposite sides', red(image.left) && green(image.right));
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 whenSettled(page);
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');
check('swap reverses dates', green(image.left) && red(image.right));
await page.evaluate(() => window.__gevRecentImagery.layer.swapSides());
await page.mouse.move(640, 400);
await page.mouse.down();
await page.mouse.move(780, 400, { steps: 10 });
await page.mouse.up();
image = await capture('divider');
check(
'physical divider drag changes rendered split',
red(image.left) && red(image.right),
);
await page.focus('.recent-imagery-split-handle');
await page.keyboard.press('Home');
image = await capture('home');
check('divider Home exposes B', green(image.left) && green(image.right));
await page.keyboard.press('End');
image = await capture('end');
check('divider End exposes A', red(image.left) && red(image.right));
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
l.setMode('image');
l.setAlpha(0);
});
const base = await capture('transparent');
await page.evaluate(() => window.__gevRecentImagery.layer.setAlpha(0.5));
const half = await capture('half');
check(
'half opacity blends source with basemap',
half.left.every(
(c, i) => Math.abs(c - (base.left[i] + report.image.left[i]) / 2) < 5,
),
);
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
l.setAlpha(1);
l.setMode('basemap');
});
image = await capture('basemap');
check(
'basemap comparison clips only A',
red(image.left) && !red(image.right),
);
report.beforeToggle = await page.evaluate(() =>
window.__gevRecentImagery.layer.diagnostics(),
);
for (let i = 0; i < 10; i++)
await page.evaluate(async () => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('recent-imagery', false);
await app.dataManager.setEnabled('recent-imagery', true);
});
await page.waitForFunction(
() => window.__gevRecentImagery.layer.diagnostics().ownedCount === 1,
{ timeout: 30000 },
);
await whenSettled(page);
image = await capture('toggles');
check('ten toggles restore visible image', red(image.left));
report.afterToggle = await page.evaluate(() =>
window.__gevRecentImagery.layer.diagnostics(),
);
if (report.afterToggle.renderer)
check(
'Navara tile admission stays bounded',
report.afterToggle.renderer.peak <= 6 &&
Object.values(report.afterToggle.renderer.slots).every(
(s) => !s || s.tiles <= 64,
),
);
failLandsat = true;
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
l.setMode('image');
l.setAssignment(
'a',
l
.getSnapshot()
.candidates.find((c) => c.product === 'L30' && c.day === '2026-10-28')
.key,
);
});
await whenSettled(page);
image = await capture('source-failure');
check(
'failed replacement does not leave stale image pixels',
failedTiles > 0 && !red(image.left),
);
report.failedTiles = failedTiles;
failLandsat = false;
await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
const keys = l
.getSnapshot()
.candidates.filter((c) => c.product === 'S30')
.sort((a, b) => b.day.localeCompare(a.day))
.map((c) => c.key);
for (let i = 0; i < 10; i++) l.setAssignment('a', keys[i % 2]);
});
await whenSettled(page);
image = await capture('replaced');
check(
'rapid replacement leaves final date only',
green(image.left) && green(image.right),
);
await page.evaluate(() => window.__gevRecentImagery.layer.clear());
await whenSettled(page);
check(
'clear removes both slots',
await page.evaluate(
() => window.__gevRecentImagery.layer.diagnostics().ownedCount === 0,
),
);
const dragStart = await page.evaluate(() => {
const app = window.__godsEyeView;
return {
pose: app.engine.camera.pose().position,
started: window.__gevRecentImagery.tool.start(),
};
});
await page.mouse.move(480, 300);
await page.mouse.down();
await page.mouse.move(780, 500, { steps: 12 });
await capture('box-drag');
await page.mouse.up();
const dragged = await page.evaluate(() => ({
box: window.__gevRecentImagery.layer.getSnapshot().box,
active: window.__gevRecentImagery.tool.isActive(),
pose: window.__godsEyeView.engine.camera.pose().position,
}));
check(
'physical box drag selects geography without moving camera',
dragStart.started &&
dragged.box &&
!dragged.active &&
['x', 'y', 'z'].every(
(k) => Math.abs(dragged.pose[k] - dragStart.pose[k]) < 0.1,
),
);
const usedView = await page.evaluate(() =>
window.__gevRecentImagery.layer.useCurrentView(),
);
check('USE VIEW works on real engine camera', usedView);
await page.evaluate(() => {
const e = window.__godsEyeView.engine,
p = e.camera.pose();
e.camera.setView({
destination: {
x: p.position.x * 1.2,
y: p.position.y * 1.2,
z: p.position.z * 1.2,
},
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
});
await page.waitForFunction(
() =>
window.__godsEyeView.engine.camera.pose().cartographic.height > 1000000,
{ timeout: 10000 },
);
await whenSettled(page);
const fit = (report.fit = await page.evaluate(() => {
const l = window.__gevRecentImagery.layer;
const used = l.useCurrentView();
return { used, offered: l.getSnapshot().zoomToFit, height: l.zoomToFit() };
}));
check(
'oversized view refuses and offers bounded zoom',
!fit.used && fit.offered && fit.height >= 5000 && fit.height <= 400000,
);
await whenSettled(page);
check(
'ZOOM IN lands at requested height',
await page.evaluate(
(h) =>
Math.abs(
window.__godsEyeView.engine.camera.pose().cartographic.height - h,
) < 5,
fit.height,
),
);
await page.evaluate(async () => {
window.__gevRecentImagery.layer.clear();
await window.__godsEyeView.dataManager.setEnabled('recent-imagery', false);
});
await whenSettled(page);
report.disabled = await page.evaluate(() =>
window.__gevRecentImagery.layer.diagnostics(),
);
check(
'disable leaves no imagery owners or queued requests',
report.disabled.ownedCount === 0 &&
(!report.disabled.renderer ||
(report.disabled.renderer.active === 0 &&
report.disabled.renderer.queued === 0)),
);
check('no browser exceptions', report.errors.length === 0);
} catch (e) {
report.errors.push(e.stack);
check('driver completes', false);
} finally {
await closeBrowser(browser);
await fs.writeFile(output, JSON.stringify(report, null, 2));
}
const failed = report.checks.some((c) => !c.pass);
await strict.finishAndExit({ failed: failed ? 'imagery checks failed' : null });