Files
gods-eye-view/src/tooling/streetLevelQa.test.mjs
T
Said TurkseverandClaude Opus 5.5 22b0009138 feat(street-level): Google Street View as the second imagery provider
Google Street View registers beside Mapillary as provider `google`. It runs
on the Maps JavaScript API with the browser GOOGLE_MAPS_API_KEY, loaded on
first use (mapsLoader.js), and uses Google's own StreetViewPanorama as its
viewer adapter, emitting provider-neutral poses (address and month in the
caption, a link to Google Maps, steps along Google's arrows, FOLLOW on
Google 3D). Its chip starts off because every panorama is billed to the
key; the share option is `g`.

Google offers no coverage layer for other maps, so the provider draws
none: with it on, a click on bare ground at street zoom opens its nearest
Google-collected outdoor panorama (groundClick.js). A key Google refuses
reads KEY REJECTED and fails a pending open at once; a lookup Google never
answers fails after 10 s.

Core: providers may declare `groundClick` and `defaultOn`; poses may carry
`capturedAtPrecision` and `title`; the coverage line keeps every
provider's hint; opening one provider's image releases another's
prewarmed viewer; FIT/FILL hides for viewers without render modes. CSP
script-src admits https://maps.googleapis.com. The browser gate gains two
Street View steps (the keyed one skips as 'no Google key' in CI).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 02:32:06 +01:00

