refactor(street-level): drop hover, legend and byte-budget cache; flatten the provider directory

- No pointer cursor over coverage lines (no other layer has one); a click
  test replaces the hover tests.
- No legend: one source, one colour.
- The on-globe credit lives in index.js; credits.js is gone.
- Billboard glyphs are SVG data URLs with the same geometry, so tests need
  no canvas stub.
- The tile cache keeps the 120 most recently used tiles, same 24 h TTL,
  instead of a byte budget; listTileLayers was test-only.
- providers/mapillary/ is flattened into the layer and its policy merged
  into one policy.js; the source's method list is a literal in the catalog.
- One fakeCesiumViewer for the layer tests.
- The fixture gate keeps the steps the unit tests do not cover: coverage and
  credit, a rejected key, line -> cone -> photo, close, a keyless page.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Fable 5.1 committed 2026-10-08 19:17:45 +04:00
1 parent 34d43c7e3f
commit e214129e45
41 files changed
+335 -909

No files matched your search

+28 -103
View File
@@ -3,17 +3,9 @@ import test from 'node:test';
import * as Cesium from 'cesium';
import { createSelection } from './selection.js';
/**
* The selection installed on a stand-in viewer. Animation frames queue until
* `frame()` runs them.
*/
/** The selection installed on a stand-in viewer, with what its clicks reach. */
function harness({ selected = false } = {}) {
const saved = {
document: globalThis.document,
requestAnimationFrame: globalThis.requestAnimationFrame,
};
const frames = [];
globalThis.requestAnimationFrame = (task) => frames.push(task);
const saved = globalThis.document;
globalThis.document = new EventTarget();
// The document's keydown listeners, so a test can hand them any target.
const keyListeners = new Set();
@@ -31,26 +23,17 @@ function harness({ selected = false } = {}) {
if (type === 'keydown') keyListeners.delete(listener);
removeListener(type, listener, options);
};
const sequences = { selected, cleared: 0 };
const picks = [];
/** What the pointer is over: a line this layer owns, or nothing. */
const sequences = { selected, cleared: 0, selects: [] };
const opened = [];
/** What the pointer is over: a pick id, or nothing. */
const scene = { under: null };
const canvas = Object.assign(new EventTarget(), {
style: {},
// Keep Cesium's handler on the canvas; there is no real document here.
disableRootEvents: true,
onwheel: null,
});
const camera = { moveEnd: new Cesium.Event() };
const viewer = {
camera,
scene: {
canvas,
pick(position) {
picks.push(position);
return scene.under && { id: scene.under };
},
},
scene: { canvas, pick: () => scene.under && { id: scene.under } },
};
const state = {
viewer,
@@ -68,20 +51,24 @@ function harness({ selected = false } = {}) {
state,
parts: {
sequences: {
select: (id) => sequences.selects.push(id),
clearSelection() {
sequences.cleared++;
sequences.selected = false;
},
},
openImage: (id) => opened.push(id),
},
});
selection.install(viewer);
const action = (type) =>
state.clickHandler.getInputAction(Cesium.ScreenSpaceEventType[type]);
const onMove = action('MOUSE_MOVE');
return {
state,
sequences,
opened,
scene,
click: () => action('LEFT_CLICK')({ position: { x: 10, y: 10 } }),
/** A keydown reaching the document from `target`; the event as handled. */
key(key, target, { defaultPrevented = false } = {}) {
const event = {
@@ -95,94 +82,32 @@ function harness({ selected = false } = {}) {
for (const listener of [...keyListeners]) listener(event);
return event;
},
scene,
canvas,
picks,
move: (x) => onMove({ endPosition: { x, y: 10 } }),
camera,
/** A pointer event on the canvas with `buttons` held. */
buttons(type, buttons) {
const event = new Event(type);
Object.defineProperty(event, 'buttons', { value: buttons });
canvas.dispatchEvent(event);
},
frame() {
for (const task of frames.splice(0)) task();
},
restore() {
selection.uninstall();
Object.assign(globalThis, saved);
globalThis.document = saved;
},
};
}
test('hover picks once a frame, at the latest position, and shows a pointer over a line', () => {
test('a click on a line selects its sequence, on a cone opens its image, elsewhere does nothing', () => {
const h = harness();
try {
h.scene.under = 'mly:seq:1';
for (let x = 0; x < 5; x++) h.move(x);
h.frame();
assert.deepEqual(
h.picks.map(({ x }) => x),
[4],
'one pick for the burst',
);
assert.equal(h.canvas.style.cursor, 'pointer');
h.scene.under = null;
h.move(6);
h.frame();
assert.equal(h.canvas.style.cursor, '', 'off the line');
h.frame();
assert.equal(h.picks.length, 2, 'a still pointer costs nothing');
} finally {
h.restore();
}
});
test('a drag with any button picks nothing, and a release with a modifier held does not stick', () => {
const h = harness();
try {
for (const held of [1, 2, 4]) {
h.buttons('pointerdown', held);
h.move(held);
h.frame();
h.scene.under = 'mly:seq:abc';
h.click();
assert.deepEqual(h.sequences.selects, ['abc']);
h.scene.under = 'mly:img:42';
h.click();
assert.deepEqual(h.opened, ['42']);
for (const under of [null, 'cctv:7', 'sl:pos']) {
h.scene.under = under;
h.click();
}
assert.equal(h.picks.length, 0, 'left, right and middle drags');
// Released with Shift held: Cesium's plain LEFT_UP never fires.
h.buttons('pointerup', 0);
h.move(5);
h.frame();
assert.equal(h.picks.length, 1, 'hover picks again');
} finally {
h.restore();
}
});
test('when the camera rests, a still pointer is picked again so the cursor cannot stick', () => {
const h = harness();
try {
h.scene.under = 'mly:seq:1';
h.move(5);
h.frame();
assert.equal(h.canvas.style.cursor, 'pointer');
// The camera moves the line out from under a still pointer.
h.scene.under = null;
h.camera.moveEnd.raiseEvent();
h.frame();
assert.equal(h.picks.length, 2);
assert.equal(h.canvas.style.cursor, '');
} finally {
h.restore();
}
});
test('a hover frame queued before the layer went off does not pick', () => {
const h = harness();
try {
h.move(2);
h.state.enabled = false;
h.frame();
assert.equal(h.picks.length, 0, 'the layer went off');
assert.deepEqual([h.sequences.selects, h.opened], [['abc'], ['42']]);
// Mapillary switched off under a layer that is on: lines are not drawn.
h.state.providerOn = false;
h.scene.under = 'mly:seq:abc';
h.click();
assert.deepEqual(h.sequences.selects, ['abc']);
} finally {
h.restore();
}