Files
gods-eye-view/src/layers/streetLevel/selection.test.mjs
T
German KhokhlovandClaude Fable 5.1 e214129e45 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]>
2026-10-08 19:17:45 +04:00

169 lines
5.2 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { createSelection } from './selection.js';
/** The selection installed on a stand-in viewer, with what its clicks reach. */
function harness({ selected = false } = {}) {
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();
const addListener = globalThis.document.addEventListener.bind(
globalThis.document,
);
const removeListener = globalThis.document.removeEventListener.bind(
globalThis.document,
);
globalThis.document.addEventListener = (type, listener, options) => {
if (type === 'keydown') keyListeners.add(listener);
addListener(type, listener, options);
};
globalThis.document.removeEventListener = (type, listener, options) => {
if (type === 'keydown') keyListeners.delete(listener);
removeListener(type, listener, options);
};
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(), {
// Keep Cesium's handler on the canvas; there is no real document here.
disableRootEvents: true,
onwheel: null,
});
const viewer = {
scene: { canvas, pick: () => scene.under && { id: scene.under } },
};
const state = {
viewer,
enabled: true,
providerOn: true,
clickHandler: null,
services: { picking: null, input: null },
sequence: {
get selectedId() {
return sequences.selected ? 'seq-1' : null;
},
},
};
const selection = createSelection({
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]);
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 = {
key,
target,
defaultPrevented,
preventDefault() {
this.defaultPrevented = true;
},
};
for (const listener of [...keyListeners]) listener(event);
return event;
},
restore() {
selection.uninstall();
globalThis.document = saved;
},
};
}
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: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.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();
}
});
/**
* An element as Esc sees it: `closest` matches tag names in a selector list;
* `isContentEditable` is inherited from an editing host, as in a browser.
*/
function element(tag, { isContentEditable = false } = {}) {
return {
tagName: tag.toUpperCase(),
isContentEditable,
closest(selector) {
const names = selector.split(',').map((part) => part.trim());
return names.includes(tag) ? this : null;
},
};
}
test('Esc on the globe clears the selected sequence', () => {
const h = harness({ selected: true });
try {
const event = h.key('Escape', element('canvas'));
assert.equal(h.sequences.cleared, 1);
assert.equal(event.defaultPrevented, true, 'and claims the key');
h.key('Escape', element('canvas'));
assert.equal(h.sequences.cleared, 1, 'nothing left to clear');
} finally {
h.restore();
}
});
test('an Esc something else already handled keeps the selection (M60)', () => {
const h = harness({ selected: true });
try {
h.key('Escape', element('canvas'), { defaultPrevented: true });
assert.equal(h.sequences.cleared, 0);
} finally {
h.restore();
}
});
test('Esc in a text field or rich-text editor keeps the selection (M61)', () => {
const h = harness({ selected: true });
try {
for (const tag of ['input', 'textarea', 'select']) {
const event = h.key('Escape', element(tag));
assert.equal(h.sequences.cleared, 0, `typing in a <${tag}>`);
assert.equal(event.defaultPrevented, false, `<${tag}> keeps its Esc`);
}
// A <div contenteditable> (or anything inside one) is a text field too.
h.key('Escape', element('div', { isContentEditable: true }));
assert.equal(h.sequences.cleared, 0, 'typing in a contenteditable');
} finally {
h.restore();
}
});