Files
gods-eye-view/scripts/qa-gbfs-names.mjs
T
German Khokhlov 0ef29781e5 Fix the review findings across the engines, layers, data and QA
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.
2026-11-09 21:34:42 +04:00

616 lines
20 KiB
JavaScript

#!/usr/bin/env node
/**
* Keyless station-name regression against the real globe and bike-share tool.
* Run with a dev or QA preview server and an explicitly assigned --url.
* GBFS documents are synthetic; no operator feed or provider key is required.
*/
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import { fromDegrees } from '../src/geo/cartesian3.js';
import { watchBrowserPage, redact } from './qa-browserEvidence.mjs';
const urlIndex = process.argv.indexOf('--url');
if (urlIndex < 0) throw new Error('An explicit --url is required');
const appUrl = process.argv[urlIndex + 1];
const engineName = new URL(appUrl).searchParams.get('engine') || 'cesium';
const extended = process.argv.includes('--w2');
const reportIndex = process.argv.indexOf('--report');
const reportPath =
reportIndex < 0 ? null : path.resolve(process.argv[reportIndex + 1]);
const shots = reportPath
? reportPath.replace(/\.json$/, '-shots')
: path.resolve(`qa-shots/gbfs-names/${engineName}`);
fs.mkdirSync(shots, { recursive: true });
const report = { engine: engineName, checks: [], poses: {}, browserErrors: [] };
const information = {
version: '3.0',
data: {
stations: [
{
station_id: 'english',
name: [
{ language: 'hu', text: 'Magyar tér' },
{ language: 'en', text: 'QA English Square' },
],
},
{
station_id: 'short',
name: [null, { language: 'en', text: ' ' }],
short_name: [{ language: 'en-GB', text: 'QA Short Name' }],
},
{ station_id: 'unnamed', name: [null, {}, { language: 42, text: [] }] },
{ station_id: 'legacy', name: ' QA Legacy Station ' },
{
station_id: 'fallback',
name: [null, { language: 'hu', text: 'QA Magyar Station' }],
},
].map((station, index) => ({
...station,
lat: 30.2681 + index * 0.001,
lon: -97.7469 + index * 0.001,
capacity: 13,
})),
},
};
const status = {
data: {
stations: information.data.stations.map(({ station_id }) => ({
station_id,
num_bikes_available: 4,
num_docks_available: 9,
is_renting: true,
is_returning: true,
})),
},
};
const browser = await puppeteer.launch({
headless: 'new',
...(process.env.PUPPETEER_EXECUTABLE_PATH
? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH }
: {}),
args:
process.platform === 'darwin'
? ['--use-angle=metal']
: ['--use-gl=angle', '--use-angle=swiftshader'],
});
let checks = 0;
const check = (name, actual, expected) => {
report.checks.push({ name, actual, expected });
assert.deepEqual(actual, expected, name);
checks++;
console.log(`[PASS] ${name}`);
};
async function open(mode = 'normal') {
const page = await browser.newPage();
await page.bringToFront();
const errors = [];
if (extended)
watchBrowserPage(page, report, {
name: mode,
baseUrl: appUrl,
preview: process.argv.includes('--preview'),
});
page.on('pageerror', (error) => errors.push(error.message));
await page.setViewport({ width: 1440, height: 900 });
await page.evaluateOnNewDocument(
({ information, status, mode }) => {
localStorage.clear();
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
window.__qaPainted = [];
const paint = CanvasRenderingContext2D.prototype.fillText;
CanvasRenderingContext2D.prototype.fillText = function (text, ...args) {
if (
String(text).startsWith('QA ') ||
String(text) === 'Station unnamed'
) {
window.__qaPainted.push(String(text));
if (window.__qaPainted.length > 100) window.__qaPainted.shift();
}
return paint.call(this, text, ...args);
};
const original = window.fetch.bind(window);
window.fetch = async (input, init) => {
const url = String(
typeof input === 'string' || input instanceof URL ? input : input.url,
);
if (url.includes('/api/gbfs/')) {
if (mode === 'failure') return new Response('{}', { status: 503 });
const payload =
mode === 'empty'
? { data: { stations: [] } }
: decodeURIComponent(url).includes('station_information')
? information
: status;
return new Response(JSON.stringify(payload), {
headers: { 'Content-Type': 'application/json' },
});
}
if (url.includes('/api/flights')) return new Response('{"states":[]}');
if (url.includes('/api/military')) return new Response('{"ac":[]}');
if (url.includes('/api/vessels'))
return new Response('{"connected":false,"vessels":[]}');
return original(input, init);
};
},
{ information, status, mode },
);
await page.goto(appUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager && window.__godsEyeView?.voiceCommands,
{ timeout: 60000 },
);
await page.waitForFunction(
() =>
!document.getElementById('loading-screen') ||
(document.getElementById('loading-screen').classList.contains('hidden') &&
getComputedStyle(document.getElementById('loading-screen')).opacity ===
'0'),
{ timeout: 60000 },
);
await page.evaluate(
async (destination) => {
const { dataManager, engine } = window.__godsEyeView;
await window.__godsEyeView.styleManager.initialRestorePromise;
window.__godsEyeView.styleManager.orbitController.stop();
for (const [id, layer] of dataManager.layers) {
if (layer.enabled)
await dataManager.setEnabled(id, false, { origin: 'user' });
}
engine.camera.cancelFlight();
engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await dataManager.setEnabled('bikeshare', true, { origin: 'user' });
window.__godsEyeView.requestRender('gbfs-name-qa');
},
fromDegrees(-97.7449, 30.2701, 3000),
);
return { page, errors };
}
async function nextFrame(page) {
await page.evaluate(
() =>
new Promise((resolve) => {
const engine = window.__godsEyeView.engine;
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
engine.render.request('gbfs-raster-qa');
}),
);
}
async function settledTiles(page) {
await page.evaluate(async () => {
const engine = window.__godsEyeView.engine;
const start = performance.now();
let since = null;
while (performance.now() - start < 90000) {
if (engine.surface.tilesLoaded()) since ??= performance.now();
else since = null;
if (since !== null && performance.now() - since > 1500) return;
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error('Map tiles did not settle for raster sampling');
});
}
async function w2Checks(page) {
await page.evaluate(async () => {
const app = window.__godsEyeView;
const detection = await import('/src/data/detection.js');
detection.setMode('OFF');
const { appClock } = await import('/src/time/clock.js');
appClock.freeze(Date.parse('2026-10-28T19:00:00Z'));
const canvas = app.engine.host.canvas;
const style = document.createElement('style');
style.textContent =
'.gbfs-raster-hidden { visibility: hidden !important; }';
document.head.append(style);
for (const element of document.body.querySelectorAll('*'))
if (element !== canvas && !element.contains(canvas))
element.classList.add('gbfs-raster-hidden');
});
for (const [name, height] of [
['city', 10000],
['close', 3000],
['global', 100000],
]) {
await page.evaluate(
async (destination) => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('bikeshare', false);
app.engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
},
fromDegrees(-97.7449, 30.2701, height),
);
await settledTiles(page);
await nextFrame(page);
const off = await page.screenshot({
path: path.join(shots, `${name}-off.png`),
});
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('bikeshare', true),
);
await page.waitForFunction(
(count) =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getStats().count === count,
{},
name === 'global' ? 0 : 5,
);
await nextFrame(page);
const controls = await page.evaluate(() => {
const app = window.__godsEyeView;
const rect = app.engine.host.canvas.getBoundingClientRect();
return app.dataManager.layers
.get('bikeshare')
.module.getDetectableObjects()
.map((record) => {
const p = app.engine.projector.worldToWindow(record.position);
return { id: record.sourceId, x: p.x + rect.left, y: p.y + rect.top };
});
});
const on = await page.screenshot({
path: path.join(shots, `${name}-on.png`),
});
const a = await sharp(off)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const b = await sharp(on).ensureAlpha().raw().toBuffer();
for (const control of controls) {
let pixels = 0;
for (
let y = Math.max(0, Math.floor(control.y) - 16);
y < Math.min(a.info.height, control.y + 16);
y++
)
for (
let x = Math.max(0, Math.floor(control.x) - 16);
x < Math.min(a.info.width, control.x + 16);
x++
) {
const i = (y * a.info.width + x) * 4;
if (
Math.max(
...[0, 1, 2].map((c) => Math.abs(a.data[i + c] - b[i + c])),
) > 3
)
pixels++;
}
control.changedPixels = pixels;
}
report.poses[name] = controls;
check(
`${name} expected station population`,
controls.length,
name === 'global' ? 0 : 5,
);
if (controls.length)
check(
`${name} five nonempty marker raster controls`,
controls.every((c) => c.changedPixels > 0),
true,
);
}
await page.evaluate(() => {
for (const element of document.querySelectorAll('.gbfs-raster-hidden'))
element.classList.remove('gbfs-raster-hidden');
});
await page.evaluate(
async (destination) => {
const app = window.__godsEyeView;
app.engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await app.dataManager.setEnabled('bikeshare', false);
},
fromDegrees(-97.7449, 30.2701, 3000),
);
for (let i = 0; i < 10; i++) {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('bikeshare', true),
);
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getStats().count === 5,
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('bikeshare', false),
);
}
check(
'ten cycles clear all records',
await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getStats().count,
),
0,
);
const idle = await page.evaluate(async () => {
const app = window.__godsEyeView;
let frames = 0,
last = performance.now();
const remove = app.engine.frames.postRender.addEventListener(() => {
frames++;
last = performance.now();
});
const start = performance.now();
while (
performance.now() - start < 60000 &&
(performance.now() - last < 2000 || !app.engine.surface.tilesLoaded())
)
await new Promise((resolve) => setTimeout(resolve, 100));
const ready =
performance.now() - last >= 2000 && app.engine.surface.tilesLoaded();
frames = 0;
await new Promise((resolve) => setTimeout(resolve, 2000));
remove();
return { ready, frames, governor: app.getRenderGovernorDiagnostics() };
});
report.idle = idle;
check('camera and tiles settle before idle sample', idle.ready, true);
check(
'disabled static scene renders zero frames over two seconds',
idle.frames,
0,
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('bikeshare', true),
);
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers.get('bikeshare').module.getStats()
.count === 5,
);
}
try {
const { page, errors } = await open();
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers.get('bikeshare').module.getStats()
.count === 5,
{ timeout: 15000 },
);
await page.evaluate(async () => {
const { engine } = window.__godsEyeView;
const rendered = new Promise((resolve) => {
const remove = engine.frames.postRender.addEventListener(() => {
remove();
resolve();
});
});
window.__godsEyeView.requestRender('gbfs-name-pick-qa');
await rendered;
});
const records = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getAnalystRecords(),
);
check(
'the layer retains all five stations and normalized names',
records.map((r) => r.name),
[
'QA English Square',
'QA Short Name',
null,
'QA Legacy Station',
'QA Magyar Station',
],
);
const tool = await page.evaluate(() =>
window.__godsEyeView.voiceCommands.runner('get_bike_share', {
area: { lat: 30.2701, lon: -97.7449, radius_km: 2 },
}),
);
check('the real bike-share tool succeeds', tool.ok, true);
check(
'the tool preserves localized, short, unnamed, legacy and non-English names',
tool.data.rows.map((r) => r.name).sort(),
[
null,
'QA English Square',
'QA Short Name',
'QA Legacy Station',
'QA Magyar Station',
].sort(),
);
check(
'availability remains attached to every station',
tool.data.rows.every(
(r) => r.bikes_available === 4 && r.docks_available === 9,
),
true,
);
if (extended) await w2Checks(page);
const clickPoint = await page.evaluate(() => {
window.__qaPainted = [];
const { engine } = window.__godsEyeView;
const station = window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getDetectableObjects()
.find(({ sourceId }) => sourceId === 'austin-capmetro:english');
const point = engine.projector.worldToWindow(station.position);
const rect = engine.host.canvas.getBoundingClientRect();
const picked = engine.picker.pick(point);
return {
x: rect.left + point.x,
y: rect.top + point.y,
pickedId: typeof picked?.id === 'string' ? picked.id : null,
primitiveId:
typeof picked?.primitive?.id === 'string' ? picked.primitive.id : null,
};
});
check(
'the scene pick resolves the first stable station key',
clickPoint.pickedId || clickPoint.primitiveId,
'austin-capmetro:english',
);
await page.mouse.click(clickPoint.x, clickPoint.y);
await page.waitForFunction(
() =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getDetectableObjects()
.some((record) => record.skipLabel),
{ timeout: 10000 },
);
await page.waitForFunction(
() => window.__qaPainted.includes('QA English Square'),
{ timeout: 10000 },
);
check(
'a real pointer click selects the station with its localized name',
await page.evaluate(() => window.__qaPainted.includes('QA English Square')),
true,
);
await page.keyboard.press('Escape');
for (const [id, title] of [
['english', 'QA English Square'],
['short', 'QA Short Name'],
['unnamed', 'Station unnamed'],
]) {
await page.evaluate((id) => {
window.__qaPainted = [];
const app = window.__godsEyeView;
app.dataManager.layers
.get('bikeshare')
.module._selectBikeshareStationForTest(`austin-capmetro:${id}`);
app.requestRender('gbfs-name-selection-qa');
}, id);
await page.waitForFunction(
(title) => window.__qaPainted.includes(title),
{ timeout: 10000 },
title,
);
check(
`the selected ${id} card paints the expected title`,
await page.evaluate((title) => window.__qaPainted.includes(title), title),
true,
);
await page.screenshot({ path: path.join(shots, `${id}.png`) });
await page.keyboard.press('Escape');
await page.waitForFunction(() =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getDetectableObjects()
.every((record) => !record.skipLabel),
);
check(
`Escape clears the ${id} card`,
await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getDetectableObjects()
.filter((record) => record.skipLabel).length,
),
0,
);
}
await page.setViewport({ width: 390, height: 844 });
await page.evaluate(
async (destination) => {
window.__qaPainted = [];
const app = window.__godsEyeView;
// Use the supported clean view to inspect the mobile card clear of panels.
app.styleManager.toggleCleanView(true);
app.engine.camera.cancelFlight();
app.engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
app.dataManager.layers
.get('bikeshare')
.module._selectBikeshareStationForTest('austin-capmetro:english');
app.requestRender('gbfs-name-mobile-qa');
},
fromDegrees(-97.7449, 30.2701, 3000),
);
await page.waitForFunction(
() => window.__qaPainted.includes('QA English Square'),
{ timeout: 10000 },
);
check(
'the mobile selected card paints its normalized name',
await page.evaluate(() => window.__qaPainted.includes('QA English Square')),
true,
);
await page.screenshot({ path: path.join(shots, 'mobile.png') });
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('bikeshare', false, {
origin: 'user',
}),
);
check(
'disabling clears selected cards and analyst records',
await page.evaluate(() => ({
count: window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getAnalystRecords().length,
selected: window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getDetectableObjects()
.filter((record) => record.skipLabel).length,
})),
{ count: 0, selected: 0 },
);
check('normal fixtures produce no page exceptions', errors, []);
await page.close();
for (const mode of ['empty', 'failure']) {
const { page, errors } = await open(mode);
await page.waitForFunction(
() =>
!window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getStats().loading,
);
check(
`${mode} leaves no rendered stations`,
await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers
.get('bikeshare')
.module.getStats().count,
),
0,
);
check(`${mode} produces no page exceptions`, errors, []);
await page.close();
}
if (extended)
check(
'all pages have no unexpected runtime errors',
report.browserErrors,
[],
);
report.pass = true;
console.log(`GBFS names: ${checks}/${checks} checks passed; ${appUrl}`);
} catch (error) {
report.pass = false;
report.failure = redact(error.message);
throw error;
} finally {
if (reportPath)
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
await browser.close();
}