mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(street-level): trim QA, tests, styles and docs
The browser gate is one pass over the remaining flows: rail strip and expand, coverage with fixtures, the ON/OFF pill, 360° and SINCE filters, KEY REJECTED, nearest photo with caption, × close, phone fit, keyless KEY REQUIRED on a second page and no page errors. Panel controls are pressed through the DOM; pick-point search, line/cone clicks, the framing-cancel, reload/share and status-route probes, the second viewport and pointer-settling retries are gone (unit tests cover them). Tests that pinned constants, copy, CSS or source text, or repeated another test, are removed or merged. CSS loses an unused variable and duplicate rules. Docs describe the slimmed feature; unused exports and the layer's dead setCoverageFilter/refreshCoverage surface are removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b5a78826f8
commit
ad58a0bdc4
23 files changed
+534
-1693
No files matched your search
@@ -318,8 +318,6 @@ export function createStreetLevelLayer({
|
||||
attachViewerHost(element) {
|
||||
parts.viewerHost.attach(element);
|
||||
},
|
||||
/** Imagery filter for coverage, cones and nearest-image lookups. */
|
||||
setCoverageFilter,
|
||||
openImage,
|
||||
/** Open the nearest image around a point (default: the view centre). */
|
||||
async openNearest(point) {
|
||||
@@ -366,7 +364,6 @@ export function createStreetLevelLayer({
|
||||
resizeViewer: () => parts.viewerHost.resize(),
|
||||
selectSequence: (sequenceId) => parts.sequences.select(sequenceId),
|
||||
clearSequence: () => parts.sequences.clearSelection(),
|
||||
refreshCoverage: () => parts.coverage.refresh(),
|
||||
};
|
||||
return layer;
|
||||
}
|
||||
@@ -143,18 +143,6 @@ function slowLookups() {
|
||||
return { lookups, source: fakeMapillarySource({ nearestImages }) };
|
||||
}
|
||||
|
||||
test('the layer is Mapillary under the codec keys it has always had', () => {
|
||||
const layer = createStreetLevelLayer({ source: fakeMapillarySource() });
|
||||
assert.equal(layer.id, 'street-level');
|
||||
assert.equal(layer.requiresKeyId, 'mapillary');
|
||||
assert.deepEqual(layer.getParams(), {
|
||||
mapillary: true,
|
||||
pano: 'all',
|
||||
sinceDays: 0,
|
||||
});
|
||||
assert.throws(() => createStreetLevelLayer({ source: {} }), /source/);
|
||||
});
|
||||
|
||||
test('the CC BY-SA credit shows while Mapillary is on and goes with it', async (t) => {
|
||||
const { layer, viewer } = await startLayer(t);
|
||||
const shown = () => viewer.credits.map((credit) => credit.html);
|
||||
@@ -284,7 +272,7 @@ test('a sequence error is withdrawn with the sequence, never the viewer’s own'
|
||||
assert.match(layer.getUIState().street.error, /Open the Street Level panel/);
|
||||
});
|
||||
|
||||
test('closing the photo stops the globe flying to it', async (t) => {
|
||||
test('closing the photo, switching the layer off or destroying it stops the framing flight', async (t) => {
|
||||
const { layer, viewer } = await startLayer(t, {
|
||||
photoViewer: posingViewer(),
|
||||
});
|
||||
@@ -292,35 +280,16 @@ test('closing the photo stops the globe flying to it', async (t) => {
|
||||
assert.equal(await layer.openImage('img1'), true);
|
||||
assert.equal(viewer.flights.started, 1, 'the photo is framed');
|
||||
layer.closeViewer();
|
||||
assert.equal(viewer.flights.cancelled, 1);
|
||||
});
|
||||
assert.equal(viewer.flights.cancelled, 1, 'closed');
|
||||
|
||||
test('switching the layer off or destroying it stops the framing flight', async (t) => {
|
||||
const { layer, viewer } = await startLayer(t, {
|
||||
photoViewer: posingViewer(),
|
||||
});
|
||||
layer.attachViewerHost({});
|
||||
await layer.openImage('img1');
|
||||
await layer.openImage('img2');
|
||||
layer.disable();
|
||||
assert.equal(viewer.flights.cancelled, 1, 'layer off');
|
||||
assert.equal(viewer.flights.cancelled, 2, 'layer off');
|
||||
|
||||
layer.enable(viewer);
|
||||
await layer.openImage('img2');
|
||||
await layer.openImage('img3');
|
||||
layer.destroy();
|
||||
assert.equal(viewer.flights.cancelled, 2, 'destroyed');
|
||||
});
|
||||
|
||||
test('closing the photo leaves a newer navigation flight alone', async (t) => {
|
||||
const { layer, viewer } = await startLayer(t, {
|
||||
photoViewer: posingViewer(),
|
||||
});
|
||||
layer.attachViewerHost({});
|
||||
await layer.openImage('img1');
|
||||
assert.equal(viewer.flights.started, 1, 'the photo is being framed');
|
||||
// A search result flies the globe elsewhere before the framing lands.
|
||||
viewer.camera.flyToBoundingSphere(null, {});
|
||||
layer.closeViewer();
|
||||
assert.equal(viewer.flights.cancelled, 0, 'the search flight keeps going');
|
||||
assert.equal(viewer.flights.cancelled, 3, 'destroyed');
|
||||
});
|
||||
|
||||
test('an older nearest lookup that answers late cannot replace a newer one', async (t) => {
|
||||
@@ -383,7 +352,13 @@ test('closing the viewer or switching the layer off retires a nearest lookup', a
|
||||
});
|
||||
|
||||
test('setParams takes "any date" (0 days) and the switch over the current values (share-link defaults)', async (t) => {
|
||||
assert.throws(() => createStreetLevelLayer({ source: {} }), /source/);
|
||||
const { layer } = await startLayer(t);
|
||||
assert.deepEqual(layer.getParams(), {
|
||||
mapillary: true,
|
||||
pano: 'all',
|
||||
sinceDays: 0,
|
||||
});
|
||||
layer.setParams({ pano: 'flat', sinceDays: 365 });
|
||||
assert.equal(layer.getParams().sinceDays, 365);
|
||||
layer.setParams({ sinceDays: 0 });
|
||||
|
||||
@@ -57,7 +57,7 @@ export function sequenceIdFromPick(pickId) {
|
||||
/** Hint above the street-zoom ceiling, where no coverage is drawn. */
|
||||
export const ZOOM_IN_HINT = 'Zoom in to see street-level coverage';
|
||||
/** Hint when no ground is in view within range (looking at the sky). */
|
||||
export const NO_GROUND_HINT =
|
||||
const NO_GROUND_HINT =
|
||||
'Point the camera at the ground for street-level coverage';
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PbfWriter } from 'pbf';
|
||||
const zigzag = (value) => (value << 1) ^ (value >> 31);
|
||||
|
||||
/** Tile-local [x, y] (0..extent) of a lon/lat inside tile (x, y, z). */
|
||||
export function lonLatToTileLocal([lon, lat], { x, y, z }, extent = 4096) {
|
||||
function lonLatToTileLocal([lon, lat], { x, y, z }, extent = 4096) {
|
||||
const n = 2 ** z;
|
||||
const rad = (lat * Math.PI) / 180;
|
||||
const tx = ((lon + 180) / 360) * n;
|
||||
|
||||
@@ -19,7 +19,7 @@ function linePart(points) {
|
||||
|
||||
/**
|
||||
* A minimal mly1_public-shaped tile: one sequence line, plus one point in the
|
||||
* low-zoom `overview` layer the layer never draws.
|
||||
* z14 `image` layer the decoder skips.
|
||||
*/
|
||||
function sampleTile({ geometry = null } = {}) {
|
||||
const writer = new PbfWriter();
|
||||
@@ -73,7 +73,7 @@ function sampleTile({ geometry = null } = {}) {
|
||||
3,
|
||||
(_, layer) => {
|
||||
layer.writeVarintField(15, 2);
|
||||
layer.writeStringField(1, 'overview');
|
||||
layer.writeStringField(1, 'image');
|
||||
layer.writeVarintField(5, 4096);
|
||||
layer.writeStringField(3, 'is_pano');
|
||||
layer.writeMessage(
|
||||
@@ -97,7 +97,7 @@ function sampleTile({ geometry = null } = {}) {
|
||||
return new Uint8Array(writer.finish());
|
||||
}
|
||||
|
||||
test('sequences decode into lon/lat records; overview points are ignored', () => {
|
||||
test('sequences decode into lon/lat records; image points are skipped', () => {
|
||||
const decoded = decodeCoverageTile(sampleTile(), { x: 0, y: 0, z: 1 });
|
||||
assert.equal(decoded.sequences.length, 1);
|
||||
const [sequence] = decoded.sequences;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { nearestImageId } from './nearest.js';
|
||||
import { COLORS, mapillaryImageUrl } from './policy.js';
|
||||
import { mapillaryImageUrl } from './policy.js';
|
||||
import { sequenceIdFromPick } from './coverage.js';
|
||||
import { thinImages } from './sequences.js';
|
||||
import { resolveFilter } from '../../filter.js';
|
||||
@@ -27,8 +27,7 @@ const at = (id, lon, lat) => ({
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
});
|
||||
|
||||
test('Mapillary draws in its green; image deep links match the mapillary.com share format', () => {
|
||||
assert.equal(COLORS.coverage, '#05cb63');
|
||||
test('image deep links match the mapillary.com share format', () => {
|
||||
assert.equal(
|
||||
mapillaryImageUrl(1814275685699406),
|
||||
'https://www.mapillary.com/app/?pKey=1814275685699406&focus=photo',
|
||||
|
||||
@@ -15,7 +15,7 @@ const SPRITE_ID = 'street-level:mapillary-cones';
|
||||
const SEQUENCE_CACHE_SIZE = 40;
|
||||
|
||||
/** Normalize a graph image record into the shape the cones use. */
|
||||
export function normalizeSequenceImage(record) {
|
||||
function normalizeSequenceImage(record) {
|
||||
const coordinates = record?.geometry?.coordinates;
|
||||
if (!Array.isArray(coordinates) || coordinates.length < 2) return null;
|
||||
return {
|
||||
|
||||
@@ -104,12 +104,6 @@ test('graph API errors carry the upstream message and status', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('the source exposes only imagery lookups', () => {
|
||||
const source = createMapillarySource({ token: 'MLY|1|abc' });
|
||||
for (const gone of ['queryFeatures', 'plan', 'geocode', 'spriteUrl'])
|
||||
assert.equal(source[gone], undefined, gone);
|
||||
});
|
||||
|
||||
test('a rejected key and a rate limit are told apart from other tile errors', async () => {
|
||||
const rejectedSource = createMapillarySource({
|
||||
token: 'MLY|1|abc',
|
||||
|
||||
@@ -41,13 +41,6 @@ test('normalizeBbox orders and clamps coordinates', () => {
|
||||
assert.equal(normalizeBbox([1, 2, Number.NaN, 3]), null);
|
||||
});
|
||||
|
||||
test('city-scale boxes span many z14 tiles', () => {
|
||||
const sacramento = [-121.56, 38.44, -121.36, 38.69];
|
||||
assert.equal(tilesForBbox(sacramento, 14).total, 160);
|
||||
const detroit = [-83.29, 42.25, -82.91, 42.45];
|
||||
assert.equal(tilesForBbox(detroit, 14).total, 234);
|
||||
});
|
||||
|
||||
test('tilesForBbox orders from the centre outwards and honours the cap', () => {
|
||||
const result = tilesForBbox([-121.56, 38.44, -121.36, 38.69], 14, {
|
||||
limit: 5,
|
||||
|
||||
@@ -154,7 +154,7 @@ export function cameraNadir(viewer) {
|
||||
* Horizon occluder depth below WGS84 (m): ground can lie below the ellipsoid
|
||||
* (NYC −22 m), and an occluder at 0 m would hide the cones around it.
|
||||
*/
|
||||
export const HORIZON_CULL_DEPTH_M = 1000;
|
||||
const HORIZON_CULL_DEPTH_M = 1000;
|
||||
const HORIZON_CULL_ELLIPSOID = new Cesium.Ellipsoid(
|
||||
Cesium.Ellipsoid.WGS84.radii.x - HORIZON_CULL_DEPTH_M,
|
||||
Cesium.Ellipsoid.WGS84.radii.y - HORIZON_CULL_DEPTH_M,
|
||||
|
||||
@@ -189,24 +189,6 @@ function deferred() {
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
test('switching the layer off and on during a cold first open keeps the shared viewer', async () => {
|
||||
const gate = deferred();
|
||||
const adapter = fakeAdapter({ gate });
|
||||
const { state, host } = harness(adapter);
|
||||
const first = host.open('a'); // the library is still loading
|
||||
host.unmount(); // layer off …
|
||||
const second = host.open('b'); // … on again, and a new click
|
||||
gate.resolve();
|
||||
await Promise.all([first, second]);
|
||||
assert.equal(adapter.calls.unmount, 1, 'only the layer going off');
|
||||
assert.equal(adapter.calls.listeners, 1);
|
||||
assert.equal(state.street.error, null);
|
||||
assert.equal(state.street.imageId, 'b');
|
||||
await host.open('c');
|
||||
assert.equal(state.street.error, null, 'later opens are not stuck');
|
||||
assert.equal(state.street.imageId, 'c');
|
||||
});
|
||||
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
test('every pose the viewer reports moves the marker; only the open frames the camera', async () => {
|
||||
@@ -393,15 +375,6 @@ test('a Mapillary viewer unmounted mid-download is never built; the next mount b
|
||||
assert.equal(viewers.live, 0, 'nothing left holding a WebGL context');
|
||||
});
|
||||
|
||||
test('the Mapillary viewer leaves resizing to the panel, so a hidden one never asks for z=NaN', async () => {
|
||||
const { gate, viewers } = fakeLibrary();
|
||||
gate.resolve();
|
||||
const adapter = createMapillaryViewer({ source: { token: 't' } });
|
||||
await adapter.mount({});
|
||||
assert.equal(viewers.instances[0].options.trackResize, false);
|
||||
adapter.unmount();
|
||||
});
|
||||
|
||||
test('closing the photo stops sequence playback in the hidden viewer', async () => {
|
||||
const { gate, viewers } = fakeLibrary();
|
||||
gate.resolve();
|
||||
@@ -415,18 +388,6 @@ test('closing the photo stops sequence playback in the hidden viewer', async ()
|
||||
assert.equal(viewers.live, 1, 'the viewer itself stays warm');
|
||||
});
|
||||
|
||||
test('closing the photo or switching the layer off stops its framing flight', async () => {
|
||||
const adapter = fakeAdapter();
|
||||
const { framing, host } = harness(adapter);
|
||||
await host.open('a');
|
||||
assert.equal(framing.started, 1, 'the open framed the photo');
|
||||
host.close();
|
||||
assert.equal(framing.cancelled, 1, 'closed: the globe stops flying to it');
|
||||
await host.open('b');
|
||||
host.unmount();
|
||||
assert.equal(framing.cancelled, 2, 'layer off: likewise');
|
||||
});
|
||||
|
||||
/** The real Mapillary adapter, mounted on a ready library, and its poses. */
|
||||
async function mountedMapillary() {
|
||||
const library = fakeLibrary();
|
||||
|
||||
@@ -13,10 +13,8 @@ const html = expandApplicationHtml(read('index.html'));
|
||||
const css = readStylesheet(path.join(ROOT, 'style.css'));
|
||||
const panelChrome = read('src/ui/panelChrome.js');
|
||||
const layoutController = read('src/ui/panelLayoutController.js');
|
||||
const panelCss = read('src/ui/styles/street-level.css');
|
||||
const controls = read('src/ui/streetLevelControls.js');
|
||||
|
||||
test('Street Level is an ordinary collapsible GEV panel that starts collapsed', () => {
|
||||
test('Street Level is a collapsible right-rail panel that starts collapsed', () => {
|
||||
assert.match(
|
||||
html,
|
||||
/<div id="street-level-panel" class="panel-collapsible collapsed" data-panel-id="street-level-panel">/,
|
||||
@@ -25,20 +23,6 @@ test('Street Level is an ordinary collapsible GEV panel that starts collapsed',
|
||||
html,
|
||||
/<button class="panel-collapse-btn" data-collapse-target="street-level-panel"/,
|
||||
);
|
||||
assert.match(html, /<span class="panel-title">STREET LEVEL<\/span>/);
|
||||
// Provider-neutral header: no vendor mark; the inner restores its scroll.
|
||||
assert.doesNotMatch(html, /sl-mark|mly-mark/);
|
||||
assert.match(
|
||||
html,
|
||||
/<div class="street-level-panel-inner" data-rail-scroller>/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/mapillary-dock|sl-minimized|sl-3d-btn|sl-photoreal-btn/,
|
||||
);
|
||||
});
|
||||
|
||||
test('the panel is registered with panel chrome, cockpit entry and the right rail', () => {
|
||||
assert.match(panelChrome, /\{ id: 'street-level-panel' \}/);
|
||||
assert.match(
|
||||
panelChrome,
|
||||
@@ -48,106 +32,24 @@ test('the panel is registered with panel chrome, cockpit entry and the right rai
|
||||
panelChrome,
|
||||
/const isRightRail = \[[\s\S]*'street-level-panel'/,
|
||||
);
|
||||
// The layout controller moves every rail panel in one loop; ours is listed.
|
||||
assert.match(
|
||||
layoutController,
|
||||
/for \(const panel of \[[\s\S]*?this\._streetLevelPanel,[\s\S]*?\]\) \{[\s\S]*?stack\.insertBefore\(panel, globalContextPanel\)/,
|
||||
);
|
||||
for (const rule of [
|
||||
'#right-context-rail > #street-level-panel',
|
||||
'#right-context-rail #street-level-panel.collapsed',
|
||||
])
|
||||
assert.ok(css.includes(rule), `layers.css names ${rule}`);
|
||||
assert.match(
|
||||
css,
|
||||
/#right-context-rail\.layout-focus\s*>\s*#street-level-panel:not\(\.collapsed\)/,
|
||||
);
|
||||
assert.ok(css.includes('#right-context-rail > #street-level-panel'));
|
||||
});
|
||||
|
||||
test('panel styles stay inside GEV conventions: no !important, no fixed panel', () => {
|
||||
assert.equal((panelCss.match(/!important/g) || []).length, 0);
|
||||
assert.doesNotMatch(panelCss, /position:\s*fixed/);
|
||||
});
|
||||
|
||||
test('the keyless state gates the controls rather than leaving dead buttons', () => {
|
||||
// The key requirement is documented (README, .env.example), not repeated in the panel.
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/sl-keyless|sl-query|sl-results|data-sl-suggestion/,
|
||||
);
|
||||
assert.match(html, /<fieldset id="sl-controls"/);
|
||||
// The gate holds the filters; the error line above it says how to add the key.
|
||||
const gate = html.slice(
|
||||
html.indexOf('<fieldset id="sl-controls"'),
|
||||
html.indexOf('</fieldset>', html.indexOf('<fieldset id="sl-controls"')),
|
||||
);
|
||||
assert.ok(
|
||||
html.indexOf('id="sl-error"') < html.indexOf('<fieldset id="sl-controls"'),
|
||||
);
|
||||
test('the key gate holds every filter, with the error line outside it', () => {
|
||||
const start = html.indexOf('<fieldset id="sl-controls"');
|
||||
assert.ok(start > 0);
|
||||
const gate = html.slice(start, html.indexOf('</fieldset>', start));
|
||||
assert.match(gate, /data-sl-pano="all"/);
|
||||
assert.match(gate, /id="sl-since"/);
|
||||
assert.match(
|
||||
html,
|
||||
/<input id="sl-since" class="sl-range" type="range" min="0" max="8" step="1"/,
|
||||
);
|
||||
assert.match(html, /<output id="sl-since-label"/);
|
||||
assert.doesNotMatch(html, /<select id="sl-since"|value="year:/);
|
||||
assert.match(html, /<ul id="sl-legend"/);
|
||||
assert.ok(html.indexOf('id="sl-error"') < start);
|
||||
assert.match(html, /id="sl-error"[^>]*role="alert"/);
|
||||
// The header pill is the layer's on/off switch, not just a readout.
|
||||
assert.match(
|
||||
html,
|
||||
/<button id="sl-status" class="sl-status" type="button" aria-pressed="false"/,
|
||||
);
|
||||
assert.match(
|
||||
controls,
|
||||
/this\.listen\(el\.status, 'click', \(\) => this\._toggleEnabled\(\)\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test('labels say what the buttons do', () => {
|
||||
for (const label of ['>EXPAND<', '>FIT<', '>FILL<', 'SINCE'])
|
||||
assert.ok(html.includes(label), label);
|
||||
// The header pill is the only on/off switch: no provider chips, no FOLLOW.
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/sl-provider-chips|sl-follow-btn|OPEN NEAREST PHOTO|STREET LEVEL OFF|sl-enable-btn|sl-look-btn|LOOK HERE|STREET COCKPIT|>FRAME<|ZOOM TO RESULTS|ASK IN PLAIN ENGLISH/,
|
||||
);
|
||||
});
|
||||
|
||||
test('on phones the viewer is sized from the rail band, not its aspect ratio', () => {
|
||||
assert.match(
|
||||
panelCss,
|
||||
/@media \(max-width: 720px\) \{\s*\.sl-viewer \{[^}]*height: clamp\(/,
|
||||
);
|
||||
// The full-screen viewer must not inherit the phone height.
|
||||
assert.match(
|
||||
panelCss,
|
||||
/\.sl-viewer-wrap:fullscreen \.sl-viewer \{[^}]*height: auto/,
|
||||
);
|
||||
});
|
||||
|
||||
test('the viewer comes first, with EXPAND in its toolbar', () => {
|
||||
const controlsBlock = html.slice(html.indexOf('<div class="sl-main">'));
|
||||
assert.ok(
|
||||
controlsBlock.indexOf('id="sl-viewer-wrap"') <
|
||||
controlsBlock.indexOf('class="sl-settings"'),
|
||||
'imagery sits above the settings, visible without scrolling',
|
||||
);
|
||||
assert.match(
|
||||
controlsBlock.slice(0, controlsBlock.indexOf('id="sl-viewer"')),
|
||||
/id="sl-viewer-expand"/,
|
||||
'EXPAND lives in the viewer toolbar',
|
||||
);
|
||||
});
|
||||
|
||||
test('radiogroups are one tab stop and their focus ring is not clipped (P3)', () => {
|
||||
const seg = /\.sl-seg \{([^}]*)\}/.exec(panelCss)[1];
|
||||
assert.doesNotMatch(seg, /overflow/);
|
||||
assert.match(
|
||||
panelCss,
|
||||
/\.sl-seg-btn:focus-visible \{[^}]*outline: 1px solid/,
|
||||
);
|
||||
test('each radiogroup is one tab stop, on its checked button', () => {
|
||||
for (const [group, checked] of [
|
||||
['data-sl-render', 'letterbox'],
|
||||
['data-sl-pano', 'all'],
|
||||
@@ -166,17 +68,3 @@ test('radiogroups are one tab stop and their focus ring is not clipped (P3)', ()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the Cyber theme frames the Street Level panel like its rail peers (P3)', () => {
|
||||
const cyber = read('src/ui/styles/cyber.css');
|
||||
const inner = (cyber.match(/\.recent-imagery-panel-inner,/g) || []).length;
|
||||
const panel = (cyber.match(/#recent-imagery-panel,/g) || []).length;
|
||||
assert.ok(inner > 0 && panel > 0);
|
||||
assert.equal(
|
||||
(cyber.match(/\.street-level-panel-inner,/g) || []).length,
|
||||
inner,
|
||||
);
|
||||
assert.equal((cyber.match(/#street-level-panel,/g) || []).length, panel);
|
||||
// A theme header rule must be able to outrank the panel's own.
|
||||
assert.doesNotMatch(panelCss, /#street-level-panel \.panel-header/);
|
||||
});
|
||||
@@ -1,15 +1,6 @@
|
||||
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,
|
||||
LEGEND_SWATCHES,
|
||||
isCollapsed,
|
||||
PARK,
|
||||
STRICT_ALLOWED_SKIPS,
|
||||
strictViolations,
|
||||
} from '../../scripts/qa-street-level.mjs';
|
||||
import { fixtureTile } from '../../scripts/qa-street-level.mjs';
|
||||
import {
|
||||
answerMapillaryRequest,
|
||||
describeCall,
|
||||
@@ -21,37 +12,6 @@ import {
|
||||
} 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 the legend the app layer draws', () => {
|
||||
// Any method the layer asks its source for is a no-op: only the legend matters.
|
||||
const source = new Proxy({}, { get: () => () => {} });
|
||||
const layer = createApplicationStreetLevel({
|
||||
sources: { mapillary: source },
|
||||
});
|
||||
assert.equal(layer.getUIState().legend.length, LEGEND_SWATCHES);
|
||||
});
|
||||
|
||||
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');
|
||||
@@ -72,52 +32,6 @@ test('fixture tiles give the hermetic gate something real to filter', async () =
|
||||
assert.equal(fixtureTile(8, 1, 1, now).length, 0, 'nor 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,
|
||||
);
|
||||
});
|
||||
|
||||
/* ── 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 () => {
|
||||
@@ -157,9 +71,6 @@ test('the photo sequence runs through the parked view, clipped to each tile it c
|
||||
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', () => {
|
||||
@@ -322,54 +233,3 @@ test('evidence never carries a Mapillary token', () => {
|
||||
'loaded with AIza…REDACTED inline',
|
||||
);
|
||||
});
|
||||
|
||||
test('--strict accepts only the documented skips', () => {
|
||||
assert.deepEqual([...STRICT_ALLOWED_SKIPS], ['fixtures only']);
|
||||
assert.deepEqual(
|
||||
strictViolations([
|
||||
{ reason: 'fixtures only', label: 'keyless page' },
|
||||
{ reason: 'no Mapillary key', label: 'the keyed steps' },
|
||||
]),
|
||||
[{ reason: 'no Mapillary key', label: 'the keyed steps' }],
|
||||
);
|
||||
});
|
||||
|
||||
test('the gate saves failure evidence and fails on render-loop errors', () => {
|
||||
const source = fs.readFileSync(
|
||||
new URL('../../scripts/qa-street-level.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(source, /saveFailureArtifacts\(/);
|
||||
assert.match(source, /hookRenderErrors\(/);
|
||||
assert.match(source, /readRenderErrors\(/);
|
||||
});
|
||||
|
||||
test('CI runs the gate 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\//);
|
||||
});
|
||||
@@ -435,25 +435,6 @@ test('under KEY REQUIRED the filters are gated and the error line says how to ad
|
||||
controls.destroy();
|
||||
}));
|
||||
|
||||
test('the legend rebuilds when a swatch changes, even at the same count', () =>
|
||||
withDom(async (dom) => {
|
||||
const { layer, controls } = stubPanel(dom, uiState());
|
||||
const legend = dom.root.querySelector('#sl-legend');
|
||||
const swatch = () => legend.children[0].children[0].style.background;
|
||||
assert.equal(swatch(), '#05cb63');
|
||||
layer.publish(
|
||||
uiState({
|
||||
legend: [
|
||||
{ key: 'mapillary', label: 'Mapillary', color: '#a66bff' },
|
||||
{ key: 'selected', label: 'Selected', color: '#00d4ff' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert.equal(legend.childElementCount, 2);
|
||||
assert.equal(swatch(), '#a66bff');
|
||||
controls.destroy();
|
||||
}));
|
||||
|
||||
test('the viewer is resized once when it opens, not on every render', () =>
|
||||
withDom(async (dom) => {
|
||||
const { layer, controls } = stubPanel(dom, uiState());
|
||||
|
||||
@@ -2,7 +2,6 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
presentStreetLevelPanel,
|
||||
SINCE_STOPS,
|
||||
sinceStopIndex,
|
||||
} from './streetLevelPresentation.js';
|
||||
function snapshot(overrides = {}) {
|
||||
@@ -51,14 +50,7 @@ function deepMerge(target, source) {
|
||||
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', () => {
|
||||
test('KEY REQUIRED disables every control and says how to add the key', () => {
|
||||
const view = presentStreetLevelPanel(
|
||||
snapshot({
|
||||
enabled: true,
|
||||
@@ -66,7 +58,9 @@ test('KEY REQUIRED says how to add the key instead of a raw error code', () => {
|
||||
coverage: { error: 'no_key' },
|
||||
}),
|
||||
);
|
||||
assert.equal(view.controlsDisabled, true);
|
||||
assert.equal(view.status.text, 'KEY REQUIRED');
|
||||
assert.equal(view.status.tone, 'warn');
|
||||
assert.match(view.error, /^Mapillary: Needs MAPILLARY_CLIENT_TOKEN/);
|
||||
});
|
||||
|
||||
@@ -149,18 +143,10 @@ test('errors from the viewer or the coverage web surface in one alert', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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],
|
||||
);
|
||||
test('a SINCE window from a link lands on the nearest slider stop', () => {
|
||||
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(400), 5);
|
||||
assert.equal(sinceStopIndex(-4), 0);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
The rail owns position and width (layers.css). */
|
||||
#street-level-panel {
|
||||
--sl-green: #05cb63;
|
||||
--sl-text: 0.62rem;
|
||||
--sl-text-small: 0.6rem;
|
||||
--sl-text: 0.6rem;
|
||||
}
|
||||
|
||||
#street-level-panel .panel-glow {
|
||||
@@ -44,30 +43,24 @@
|
||||
}
|
||||
|
||||
.sl-status {
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
font-size: var(--sl-text);
|
||||
letter-spacing: 0.1em;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--glass-border-hover);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button.sl-status {
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
letter-spacing: 0.1em;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
button.sl-status:hover,
|
||||
button.sl-status:focus-visible {
|
||||
.sl-status:hover,
|
||||
.sl-status:focus-visible {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
@@ -94,24 +87,25 @@ button.sl-status:focus-visible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sl-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sl-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sl-body,
|
||||
.sl-main,
|
||||
.sl-settings,
|
||||
.sl-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sl-body,
|
||||
.sl-settings {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sl-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sl-controls {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -123,13 +117,6 @@ button.sl-status:focus-visible {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sl-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Errors ──────────────────────────────────────────────────────────────── */
|
||||
.sl-error {
|
||||
font-family: var(--font-sans);
|
||||
@@ -156,7 +143,7 @@ button.sl-status:focus-visible {
|
||||
|
||||
.sl-seg-btn {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
font-size: var(--sl-text);
|
||||
letter-spacing: 0.08em;
|
||||
padding: 4px 9px;
|
||||
border: 0;
|
||||
@@ -211,7 +198,7 @@ button.sl-status:focus-visible {
|
||||
.sl-since-label {
|
||||
grid-column: 2;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
font-size: var(--sl-text);
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -230,7 +217,7 @@ button.sl-status:focus-visible {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
font-size: var(--sl-text);
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@@ -322,7 +309,7 @@ button.sl-status:focus-visible {
|
||||
|
||||
.sl-viewer-tools .scene-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: var(--sl-text-small);
|
||||
font-size: var(--sl-text);
|
||||
background: rgba(10, 10, 15, 0.75);
|
||||
flex: none;
|
||||
min-width: 0;
|
||||
@@ -372,7 +359,7 @@ button.sl-status:focus-visible {
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
font-size: var(--sl-text);
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
@@ -418,7 +405,7 @@ button.sl-status:focus-visible {
|
||||
|
||||
.sl-meta {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
font-size: var(--sl-text);
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user