import assert from 'node:assert/strict'; import test from 'node:test'; import { presentStreetLevelPanel, SINCE_STOPS, sinceStopIndex, } from './streetLevelPresentation.js'; function snapshot(overrides = {}) { const base = { enabled: false, providerOn: true, keyRequired: false, keyRejected: false, filter: { pano: 'all', sinceDays: 0 }, coverage: { loading: false, count: 0, hint: '', error: null }, legend: [ { key: 'mapillary', label: 'Mapillary', color: '#05cb63' }, { key: 'selected', label: 'Selected', color: '#00d4ff' }, ], sequence: { selectedId: null, images: 0, loading: false }, street: { open: false, loading: false, error: null, imageId: null, position: null, bearing: null, isPano: false, capturedAt: null, sequenceId: null, creator: null, externalUrl: null, renderMode: 'letterbox', }, }; return deepMerge(base, overrides); } function deepMerge(target, source) { const out = { ...target }; for (const [key, value] of Object.entries(source)) { out[key] = value && typeof value === 'object' && !Array.isArray(value) && target[key] && typeof target[key] === 'object' ? deepMerge(target[key], value) : value; } return out; } test('a key-gated layer disables every control and flags KEY REQUIRED', () => { const view = presentStreetLevelPanel(snapshot({ keyRequired: true })); assert.equal(view.controlsDisabled, true); assert.equal(view.status.text, 'KEY REQUIRED'); assert.equal(view.status.tone, 'warn'); }); test('KEY REQUIRED says how to add the key instead of a raw error code', () => { const view = presentStreetLevelPanel( snapshot({ enabled: true, keyRequired: true, coverage: { error: 'no_key' }, }), ); assert.equal(view.status.text, 'KEY REQUIRED'); assert.match(view.error, /^Mapillary: Needs MAPILLARY_CLIENT_TOKEN/); }); test('a key Mapillary rejected reads KEY REJECTED and names the fix', () => { const error = 'Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings'; const view = presentStreetLevelPanel( snapshot({ enabled: true, keyRequired: true, keyRejected: true, coverage: { error }, }), ); assert.equal(view.status.text, 'KEY REJECTED'); assert.equal(view.status.tone, 'warn'); assert.equal(view.controlsDisabled, true); assert.equal(view.error, error); }); test('a key problem outranks loading on the pill: KEY REJECTED, then KEY REQUIRED (M65)', () => { // A keyless layer can still report loading (its status check, a retry): // the pill must say what is wrong, not that something is on its way. const loading = { enabled: true, coverage: { loading: true } }; assert.equal( presentStreetLevelPanel(snapshot({ ...loading, keyRequired: true })).status .text, 'KEY REQUIRED', ); assert.equal( presentStreetLevelPanel( snapshot({ ...loading, keyRequired: true, keyRejected: true }), ).status.text, 'KEY REJECTED', ); }); test('the header pill is the on/off switch and reads OFF, LOADING or ON', () => { assert.deepEqual(presentStreetLevelPanel(snapshot()).status, { text: 'OFF', tone: '', pressed: false, title: 'Turn Street Level on', }); assert.deepEqual( presentStreetLevelPanel( snapshot({ enabled: true, coverage: { loading: true } }), ).status, { text: 'LOADING', tone: 'busy', pressed: true, title: 'Turn Street Level off', }, ); assert.deepEqual( presentStreetLevelPanel(snapshot({ enabled: true })).status, { text: 'ON', tone: 'on', pressed: true, title: 'Turn Street Level off' }, ); assert.equal( presentStreetLevelPanel(snapshot({ enabled: true, keyRequired: true })) .status.pressed, true, 'a keyless layer that is on can still be switched off', ); }); test('errors from the viewer or the coverage web surface in one alert', () => { assert.equal(presentStreetLevelPanel(snapshot()).error, null); assert.equal( presentStreetLevelPanel( snapshot({ street: { error: 'Image could not be opened' } }), ).error, 'Image could not be opened', ); assert.equal( presentStreetLevelPanel(snapshot({ coverage: { error: 'Tile HTTP 502' } })) .error, 'Tile HTTP 502', ); }); test('the SINCE slider runs from any date on the left to the last month on the right', () => { assert.deepEqual( SINCE_STOPS.map((stop) => stop.days), [0, 3652, 1826, 1095, 730, 365, 182, 91, 30], ); assert.equal(sinceStopIndex(0), 0); assert.equal(sinceStopIndex(365), 5); assert.equal( sinceStopIndex(400), 5, 'an off-stop link lands on the nearest stop', ); assert.equal(sinceStopIndex(-4), 0); }); test('the SINCE readout names the window and the cut-off date it means today', () => { const now = Date.UTC(2026, 8, 25); const any = presentStreetLevelPanel(snapshot(), { now }); assert.deepEqual(any.since, { index: 0, days: 0, label: 'ANY DATE' }); const year = presentStreetLevelPanel( snapshot({ filter: { pano: 'pano', sinceDays: 365 } }), { now }, ); assert.deepEqual(year.filter, { pano: 'pano', sinceDays: 365 }); assert.deepEqual(year.since, { index: 5, days: 365, label: 'LAST YEAR · SINCE 2025-09-25', }); const custom = presentStreetLevelPanel( snapshot({ filter: { pano: 'all', sinceDays: 400 } }), { now }, ); assert.equal(custom.since.index, 5); assert.equal(custom.since.label, 'LAST 400 DAYS · SINCE 2025-08-21'); }); test('the meta line never mixes the visible-sequence count with the selected sequence', () => { assert.equal( presentStreetLevelPanel(snapshot()).meta, 'Switch Street Level on to draw its coverage.', ); const browsing = presentStreetLevelPanel( snapshot({ enabled: true, coverage: { count: 812 } }), ); assert.match(browsing.meta, /^812 sequences in view · click a line/); const selected = presentStreetLevelPanel( snapshot({ enabled: true, coverage: { count: 812 }, sequence: { selectedId: 'abc', images: 33 }, }), ); assert.equal(selected.meta, '33 images in this sequence · Esc clears'); assert.doesNotMatch(selected.meta, /sequences in view/); const hinted = presentStreetLevelPanel( snapshot({ enabled: true, coverage: { hint: 'Point the camera at the globe' }, }), ); assert.equal(hinted.meta, 'Point the camera at the globe'); }); test('viewer caption reads "Image by" left, date right, and links to Mapillary', () => { const view = presentStreetLevelPanel( snapshot({ enabled: true, street: { open: true, imageId: '1814275685699406', creator: 'mapfool', capturedAt: Date.UTC(2023, 9, 8), bearing: 93.4, isPano: true, externalUrl: 'https://www.mapillary.com/app/?pKey=1814275685699406&focus=photo', }, }), ); assert.equal(view.viewer.captionLeft, 'Image by mapfool'); assert.equal(view.viewer.captionRight, '360° · 93° · 2023-10-08'); assert.equal( view.viewer.link, 'https://www.mapillary.com/app/?pKey=1814275685699406&focus=photo', ); assert.equal(view.viewer.linkLabel, 'MAPILLARY ↗'); assert.equal(view.viewer.open, true); }); test('an image without a creator name leaves the left caption empty', () => { const view = presentStreetLevelPanel( snapshot({ street: { open: true, imageId: '1', capturedAt: Date.UTC(2024, 0, 2) }, }), ); assert.equal(view.viewer.captionLeft, ''); assert.equal(view.viewer.captionRight, '2024-01-02'); assert.equal(view.viewer.link, null); assert.equal(view.viewer.linkLabel, ''); });