642 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import { createApplicationStreetLevel } from '../app/layers/streetLevel.js';
import {
fixtureTile,
EXPECTED_PROVIDERS,
isCollapsed,
PARK,
STRICT_ALLOWED_SKIPS,
strictViolations,
} from '../../scripts/qa-street-level.mjs';
import {
answerMapillaryRequest,
describeCall,
metresApart,
PHOTO_LINE,
PHOTO_SEQUENCE_ID,
photoImages,
THUMB_HOST,
} from '../../scripts/fixtures/street-level/mapillaryGraph.mjs';
import { redact } from '../../scripts/qa-browserEvidence.mjs';
// The press helpers wait for paint; Node has no frames, so a tick stands in.
globalThis.requestAnimationFrame ??= (callback) =>
setTimeout(() => callback(Date.now()), 0);
test('the harness expects exactly the providers the app registers, in chip order', () => {
// Any method a provider asks its source for is a no-op: only ids matter.
const source = new Proxy({}, { get: () => () => {} });
const layer = createApplicationStreetLevel({
surface: null,
sources: { mapillary: source },
});
assert.deepEqual([...EXPECTED_PROVIDERS], [...layer.providerIds]);
});
test('a panel reads as collapsed only with the collapsed class', () => {
assert.equal(isCollapsed(['panel-collapsible', 'collapsed']), true);
assert.equal(isCollapsed(['panel-collapsible']), false);
});
test('the harness only runs its browser flow when executed directly', () => {
const source = fs.readFileSync(
new URL('../../scripts/qa-street-level.mjs', import.meta.url),
'utf8',
);
assert.match(
source,
/import\.meta\.url === pathToFileURL\(process\.argv\[1\]\)\.href/,
);
assert.match(source, /PUPPETEER_EXECUTABLE_PATH/);
assert.match(source, /--url/);
});
test('fixture tiles give the hermetic gate something real to filter', async () => {
const { decodeCoverageTile } =
await import('../layers/streetLevel/providers/mapillary/decode.js');
const now = Date.UTC(2026, 9, 1);
const street = decodeCoverageTile(fixtureTile(14, 2662, 6286, now), {
x: 2662,
y: 6286,
z: 14,
});
const grid = street.sequences.filter((s) => s.id !== PHOTO_SEQUENCE_ID);
assert.equal(grid.length, 8);
const pano = grid.filter((s) => s.isPano).length;
assert.ok(pano > 0 && pano < 8, '360° and flat both present');
const year = 365 * 86_400_000;
const old = grid.filter((s) => now - s.capturedAt > year).length;
assert.ok(old > 0 && old < 8, 'recent and older both present');
const orbit = decodeCoverageTile(fixtureTile(3, 1, 3, now), {
x: 1,
y: 3,
z: 3,
});
assert.ok(orbit.overview.length > 0, 'overview points from orbit');
assert.equal(fixtureTile(8, 1, 1, now).length, 0, 'nothing in between');
});
/* ── The real status route probe ─────────────────────────────────────── */
test('the gate requires the server’s real Mapillary status route', async () => {
const { assertRealStatusRoute } =
await import('../../scripts/qa-street-level.mjs');
const asked = [];
const answer = (body, init) => async (url) => {
asked.push(url);
return typeof body === 'string'
? new Response(body, init)
: Response.json(body, init);
};
assert.deepEqual(
await assertRealStatusRoute(
'http://localhost:4173',
answer({ configured: false }),
),
{ configured: false },
);
assert.deepEqual(asked, ['http://localhost:4173/api/mapillary/status']);
for (const [why, fetchImpl] of [
[
'an unregistered route (the API 404)',
answer({ error: 'Unknown API route' }, { status: 404 }),
],
[
'the SPA fallback',
answer('<!doctype html><title>GEV</title>', {
headers: { 'content-type': 'text/html' },
}),
],
['a malformed status', answer({ configured: 'yes' })],
[
'an unreachable server',
async () => {
throw new TypeError('fetch failed');
},
],
])
await assert.rejects(
assertRealStatusRoute('http://localhost:4173', fetchImpl),
/api\/mapillary\/status/,
why,
);
});
/* ── Header press-and-verify shared with the panel gate ───────────────── */
/**
* A page stand-in that runs page functions in Node against a fake document.
* The hit check always sees the header; `presses` says where each pointerdown
* lands ('header', 'other', 'moved' or 'none').
*/
function fakePanelPage({ presses = [], floating = false, stuck = false } = {}) {
const state = { floating, downs: 0, last: null, dragging: false, shift: 0 };
const listeners = [];
const node = (tagName, id, classes, inHeader) => ({
tagName,
id,
classList: classes,
closest: (selector) =>
inHeader && selector.includes('.panel-header') ? {} : null,
});
const header = node('DIV', '', ['panel-title'], true);
const other = node('SECTION', 'weather-panel', ['panel'], false);
const rect = (left, top, width, height) => () => ({
left,
top,
width,
height,
});
const document = {
querySelector: () => ({ getBoundingClientRect: rect(120, 210, 100, 20) }),
getElementById: () => ({
getBoundingClientRect: () => rect(100, 200 + state.shift, 300, 400)(),
classList: {
contains: (name) => name === 'panel-floating' && state.floating,
},
}),
elementFromPoint: () => header,
};
const window = {
addEventListener: (type, listener) => listeners.push(listener),
};
const inPage = (fn, ...args) => {
const saved = { document: globalThis.document, window: globalThis.window };
Object.assign(globalThis, { document, window });
try {
return fn(...args);
} finally {
Object.assign(globalThis, saved);
}
};
const evaluate = async (fn, ...args) => inPage(fn, ...args);
const press = () => {
state.last = presses[state.downs++] ?? 'header';
// 'none': the pointerdown never reaches the page.
if (state.last === 'none') return;
const target = state.last === 'header' ? header : other;
// 'moved': the panel's layout shifted just before the press landed.
if (state.last === 'moved') state.shift = -116;
for (const listener of listeners.splice(0)) inPage(listener, { target });
state.shift = 0;
};
return {
state,
evaluate,
async waitForFunction(fn, options, ...args) {
for (let i = 0; i < 3; i++) {
if (await evaluate(fn, ...args)) return;
await new Promise((resolve) => setTimeout(resolve, 1));
}
throw Object.assign(new Error('timed out'), { name: 'TimeoutError' });
},
mouse: {
async move() {},
async down() {
press();
state.dragging = true;
},
async up() {
if (state.dragging && state.last === 'header' && !stuck)
state.floating = true;
state.dragging = false;
},
async click(x, y, { clickCount }) {
if (clickCount === 1) press();
else if (state.last === 'header' && !stuck) state.floating = false;
},
},
};
}
test('a header press is retried only when it provably missed the header', async () => {
const { pressMissed } = await import('../../scripts/qa-panelDrag.mjs');
assert.equal(pressMissed({ done: true, pressed: null }), false);
assert.equal(
pressMissed({ done: false, pressed: { inHeader: true } }),
false,
);
assert.equal(
pressMissed({ done: false, pressed: { inHeader: false } }),
true,
);
// No recorded press is a failure, not a miss to retry.
assert.equal(pressMissed({ done: false, pressed: null }), false);
});
test('a lift whose press the rail moved off the header is retried, then lifts', async () => {
const { liftPanelByHeader, describePress } =
await import('../../scripts/qa-panelDrag.mjs');
const page = fakePanelPage({ presses: ['other', 'header'] });
const notes = [];
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
dx: -400,
dy: 80,
log: (line) => notes.push(line),
pauseMs: 0,
});
assert.equal(attempt.done, true);
assert.equal(page.state.downs, 2);
assert.equal(notes.length, 2, 'a note and a GitHub warning');
assert.match(notes[0], /missed the street-level-panel header/);
assert.match(notes[0], /section#weather-panel\.panel/, 'names what it hit');
assert.match(notes[0], /pre-press hit check passed/);
assert.match(notes[0], /stillness (held|timed out)/);
assert.match(
notes[0],
/panel box at hit check 100,200 300x400 -> at press 100,200 300x400/,
);
assert.match(notes[1], /^::warning title=Panel press retried::retry 1\/2/);
assert.match(describePress(attempt), /in header: true, after 1 retry$/);
});
test('a press in the header that does not lift fails at once, naming the target', async () => {
const { liftPanelByHeader, describePress } =
await import('../../scripts/qa-panelDrag.mjs');
const page = fakePanelPage({ stuck: true });
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
dx: -400,
dy: 80,
log: () => assert.fail('no retry'),
pauseMs: 0,
});
assert.equal(attempt.done, false);
assert.equal(page.state.downs, 1);
assert.match(describePress(attempt), /on div\.panel-title, in header: true/);
});
test('presses that keep missing stop after the retries and report the miss', async () => {
const { liftPanelByHeader, PRESS_RETRIES } =
await import('../../scripts/qa-panelDrag.mjs');
const page = fakePanelPage({ presses: ['other', 'other', 'other', 'other'] });
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
dx: -400,
dy: 80,
log: () => {},
pauseMs: 0,
});
assert.equal(attempt.done, false);
assert.equal(page.state.downs, PRESS_RETRIES + 1);
assert.equal(attempt.pressed.inHeader, false);
});
test('a header double-click docks, waiting on the docked state', async () => {
const { dockPanelByDoubleClick } =
await import('../../scripts/qa-panelDrag.mjs');
const page = fakePanelPage({ floating: true, presses: ['other', 'header'] });
const attempt = await dockPanelByDoubleClick(page, 'street-level-panel', {
log: () => {},
pauseMs: 0,
});
assert.equal(attempt.done, true);
assert.equal(page.state.floating, false);
assert.equal(page.state.downs, 2, 'the missed first press was retried');
});
test('both panel gates press the header through the shared helper', () => {
for (const script of ['qa-street-level.mjs', 'qa-panel-resize.mjs']) {
const source = fs.readFileSync(
new URL(`../../scripts/${script}`, import.meta.url),
'utf8',
);
assert.match(source, /from '\.\/qa-panelDrag\.mjs'/, script);
assert.match(source, /liftPanelByHeader\(page, PANEL_ID/, script);
assert.match(source, /dockPanelByDoubleClick\(page, PANEL_ID/, script);
}
});
/* ── Hermetic photo flow: the photo line and the Graph fixtures ────────── */
test('the photo sequence runs through the parked view, clipped to each tile it crosses', async () => {
const { decodeCoverageTile } =
await import('../layers/streetLevel/providers/mapillary/decode.js');
const { lonToTileX, latToTileY } =
await import('../layers/streetLevel/tileMath.js');
const x = lonToTileX(PHOTO_LINE.lon, 14);
const rows = new Set(
[PHOTO_LINE.south, PHOTO_LINE.north].map((lat) => latToTileY(lat, 14)),
);
assert.ok(rows.size > 1, 'the line crosses a tile edge');
let south = Infinity;
let north = -Infinity;
for (const y of rows) {
const line = decodeCoverageTile(fixtureTile(14, x, y), {
x,
y,
z: 14,
}).sequences.find((s) => s.id === PHOTO_SEQUENCE_ID);
assert.ok(line, `tile 14/${x}/${y} carries the photo line`);
for (const [lon, lat] of line.parts[0]) {
assert.ok(Math.abs(lon - PHOTO_LINE.lon) < 1e-4);
south = Math.min(south, lat);
north = Math.max(north, lat);
}
}
assert.ok(Math.abs(south - PHOTO_LINE.south) < 1e-4);
assert.ok(Math.abs(north - PHOTO_LINE.north) < 1e-4);
// Far away there is no photo line, only the grid.
const elsewhere = decodeCoverageTile(fixtureTile(14, 100, 100), {
x: 100,
y: 100,
z: 14,
});
assert.equal(
elsewhere.sequences.some((s) => s.id === PHOTO_SEQUENCE_ID),
false,
);
// The parked camera looks down onto the line.
assert.equal(PARK.lon, PHOTO_LINE.lon);
assert.ok(PARK.lat > PHOTO_LINE.south && PARK.lat < PHOTO_LINE.north);
});
test('the fixture photos: one sequence, 360° and flat, close enough for any nearest lookup on the line', () => {
const images = photoImages(Date.UTC(2026, 9, 1));
assert.ok(images.length >= 3);
assert.ok(images.some((image) => image.isPano));
assert.ok(images.some((image) => !image.isPano));
for (let i = 1; i < images.length; i++) {
const gap = metresApart(images[i - 1], images[i]);
// Above the cones' 3 m thinning, and at most 50 m apart: any point on
// the line has a photo within the Graph API's 50 m radius.
assert.ok(gap > 3 && gap < 50, `gap ${gap}`);
}
assert.equal(new Set(images.map((image) => image.id)).size, images.length);
});
const graph = (path, params) =>
`https://graph.mapillary.com/${path}?${new URLSearchParams({ access_token: 'MLY|0|qa-fixture', ...params })}`;
const answer = (url, method = 'GET') => {
const out = answerMapillaryRequest({ method, url });
return {
...out,
json: out.contentType === 'application/json' ? JSON.parse(out.body) : null,
};
};
test('the Graph fixtures answer the provider’s nearest and sequence lookups', () => {
const images = photoImages();
const middle = images[Math.floor(images.length / 2)];
const near = answer(
graph('images', {
lat: String(middle.lat + 0.0001),
lng: String(middle.lon),
radius: '50',
limit: '8',
fields: 'id,geometry,is_pano,captured_at,sequence',
}),
);
assert.equal(near.status, 200);
assert.ok(near.json.data.length > 0);
assert.ok(near.json.data.some((record) => record.id === middle.id));
for (const record of near.json.data) {
assert.deepEqual(Object.keys(record).sort(), [
'captured_at',
'geometry',
'id',
'is_pano',
'sequence',
]);
assert.equal(record.sequence, PHOTO_SEQUENCE_ID);
}
const far = answer(
graph('images', { lat: '0', lng: '0', radius: '50', fields: 'id' }),
);
assert.deepEqual(far.json.data, [], 'nothing outside the radius');
const sequence = answer(
graph('images', {
sequence_ids: PHOTO_SEQUENCE_ID,
fields: 'id,geometry,compass_angle,captured_at,is_pano',
limit: '2000',
}),
);
assert.equal(sequence.json.data.length, images.length);
assert.deepEqual(
answer(graph('images', { sequence_ids: 'other', fields: 'id' })).json.data,
[],
);
});
test('the Graph fixtures answer every call MapillaryJS makes to open a photo', () => {
const [first, second] = photoImages();
const spatial = answer(
graph('images', {
image_ids: `${first.id},${second.id}`,
fields:
'id,computed_geometry,geometry,sequence,camera_type,computed_rotation,thumb_1024_url,thumb_2048_url,width,height,merge_cc,sfm_cluster,mesh,creator,captured_at',
}),
);
assert.equal(spatial.status, 200);
assert.equal(spatial.headers['Access-Control-Allow-Origin'], '*');
const [pano, flat] = spatial.json.data;
assert.equal(pano.camera_type, 'spherical');
assert.equal(flat.camera_type, 'perspective');
assert.equal(pano.merge_cc, null, 'unmerged: no mesh request');
assert.equal(pano.sfm_cluster, null, 'no cluster request');
assert.equal(pano.computed_rotation.length, 3);
assert.equal(new URL(pano.thumb_2048_url).hostname, THUMB_HOST);
assert.equal(
answer(graph('images', { s2: '9749618446378729472', fields: 'id' })).json
.data.length,
0,
);
assert.deepEqual(
answer(graph('image_ids', { sequence_id: PHOTO_SEQUENCE_ID })).json.data[0],
{ id: first.id },
);
assert.deepEqual(
answer(graph(`${first.id}/tiles`, { z: '11', fields: 'url,z,x,y' })).json,
{ data: [] },
);
const photo = answer(pano.thumb_2048_url);
assert.equal(photo.status, 200);
assert.equal(photo.contentType, 'image/jpeg');
assert.deepEqual([...photo.body.subarray(0, 3)], [0xff, 0xd8, 0xff]);
const preflight = answer(graph('images', { image_ids: first.id }), 'OPTIONS');
assert.equal(preflight.status, 204);
assert.match(
preflight.headers['Access-Control-Allow-Headers'],
/Authorization/,
);
});
test('a Mapillary call no fixture covers is answered but flagged unknown', () => {
for (const url of [
graph('images', { bbox: '0,0,1,1' }),
graph('map_features', { fields: 'id' }),
'https://tiles.mapillary.com/maps/vtp/mly1_public/2/14/1/1',
]) {
const out = answer(url);
assert.equal(out.known, false, url);
assert.ok(out.status >= 400);
}
});
test('answered calls are named without the token', () => {
const call = describeCall(
'GET',
new URL(graph('9100000000000/tiles', { z: '11', fields: 'url' })),
);
assert.equal(call, 'GET graph.mapillary.com/{imageId}/tiles?fields&z');
assert.doesNotMatch(call, /MLY|access_token/);
assert.equal(
describeCall(
'GET',
new URL('https://qa-fixture.mapillary.com/thumb/9100000000016.jpg'),
),
'GET qa-fixture.mapillary.com/thumb/{imageId}.jpg',
);
});
test('evidence never carries a Mapillary token', () => {
assert.equal(
redact(
'GET https://graph.mapillary.com/images?access_token=MLY|123|abc&fields=id',
),
'GET https://graph.mapillary.com/images?access_token=REDACTED&fields=id',
);
assert.equal(
redact('token MLY|123|abc in a log'),
'token MLY|REDACTED in a log',
);
assert.equal(
redact(
'https://tile.googleapis.com/v1/3dtiles/x.glb?session=s1&key=AIzaSyBexampleexampleexampleexample00',
),
'https://tile.googleapis.com/v1/3dtiles/x.glb?session=s1&key=REDACTED',
);
assert.equal(
redact('loaded with AIzaSyBexampleexampleexampleexample00 inline'),
'loaded with AIza…REDACTED inline',
);
});
test('--strict accepts only the documented skips', () => {
assert.deepEqual(
[...STRICT_ALLOWED_SKIPS],
['no Google 3D', 'no Google key', 'fixtures only'],
);
assert.deepEqual(
strictViolations([
{ reason: 'no Google 3D', label: 'terrain' },
{ reason: 'no Google key', label: 'street view' },
{ reason: 'fixtures only', label: 'keyless page' },
{ reason: 'no Mapillary key', label: 'the keyed steps' },
{ reason: 'Google key refused', label: 'street view' },
]),
[
{ reason: 'no Mapillary key', label: 'the keyed steps' },
{ reason: 'Google key refused', label: 'street view' },
],
);
});
test('a press that records no pointerdown fails at once, without a retry', async () => {
const { liftPanelByHeader, describePress } =
await import('../../scripts/qa-panelDrag.mjs');
const page = fakePanelPage({ presses: ['none', 'header'] });
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
dx: -400,
dy: 80,
log: () => assert.fail('no retry'),
pauseMs: 0,
});
assert.equal(attempt.done, false);
assert.equal(attempt.pressed, null);
assert.equal(page.state.downs, 1);
assert.match(describePress(attempt), /no pointerdown reached the page/);
});
test('QA_FAIL_ON_RETRY turns a retry into a failure that says why', async () => {
const { liftPanelByHeader, failOnRetryDefault } =
await import('../../scripts/qa-panelDrag.mjs');
assert.equal(failOnRetryDefault({ QA_FAIL_ON_RETRY: '1' }, []), true);
assert.equal(failOnRetryDefault({}, ['--fail-on-retry']), true);
assert.equal(failOnRetryDefault({}, []), false);
const page = fakePanelPage({ presses: ['other', 'header'] });
await assert.rejects(
liftPanelByHeader(page, 'street-level-panel', {
dx: -400,
dy: 80,
log: () => {},
pauseMs: 0,
failOnRetry: true,
}),
/QA_FAIL_ON_RETRY=1 .*missed the street-level-panel header.*pre-press hit check passed/,
);
assert.equal(page.state.downs, 1);
});
test('QA_FAIL_ON_RETRY retries, with a warning, a miss the panel moving explains', async () => {
const { liftPanelByHeader, panelMoved } =
await import('../../scripts/qa-panelDrag.mjs');
assert.equal(panelMoved({ left: 0, top: 382 }, { left: 0, top: 266 }), true);
assert.equal(panelMoved({ left: 0, top: 382 }, { left: 1, top: 383 }), false);
const page = fakePanelPage({ presses: ['moved', 'header'] });
const lines = [];
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
dx: -400,
dy: 80,
log: (line) => lines.push(line),
pauseMs: 0,
failOnRetry: true,
});
assert.equal(attempt.done, true, 'the second press lifted it');
assert.equal(page.state.downs, 2);
assert.ok(
lines.some((line) =>
/::warning title=Panel press retried::.*-> at press 100,84/.test(line),
),
lines.join('\n'),
);
});
test('both gates save failure evidence and fail on render-loop errors', () => {
for (const script of ['qa-street-level.mjs', 'qa-panel-resize.mjs']) {
const source = fs.readFileSync(
new URL(`../../scripts/${script}`, import.meta.url),
'utf8',
);
assert.match(source, /saveFailureArtifacts\(/, script);
assert.match(source, /hookRenderErrors\(/, script);
assert.match(source, /readRenderErrors\(/, script);
assert.match(source, /--fail-on-retry/, script);
}
});
test('CI runs the gates hermetically against a production build, strict, with evidence', () => {
const ci = fs.readFileSync(
new URL('../../.github/workflows/ci.yml', import.meta.url),
'utf8',
);
const job = ci.slice(ci.indexOf('street-level-browser:'));
const gate = job.slice(0, job.indexOf('\n windows-onboarding:'));
// The dummy token is baked into the bundle at build time and the preview
// server's status route reads it at run time: both steps carry it.
assert.match(
gate,
/MAPILLARY_CLIENT_TOKEN: 'MLY\|0\|qa-fixture'\s+run: npm run build/,
);
assert.match(
gate,
/MAPILLARY_CLIENT_TOKEN: 'MLY\|0\|qa-fixture'\s+run: \|\s+npx vite preview --port 4173 --strictPort/,
);
assert.doesNotMatch(gate, /npx vite --port/);
assert.equal(
(gate.match(/MAPILLARY_CLIENT_TOKEN: 'MLY\|0\|qa-fixture'/g) || []).length,
2,
);
assert.match(gate, /qa:street-level:fixtures -- --strict/);
assert.match(
gate,
/if: failure\(\)[\s\S]*actions\/upload-artifact@[0-9a-f]{40}/,
);
assert.match(gate, /path: qa-artifacts\//);
assert.match(
gate,
/QA_FAIL_ON_RETRY: '1'/,
'a missed header press fails CI now that the rail settles',
);
});