test(mapillary): provider, source, presentation, markup and QA-harness coverage

scripts/qa-mapillary.mjs drives a real browser through the rail panel,
the keyless gate, coverage, a ready-made feature plan, the viewer and its
expanded dialog at 1440×900 and 1280×800.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Fable 5.1 committed 2026-09-23 15:44:32 +01:00
1 parent 70ebdeb783
commit f6f8fd8fa8
11 files changed
+1558 -97

No files matched your search

+38 -3
View File
@@ -4,6 +4,8 @@ import {
bboxFromGeocode,
enforceMinimumSpan,
fitBboxToTileBudget,
noResultsAnswer,
pluralLabel,
summarizeResults,
} from './query.js';
import { countTilesForBbox } from './tileMath.js';
@@ -54,15 +56,48 @@ test('summarizeResults reports totals, top classes and the sighting window', ()
});
assert.match(
text,
/^1,234 features: 1,000 fire hydrant, 200 bench, 20 mailbox and 1 more classes/,
/^1,234 features: 1,000 fire hydrants, 200 benches, 20 mailboxes and 1 more class/,
);
assert.match(text, /seen 2019–2026 · capped · 2 tiles failed$/);
});
test('summarizeResults leads with the class when there is only one', () => {
const text = summarizeResults({
total: 834,
counts: new Map([['object--fire-hydrant', 834]]),
earliest: Date.UTC(2017, 0, 1),
latest: Date.UTC(2021, 0, 1),
failed: 1,
});
assert.equal(text, '834 fire hydrants · seen 2017–2021 · 1 tile failed');
assert.equal(
summarizeResults({ total: 0, counts: new Map() }),
'No matching features in this area.',
summarizeResults({ total: 1, counts: new Map([['object--bench', 1]]) }),
'1 bench',
);
});
test('zero results produce no summary so the answer can be replaced', () => {
assert.equal(summarizeResults({ total: 0, counts: new Map() }), '');
assert.equal(
noResultsAnswer(
{ title: 'Fire hydrants · Springfield' },
{ label: 'Springfield, IL' },
),
'No fire hydrants mapped in Springfield, IL yet. Try a larger area, another place, or set SINCE back to any date.',
);
assert.match(
noResultsAnswer({}, null),
/^No matching features mapped here yet/,
);
});
test("pluralLabel handles the taxonomy's English labels", () => {
assert.equal(pluralLabel('Fire hydrant', 2), 'fire hydrants');
assert.equal(pluralLabel('Bench', 3), 'benches');
assert.equal(pluralLabel('Mailbox', 4), 'mailboxes');
assert.equal(pluralLabel('Bench', 1), 'bench');
});
test('node-sized geocode bounds are grown to a queryable neighbourhood box', () => {
const tiny = bboxFromGeocode({
geometry: {
+145
View File
@@ -0,0 +1,145 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createMapillarySource, MapillarySourceError } from './source.js';
function fakeFetch(handler) {
const calls = [];
const fetchImpl = async (url, init = {}) => {
calls.push({ url: String(url), init });
return handler(String(url), init);
};
return { calls, fetchImpl };
}
const jsonResponse = (payload, status = 200) =>
new Response(JSON.stringify(payload), {
status,
headers: { 'content-type': 'application/json' },
});
test('tiles go through the local proxy and never carry the token', async () => {
const { calls, fetchImpl } = fakeFetch(
() => new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
);
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
const bytes = await source.getTile('coverage', 14, 2662, 6285);
assert.deepEqual([...bytes], [1, 2, 3]);
assert.equal(calls[0].url, '/api/mapillary/tiles/coverage/14/2662/6285');
assert.doesNotMatch(calls[0].url, /MLY/);
});
test('an empty tile (204) is an empty byte array, not an error', async () => {
const { fetchImpl } = fakeFetch(() => new Response(null, { status: 204 }));
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
assert.equal((await source.getTile('coverage', 14, 1, 1)).length, 0);
});
test('a missing key on the proxy surfaces as keyRequired', async () => {
const { fetchImpl } = fakeFetch(() =>
jsonResponse({ error: 'no_key', keyRequired: true }, 503),
);
const source = createMapillarySource({ token: '', fetchImpl });
assert.equal(source.hasToken(), false);
await assert.rejects(
() => source.getTile('coverage', 14, 1, 1),
(error) =>
error instanceof MapillarySourceError &&
error.status === 503 &&
error.keyRequired === true,
);
});
test('graph lookups call graph.mapillary.com with the client token and requested fields', async () => {
const { calls, fetchImpl } = fakeFetch(() =>
jsonResponse({ data: [{ id: '1', geometry: { coordinates: [1, 2] } }] }),
);
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
const images = await source.getSequenceImages('seq-1');
assert.equal(images.length, 1);
const url = new URL(calls[0].url);
assert.equal(url.host, 'graph.mapillary.com');
assert.equal(url.pathname, '/images');
assert.equal(url.searchParams.get('access_token'), 'MLY|1|abc');
assert.equal(url.searchParams.get('sequence_ids'), 'seq-1');
assert.match(url.searchParams.get('fields'), /geometry/);
const nearest = await source.nearestImages({
lat: 38.5,
lon: -121.5,
radius: 500,
limit: 500,
});
assert.equal(nearest.length, 1);
const nearestUrl = new URL(calls[1].url);
assert.equal(
nearestUrl.searchParams.get('radius'),
'50',
'radius is clamped to the API maximum',
);
assert.equal(nearestUrl.searchParams.get('limit'), '100');
});
test('graph calls without a token fail fast as keyRequired without a request', async () => {
const { calls, fetchImpl } = fakeFetch(() => jsonResponse({}));
const source = createMapillarySource({ token: '', fetchImpl });
await assert.rejects(
() => source.getImage('123'),
(error) =>
error instanceof MapillarySourceError && error.keyRequired === true,
);
assert.equal(calls.length, 0);
});
test('graph API errors carry the upstream message and status', async () => {
const { fetchImpl } = fakeFetch(() =>
jsonResponse({ error: { message: 'Invalid OAuth 2.0 Access Token' } }, 400),
);
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
await assert.rejects(
() => source.getMapFeature('999'),
(error) =>
error instanceof MapillarySourceError &&
error.status === 400 &&
/Invalid OAuth/.test(error.message),
);
});
test('feature queries stream NDJSON records and resolve with the done record', async () => {
const lines = [
JSON.stringify({ type: 'start', tiles: 2 }),
JSON.stringify({
type: 'tile',
rows: [[-121.5, 38.5, 'a', 'object--bench', 0, 0]],
}),
JSON.stringify({ type: 'done', total: 1 }),
].join('\n');
const { calls, fetchImpl } = fakeFetch(
() =>
new Response(lines, {
status: 200,
headers: { 'content-type': 'application/x-ndjson' },
}),
);
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
const seen = [];
const done = await source.queryFeatures(
{
bbox: [-121.5, 38.5, -121.4, 38.6],
layer: 'points',
values: ['object--bench'],
},
(record) => seen.push(record.type),
);
assert.deepEqual(seen, ['start', 'tile', 'done']);
assert.equal(done.total, 1);
assert.equal(calls[0].url, '/api/mapillary/features');
assert.equal(calls[0].init.method, 'POST');
});
test('sprite URLs are proxied per taxonomy value', () => {
const source = createMapillarySource({ token: 'MLY|1|abc' });
assert.equal(
source.spriteUrl('object--fire-hydrant'),
'/api/mapillary/sprite/object--fire-hydrant.svg',
);
});