mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
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:
1 parent
70ebdeb783
commit
f6f8fd8fa8
11 files changed
+1558
-97
No files matched your search
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user