mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Each script launches and closes Chrome through scripts/qa/browser.mjs, waits on the application through scripts/qa/ready.mjs instead of the loading screen and fixed pauses, and reads the contract instead of Cesium where the check is not about Cesium itself.
166 lines
6.2 KiB
JavaScript
166 lines
6.2 KiB
JavaScript
import { evaluateWithGeo } from './qa/geometry.mjs';
|
|
import { whenSettled } from './qa/ready.mjs';
|
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
|
/**
|
|
* qa-air-quality.mjs — headless proof for the Air quality layer.
|
|
*
|
|
* Checks what unit tests cannot: that the live ECCC feed answers, that readings
|
|
* reach the app in the provider-shaped form other networks will share, and that
|
|
* the Weather readout descriptor carries a legend and an in-view list.
|
|
*
|
|
* AQHI is reported by ECCC as an integer from 1 with "10+" above ten. The API
|
|
* returns the underlying decimal, so a reading arriving as 1.08 — or a station
|
|
* with no reading surfacing as a confident 1 — is a real defect this catches.
|
|
*
|
|
* Run: node scripts/qa-air-quality.mjs --url http://localhost:4173
|
|
*/
|
|
|
|
|
|
const argv = process.argv.slice(2);
|
|
const getOpt = (name, dflt) => {
|
|
const i = argv.indexOf(name);
|
|
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
|
};
|
|
const APP_URL = getOpt('--url', 'http://localhost:4173');
|
|
|
|
const results = [];
|
|
function record(name, ok, detail) {
|
|
results.push({ name, ok, detail });
|
|
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
|
|
}
|
|
|
|
async function main() {
|
|
let exitCode = 0;
|
|
const consoleErrors = [];
|
|
const browser = await launchBrowser({
|
|
headless: true,
|
|
args: ['--enable-unsafe-swiftshader', '--no-sandbox', '--disable-dev-shm-usage'],
|
|
defaultViewport: { width: 1400, height: 900 },
|
|
});
|
|
|
|
try {
|
|
const page = await browser.newPage();
|
|
page.on('pageerror', (e) => consoleErrors.push(String(e).slice(0, 160)));
|
|
page.on('console', (m) => {
|
|
if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 160));
|
|
});
|
|
|
|
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
|
await page.waitForFunction(
|
|
() => window.__godsEyeView?.engine && window.__godsEyeView?.dataManager,
|
|
{ timeout: 90000 },
|
|
);
|
|
await whenSettled(page);
|
|
|
|
const stats = await page.evaluate(async () => {
|
|
const dm = window.__godsEyeView.dataManager;
|
|
await dm.setEnabled('air-quality', true);
|
|
const mod = dm.layers.get('air-quality')?.module;
|
|
if (!mod) return { missing: true };
|
|
let s = null;
|
|
for (let i = 0; i < 45; i++) {
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
s = mod.getStats();
|
|
if (s.count > 0 || s.error) break;
|
|
}
|
|
return s;
|
|
});
|
|
|
|
const loaded = !stats?.missing && !stats?.error && stats?.count > 0;
|
|
record('stations report a current index', loaded,
|
|
`count=${stats?.count} error=${stats?.error}`);
|
|
if (!loaded) exitCode = 1;
|
|
|
|
const records = await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.dataManager
|
|
.layers.get('air-quality')
|
|
?.module.getAnalystRecords(500) || [],
|
|
);
|
|
|
|
// The provider-neutral contract other networks will share.
|
|
const shaped = records.every(
|
|
(r) => r.provider && r.scale && Number.isFinite(r.value) && r.band,
|
|
);
|
|
record('every reading carries provider, scale, value and band', shaped,
|
|
records.length
|
|
? `sample={provider:${records[0].provider}, scale:${records[0].scale}, value:${records[0].value}, band:${records[0].band}}`
|
|
: 'no records');
|
|
if (!shaped) exitCode = 1;
|
|
|
|
const published = records.every((r) => Number.isInteger(r.value) && r.value >= 1);
|
|
record('every AQHI value is in the published integer form, floored at 1', published,
|
|
`values=${[...new Set(records.map((r) => r.value))].sort((a, b) => a - b).join(',')}`);
|
|
if (!published) exitCode = 1;
|
|
|
|
const zones = [...new Set(records.map((r) => r.zone).filter(Boolean))].sort();
|
|
record('coverage spans multiple ECCC administrative zones', zones.length >= 3,
|
|
`zones=${zones.join(',') || 'none'} stations=${records.length}`);
|
|
if (zones.length < 3) exitCode = 1;
|
|
|
|
// The card's whole point is "highest readings in view", so put the camera
|
|
// over Canada before asserting on it — the default view is over Austin,
|
|
// where an empty in-view list is correct and proves nothing.
|
|
await evaluateWithGeo(page, async (qaGeo) => {
|
|
const d2r = Math.PI / 180;
|
|
try {
|
|
window.__godsEyeView.engine.camera.cancelFlight();
|
|
} catch {
|
|
/* no flight in progress */
|
|
}
|
|
window.__godsEyeView.engine.camera.setView({
|
|
destination: qaGeo.cartographic.toCartesian({
|
|
longitude: -100 * d2r,
|
|
latitude: 55 * d2r,
|
|
height: 4_000_000,
|
|
}),
|
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
|
});
|
|
window.__godsEyeView.engine.render.request?.();
|
|
});
|
|
await whenSettled(page);
|
|
|
|
const controls = await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.dataManager
|
|
.layers.get('air-quality')
|
|
?.module.getRowControls?.() || null,
|
|
);
|
|
const readoutOk =
|
|
controls?.readout === true &&
|
|
Array.isArray(controls?.legend?.colors) &&
|
|
controls.legend.colors.length === controls.legend.labels.length &&
|
|
Array.isArray(controls?.list?.items) &&
|
|
controls.list.items.length > 0 &&
|
|
controls.list.items.every((item) => item.id && item.label && item.value) &&
|
|
/Canada only/.test(controls?.summary?.coverage || '');
|
|
record('the Weather readout lists the worst readings in view, with a legend', readoutOk,
|
|
controls
|
|
? `bands=${controls.legend?.labels?.join('/')} inView=${controls.list?.items?.length} coverage="${controls.summary?.coverage}"`
|
|
: 'no descriptor');
|
|
if (!readoutOk) exitCode = 1;
|
|
|
|
record('no console errors', consoleErrors.length === 0,
|
|
consoleErrors.length ? consoleErrors.slice(0, 2).join(' | ') : 'clean');
|
|
if (consoleErrors.length) exitCode = 1;
|
|
} catch (e) {
|
|
console.error('\x1b[31mHarness error:\x1b[0m', e);
|
|
exitCode = 3;
|
|
} finally {
|
|
await closeBrowser(browser);
|
|
}
|
|
|
|
const pass = results.filter((r) => r.ok === true).length;
|
|
const fail = results.filter((r) => r.ok === false).length;
|
|
console.log('\n' + '─'.repeat(60));
|
|
console.log(` RESULT: ${pass} passed, ${fail} failed`);
|
|
console.log('─'.repeat(60) + '\n');
|
|
process.exit(exitCode || (fail > 0 ? 1 : 0));
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(3);
|
|
});
|