Files
gods-eye-view/src/layers/streetLevel/uiState.test.mjs
T
Said TurkseverandClaude Opus 5.5 cb01014e36 feat(street-level): KartaView as a third, keyless imagery provider
Coverage lines from KartaView's z13-14 JSON tiles (OSM ways with photos,
chained end to start), the photo nearest a clicked line, cones along its
sequence, and a viewer for flat photos and 360° panoramas (a small WebGL
equirectangular renderer, no new dependency) with ‹ › along the sequence.

Mapillary's camera-driven line coverage and sequence cones move to the
provider-neutral core (lineCoverage.js, sequenceCones.js) with the tile
fetch, zoom ladder, colours and pick prefixes injected; Mapillary keeps
thin wrappers and its tests pass unchanged.

KartaView quirks handled: api.kartaview.org (the documented
api.openstreetcam.org serves empty tiles), photo URLs that name storage1
for photos stored elsewhere, and a photo API that answers 429 to more than
about three requests at once (two in flight, retried with backoff).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 18:33:14 +01:00

239 lines
7.4 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 test from 'node:test';
import { composeUIState, summarizeCoverage } from './uiState.js';
import { COLORS } from './policy.js';
import { providerSnapshot } from '../../testSupport/streetLevelFakes.mjs';
const provider = (overrides = {}) =>
providerSnapshot({ count: 10, ...overrides });
const base = {
enabled: true,
filter: { pano: 'all', sinceDays: 0 },
street: { open: false },
sequence: { selectedId: null, images: 0, loading: false },
};
test('counts add up across active providers only', () => {
const ui = composeUIState({
...base,
providers: [
provider(),
provider({ id: 'panoramax', name: 'Panoramax', count: 5, on: false }),
provider({ id: 'kartaview', name: 'KartaView', count: 7, loading: true }),
],
});
assert.equal(ui.coverage.count, 17);
assert.equal(ui.coverage.loading, true);
});
test('the layer is key-gated only when every switched-on provider lacks its key', () => {
const gated = composeUIState({
...base,
providers: [provider({ keyRequired: true })],
});
assert.equal(gated.keyRequired, true);
const mixed = composeUIState({
...base,
providers: [
provider({ keyRequired: true }),
provider({ id: 'panoramax', name: 'Panoramax', requiresKeyId: null }),
],
});
assert.equal(mixed.keyRequired, false, 'a keyless provider still draws');
const offOnly = composeUIState({
...base,
providers: [provider({ keyRequired: true, on: false })],
});
assert.equal(offOnly.keyRequired, false, 'nothing switched on to gate');
});
test('the legend is one swatch per active source, in its colour, then Selected', () => {
const single = composeUIState({ ...base, providers: [provider()] });
assert.deepEqual(single.legend, [
{ key: 'mapillary', label: 'Mapillary', color: '#05cb63' },
{ key: 'selected', label: 'Selected', color: COLORS.selected },
]);
const two = composeUIState({
...base,
providers: [
provider(),
provider({ id: 'panoramax', name: 'Panoramax', color: '#a66bff' }),
],
});
assert.deepEqual(
two.legend.map((entry) => [entry.label, entry.color]),
[
['Mapillary', '#05cb63'],
['Panoramax', '#a66bff'],
['Selected', COLORS.selected],
],
);
const oneOff = composeUIState({
...base,
providers: [
provider(),
provider({ id: 'panoramax', name: 'Panoramax', on: false }),
],
});
assert.deepEqual(
oneOff.legend.map((entry) => entry.key),
['mapillary', 'selected'],
);
assert.deepEqual(
composeUIState({ ...base, providers: [provider({ on: false })] }).legend,
[],
);
});
test('Street View draws no lines: no legend swatch, and no Selected on its own', () => {
const streetView = provider({
id: 'google',
name: 'Google Street View',
color: '#4285f4',
count: 0,
groundClick: true,
filters: [],
});
assert.deepEqual(
composeUIState({ ...base, providers: [streetView] }).legend,
[],
);
assert.deepEqual(
composeUIState({
...base,
providers: [provider(), streetView],
}).legend.map((entry) => entry.key),
['mapillary', 'selected'],
);
});
test('the filter controls show only for a lit provider that honours them', () => {
const streetView = provider({ id: 'google', groundClick: true, filters: [] });
const controls = (providers) =>
composeUIState({ ...base, providers }).filterControls;
assert.deepEqual(controls([streetView]), { pano: false, since: false });
assert.deepEqual(controls([provider(), streetView]), {
pano: true,
since: true,
});
assert.deepEqual(
controls([provider({ on: false }), streetView]),
{ pano: false, since: false },
'Mapillary switched off',
);
assert.deepEqual(
controls([provider({ on: false }), { ...streetView, on: false }]),
{ pano: true, since: true },
'nothing lit: both, ready for the first chip',
);
});
test('hint and error come from the first active provider that has one', () => {
const ui = composeUIState({
...base,
providers: [
provider({ hint: '', error: null }),
provider({ id: 'b', name: 'B', hint: 'Look down', error: 'boom' }),
],
});
assert.equal(ui.coverage.hint, 'Look down');
assert.equal(ui.coverage.error, 'boom');
assert.deepEqual(ui.filter, base.filter);
assert.notEqual(ui.filter, base.filter, 'snapshot copies the filter');
});
test('summarizeCoverage is what getStats reports, without building a snapshot', () => {
assert.deepEqual(
summarizeCoverage([
provider({ count: 4, hint: 'Look down' }),
provider({ id: 'b', name: 'B', count: 6, loading: true, error: 'boom' }),
provider({ id: 'c', name: 'C', count: 99, on: false, keyRequired: true }),
]),
{
count: 10,
loading: true,
hint: 'Look down',
error: 'boom',
label: null,
keyRequired: false,
keyRejected: false,
},
);
assert.equal(summarizeCoverage([]).keyRequired, false);
});
test('a rejected key gates like a missing one and is named as rejected', () => {
const rejected = provider({
keyRequired: true,
keyRejected: true,
error: 'Mapillary rejected MAPILLARY_CLIENT_TOKEN',
});
const summary = summarizeCoverage([rejected]);
assert.equal(summary.keyRequired, true);
assert.equal(summary.keyRejected, true);
assert.equal(summary.error, 'Mapillary rejected MAPILLARY_CLIENT_TOKEN');
const state = composeUIState({ ...base, providers: [rejected] });
assert.equal(state.keyRequired, true);
assert.equal(state.keyRejected, true);
// A provider that is merely missing its key is not "rejected".
assert.equal(
summarizeCoverage([provider({ keyRequired: true })]).keyRejected,
false,
);
});
test('the surface mode defaults to draped and passes through', () => {
assert.equal(
composeUIState({ ...base, providers: [provider()] }).surface,
'draped',
);
assert.equal(
composeUIState({ ...base, providers: [provider()], surface: 'terrain' })
.surface,
'terrain',
);
});
test('every switched-on provider’s hint is kept, once each', async () => {
const { summarizeCoverage } = await import('./uiState.js');
const { providerSnapshot } =
await import('../../testSupport/streetLevelFakes.mjs');
const summary = summarizeCoverage([
providerSnapshot({ hint: 'zoom in for sequences' }),
providerSnapshot({
id: 'google',
hint: 'click a street to open Street View',
}),
providerSnapshot({ id: 'off', on: false, hint: 'not shown' }),
providerSnapshot({ id: 'dup', hint: 'zoom in for sequences' }),
]);
assert.equal(
summary.hint,
'zoom in for sequences · click a street to open Street View',
);
});
test('a key-gated layer offers the switched-off sources that would draw with a key already set', async () => {
const { keyAlternatives } = await import('./uiState.js');
const mapillary = providerSnapshot({ keyRequired: true, configured: false });
const streetView = providerSnapshot({
id: 'google',
label: 'STREET VIEW',
on: false,
configured: true,
requiresKeyId: 'google-maps',
});
assert.deepEqual(keyAlternatives([mapillary, streetView]), [
{ id: 'google', label: 'STREET VIEW', requiresKeyId: 'google-maps' },
]);
// Nothing to offer: Mapillary has its key, or Street View has none either.
assert.deepEqual(keyAlternatives([providerSnapshot(), streetView]), []);
assert.deepEqual(
keyAlternatives([
mapillary,
{ ...streetView, configured: false, keyRequired: true },
]),
[],
);
});