fix(street-level): one keydown listener on the photo root; format scope listed a test twice

Esc moved to the viewer's box (`.sl-viewer-wrap`) in the last commit, but
without one it was a second keydown listener on the root; Esc is now taken
by the root's own handler there. format-scope.json listed
panorama.test.mjs twice, which made the formatter refuse the scope; the
files it skipped are formatted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-14 15:25:58 +01:00
1 parent cf502429a3
commit 1a546bec72
14 files changed
+96 -30

No files matched your search

-1
View File
@@ -937,7 +937,6 @@
"src/testSupport/panoramaxFixtures.mjs", "src/testSupport/panoramaxFixtures.mjs",
"src/layers/streetLevel/photoViewer.test.mjs", "src/layers/streetLevel/photoViewer.test.mjs",
"src/layers/streetLevel/limiter.test.mjs", "src/layers/streetLevel/limiter.test.mjs",
"src/layers/streetLevel/panorama.test.mjs",
"src/layers/streetLevel/lineCoverage.test.mjs", "src/layers/streetLevel/lineCoverage.test.mjs",
"src/testSupport/kartaViewFakes.mjs", "src/testSupport/kartaViewFakes.mjs",
"src/testSupport/googleStreetViewFakes.mjs", "src/testSupport/googleStreetViewFakes.mjs",
+4 -1
View File
@@ -1276,7 +1276,10 @@ async function main() {
}, },
) )
.catch(() => null); .catch(() => null);
assert.ok(aimed, 'the line is pickable over the other’s pictures'); assert.ok(
aimed,
'the line is pickable over the other’s pictures',
);
at = await aimed.jsonValue(); at = await aimed.jsonValue();
} }
const parked = await camera(); const parked = await camera();
+5 -1
View File
@@ -70,7 +70,11 @@ test('a task already aborted when its turn comes is skipped', async () => {
const gate = createLimiter(1); const gate = createLimiter(1);
const first = held(log, 'a'); const first = held(log, 'a');
const running = gate.run(first.task); const running = gate.run(first.task);
const signal = { aborted: false, reason: new Error('gone'), addEventListener() {} }; const signal = {
aborted: false,
reason: new Error('gone'),
addEventListener() {},
};
const queued = gate.run(held(log, 'b').task, { signal }); const queued = gate.run(held(log, 'b').task, { signal });
signal.aborted = true; signal.aborted = true;
first.finish(); first.finish();
+15 -3
View File
@@ -217,17 +217,29 @@ test('overview points take the provider’s size, each its own', async (t) => {
test('a point budget skips lines past it, and a shorter one later may still fit', async (t) => { test('a point budget skips lines past it, and a shorter one later may still fit', async (t) => {
const { state, coverage } = setup(t, { const { state, coverage } = setup(t, {
tuning: { ...TUNING, maxTiles: 1, maxSequences: 10, maxPoints: 5, keepOrder: true }, tuning: {
...TUNING,
maxTiles: 1,
maxSequences: 10,
maxPoints: 5,
keepOrder: true,
},
decoded: (centre) => { decoded: (centre) => {
const line = (id, n) => ({ const line = (id, n) => ({
id, id,
parts: [ parts: [
Array.from({ length: n }, (_, i) => [centre.lon - 0.001 + i * 0.0001, centre.lat]), Array.from({ length: n }, (_, i) => [
centre.lon - 0.001 + i * 0.0001,
centre.lat,
]),
], ],
capturedAt: 0, capturedAt: 0,
isPano: false, isPano: false,
}); });
return { sequences: [line('three', 3), line('four', 4), line('two', 2)], overview: [] }; return {
sequences: [line('three', 3), line('four', 4), line('two', 2)],
overview: [],
};
}, },
}); });
coverage.refresh(); coverage.refresh();
+8 -4
View File
@@ -178,6 +178,8 @@ export function createPhotoViewer({
let documentOf = null; let documentOf = null;
/** A context loss's look direction, for the reopen of that photo. */ /** A context loss's look direction, for the reopen of that photo. */
let restoreView = null; let restoreView = null;
/** The panel's viewer box, where Esc is listened for (null: the root). */
let escapeTarget = null;
/** Width of the panorama texture on screen (0: the preview). */ /** Width of the panorama texture on screen (0: the preview). */
let panoWidth = 0; let panoWidth = 0;
/** The sharper panorama on its way, for which open (resizes come in bursts). */ /** The sharper panorama on its way, for which open (resizes come in bursts). */
@@ -450,6 +452,7 @@ export function createPhotoViewer({
} }
function onKeyDown(event) { function onKeyDown(event) {
if (!escapeTarget) onEscapeKey(event);
if ( if (
event.defaultPrevented || event.defaultPrevented ||
event.altKey || event.altKey ||
@@ -528,7 +531,10 @@ export function createPhotoViewer({
root.append(picture, canvas, prevButton, nextButton); root.append(picture, canvas, prevButton, nextButton);
host.append(root); host.append(root);
listen(root, 'keydown', onKeyDown); listen(root, 'keydown', onKeyDown);
listen(host.closest?.('.sl-viewer-wrap') ?? root, 'keydown', onEscapeKey); // The viewer's box in the panel; without one, the root's own handler
// takes Esc (one listener per element and type).
escapeTarget = host.closest?.('.sl-viewer-wrap') ?? null;
if (escapeTarget) listen(escapeTarget, 'keydown', onEscapeKey);
applyRenderMode(); applyRenderMode();
} }
@@ -564,9 +570,7 @@ export function createPhotoViewer({
if (size[0] <= panoWidth) return; if (size[0] <= panoWidth) return;
if (panoComing?.seq === seq && size[0] <= panoComing.width) return; if (panoComing?.seq === seq && size[0] <= panoComing.width) return;
const sharp = sources.panoSharp(target, size, { const sharp = sources.panoSharp(target, size, {
expanded: Boolean( expanded: Boolean(root?.closest?.('.sl-viewer-wrap-expanded')),
root?.closest?.('.sl-viewer-wrap-expanded'),
),
}); });
if (!sharp?.url) return; if (!sharp?.url) return;
const coming = { seq, width: size[0], controller: new AbortController() }; const coming = { seq, width: size[0], controller: new AbortController() };
+5 -4
View File
@@ -178,7 +178,9 @@ test('a sharp panorama still downloading when another photo opens is aborted', a
url.endsWith('-sharp') url.endsWith('-sharp')
? new Promise((_, reject) => { ? new Promise((_, reject) => {
signals.push(options.signal); signals.push(options.signal);
options.signal?.addEventListener('abort', () => reject(new Error('aborted'))); options.signal?.addEventListener('abort', () =>
reject(new Error('aborted')),
);
}) })
: Promise.resolve({ src: url }), : Promise.resolve({ src: url }),
createRenderer: () => ({ createRenderer: () => ({
@@ -278,9 +280,8 @@ test('docked, Esc anywhere in the viewer’s box (toolbar, caption) is offered t
}); });
test('a picture file or bitmap past any real photo’s size is refused', async (t) => { test('a picture file or bitmap past any real photo’s size is refused', async (t) => {
const { loadImage, MAX_IMAGE_BYTES, MAX_IMAGE_SIDE } = await import( const { loadImage, MAX_IMAGE_BYTES, MAX_IMAGE_SIDE } =
'./photoViewer.js' await import('./photoViewer.js');
);
const saved = { const saved = {
fetch: globalThis.fetch, fetch: globalThis.fetch,
createImageBitmap: globalThis.createImageBitmap, createImageBitmap: globalThis.createImageBitmap,
@@ -78,8 +78,7 @@ export function decodeCoverageTile(bytes, address) {
const props = feature.properties || {}; const props = feature.properties || {};
const parts = []; const parts = [];
let length = 0; let length = 0;
const tolerance = const tolerance = (SIMPLIFY_TOLERANCE_UNITS * sequences.extent) / 4096;
(SIMPLIFY_TOLERANCE_UNITS * sequences.extent) / 4096;
for (const line of feature.loadGeometry()) { for (const line of feature.loadGeometry()) {
if (line.length < 2) continue; if (line.length < 2) continue;
for (let k = 1; k < line.length; k++) for (let k = 1; k < line.length; k++)
@@ -191,8 +191,15 @@ test('a z15 tile’s pictures of one sequence become cones; other sequences’ a
}); });
test('lines are simplified within a few tile units, ends and corners kept', () => { test('lines are simplified within a few tile units, ends and corners kept', () => {
const zigzag = Array.from({ length: 1000 }, (_, i) => ({ x: i, y: i % 2 ? 1 : 0 })); const zigzag = Array.from({ length: 1000 }, (_, i) => ({
assert.deepEqual(simplifyLine(zigzag, 4), [zigzag[0], zigzag.at(-1)], 'a 1-unit wobble goes'); x: i,
y: i % 2 ? 1 : 0,
}));
assert.deepEqual(
simplifyLine(zigzag, 4),
[zigzag[0], zigzag.at(-1)],
'a 1-unit wobble goes',
);
const corner = [ const corner = [
{ x: 0, y: 0 }, { x: 0, y: 0 },
{ x: 50, y: 1 }, { x: 50, y: 1 },
@@ -221,6 +228,9 @@ test('a decoded GPS track keeps a fraction of its points', () => {
encodePanoramaxTile(tile, { sequences: [{ id: 'gps', parts: [track] }] }), encodePanoramaxTile(tile, { sequences: [{ id: 'gps', parts: [track] }] }),
tile, tile,
).sequences; ).sequences;
assert.ok(sequence.parts[0].length < 100, `${sequence.parts[0].length} points`); assert.ok(
sequence.parts[0].length < 100,
`${sequence.parts[0].length} points`,
);
assert.ok(sequence.parts[0].length > 5); assert.ok(sequence.parts[0].length > 5);
}); });
@@ -227,7 +227,12 @@ test('hostile item data: a malformed link escape, null coordinates, a huge produ
}); });
test('the instance holding a picture is named (its via host), never as its author', () => { test('the instance holding a picture is named (its via host), never as its author', () => {
const item = stacItem({ id: 'a', lon: 1, lat: 2, producer: '[email protected]' }); const item = stacItem({
id: 'a',
lon: 1,
lat: 2,
producer: '[email protected]',
});
item.links.push({ rel: 'via', href: 'https://panoramax.ign.fr' }); item.links.push({ rel: 'via', href: 'https://panoramax.ign.fr' });
assert.equal(normalizeItem(item).creator, null); assert.equal(normalizeItem(item).creator, null);
assert.equal(normalizeItem(item).via, 'panoramax.ign.fr'); assert.equal(normalizeItem(item).via, 'panoramax.ign.fr');
@@ -543,7 +543,12 @@ test('a broken neighbour z15 tile only thins the cones; an unreadable home tile
const box = tileBounds(home.tile.x, home.tile.y, 15); const box = tileBounds(home.tile.x, home.tile.y, 15);
const at = { lon: box.west + 0.0003, lat: box.north - 0.0002 }; const at = { lon: box.west + 0.0003, lat: box.north - 0.0002 };
const items = [0, 1, 2].map((i) => const items = [0, 1, 2].map((i) =>
stacItem({ id: `c-${i}`, sequenceId: 'c', lon: at.lon + i * 0.00005, lat: at.lat }), stacItem({
id: `c-${i}`,
sequenceId: 'c',
lon: at.lon + i * 0.00005,
lat: at.lat,
}),
); );
const tiles = pictureTiles(items); const tiles = pictureTiles(items);
const broken = new TextEncoder().encode('<html>oops</html>'); const broken = new TextEncoder().encode('<html>oops</html>');
@@ -835,7 +840,10 @@ test('a search hit outside the radius (a bounding box’s corner) is not the nea
}); });
test('a line clicked from low down still searches its own sequence within 50 m', async () => { test('a line clicked from low down still searches its own sequence within 50 m', async () => {
const items = [...sequence('other', 1, { offset: 2 }), ...sequence('clicked', 1, { offset: 30 })]; const items = [
...sequence('other', 1, { offset: 2 }),
...sequence('clicked', 1, { offset: 30 }),
];
const { instance, source } = setup({ items }); const { instance, source } = setup({ items });
instance.init({ instance.init({
camera: { positionCartographic: { height: 300 } }, camera: { positionCartographic: { height: 300 } },
@@ -885,7 +893,9 @@ test('a filter that hides every density dot says so', async (t) => {
grid: [{ lon: ORIGIN.lon, lat: ORIGIN.lat, pano: 0, flat: 5 }], grid: [{ lon: ORIGIN.lon, lat: ORIGIN.lat, pano: 0, flat: 5 }],
}); });
env.instance.activate(env.viewer); env.instance.activate(env.viewer);
assert.ok(await until(() => env.instance.coverageStats().hint === FILTERED_HINT)); assert.ok(
await until(() => env.instance.coverageStats().hint === FILTERED_HINT),
);
}); });
test('dots are drawn at their own sizes, and the busiest is the biggest', async (t) => { test('dots are drawn at their own sizes, and the busiest is the biggest', async (t) => {
@@ -338,7 +338,9 @@ test('a Retry-After is honoured up to five minutes', async () => {
headers: { 'retry-after': header }, headers: { 'retry-after': header },
})).fetchImpl, })).fetchImpl,
}); });
return source.getTile({ x: 0, y: 0, z: 0 }).catch((error) => error.retryAfterSec); return source
.getTile({ x: 0, y: 0, z: 0 })
.catch((error) => error.retryAfterSec);
}; };
assert.equal(await wait('3600'), 300); assert.equal(await wait('3600'), 300);
assert.equal(await wait('99999999999'), 300); assert.equal(await wait('99999999999'), 300);
@@ -179,10 +179,19 @@ test('a 360° picture shows its SD equirectangular image, then HD shrunk to 4,09
test('HD panoramas too big to decode, or no bigger than SD, are never fetched; nor while docked', () => { test('HD panoramas too big to decode, or no bigger than SD, are never fetched; nor while docked', () => {
const size = [4096, 2048]; const size = [4096, 2048];
const expanded = { expanded: true }; const expanded = { expanded: true };
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size, { expanded: false }), null); assert.equal(
PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size, { expanded: false }),
null,
);
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size), null); assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.pano, size), null);
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.bigPano, size, expanded), null); assert.equal(
assert.equal(PANORAMAX_SOURCES.panoSharp(PHOTOS.smallPano, size, expanded), null); PANORAMAX_SOURCES.panoSharp(PHOTOS.bigPano, size, expanded),
null,
);
assert.equal(
PANORAMAX_SOURCES.panoSharp(PHOTOS.smallPano, size, expanded),
null,
);
assert.deepEqual( assert.deepEqual(
PANORAMAX_SOURCES.panoSharp({ ...PHOTOS.pano, width: 3000 }, size, expanded) PANORAMAX_SOURCES.panoSharp({ ...PHOTOS.pano, width: 3000 }, size, expanded)
.resize, .resize,
+7 -2
View File
@@ -816,12 +816,17 @@ test('Panoramax fixtures: two sequences on one street, searches honour sequence
const boxed = ask( const boxed = ask(
`bbox=${at.lon - d},${at.lat - d},${at.lon + d},${at.lat + d}&limit=100`, `bbox=${at.lon - d},${at.lat - d},${at.lon + d},${at.lat + d}&limit=100`,
); );
assert.ok(boxed.some((p) => p.lon < at.lon) && boxed.some((p) => p.lon > at.lon)); assert.ok(
boxed.some((p) => p.lon < at.lon) && boxed.some((p) => p.lon > at.lon),
);
const placed = ask( const placed = ask(
`place_position=${at.lon},${at.lat}&place_distance=0-30&limit=50`, `place_position=${at.lon},${at.lat}&place_distance=0-30&limit=50`,
); );
assert.ok(placed.length > 0); assert.ok(placed.length > 0);
assert.ok(placed.every((p) => p.lon <= at.lon + 1e-7), 'none it is behind'); assert.ok(
placed.every((p) => p.lon <= at.lon + 1e-7),
'none it is behind',
);
const one = ask(`ids=${at.id}&limit=1`); const one = ask(`ids=${at.id}&limit=1`);
assert.equal(one[0].id, at.id); assert.equal(one[0].id, at.id);
// Its pictures, and only those, are served; HD downloads are counted. // Its pictures, and only those, are served; HD downloads are counted.
+4 -1
View File
@@ -555,6 +555,9 @@ test('the caption names an image’s own licence after its author, and leaves it
caption({ creator: 'Fixture Mapper', license: 'CC BY-SA 4.0' }), caption({ creator: 'Fixture Mapper', license: 'CC BY-SA 4.0' }),
'Image by Fixture Mapper · CC BY-SA 4.0', 'Image by Fixture Mapper · CC BY-SA 4.0',
); );
assert.equal(caption({ license: 'Licence Ouverte 2.0' }), 'Licence Ouverte 2.0'); assert.equal(
caption({ license: 'Licence Ouverte 2.0' }),
'Licence Ouverte 2.0',
);
assert.equal(caption({ creator: 'Someone' }), 'Image by Someone'); assert.equal(caption({ creator: 'Someone' }), 'Image by Someone');
}); });