fix(street-level): review round — safer docking, mount retry, provider fallthrough

- Only a user's own collapse docks a floating window; cockpit entry,
  restores and the Cyber accordion no longer dock it or delete its saved
  place, and the accordion leaves floating windows alone.
- The viewer host records an adapter as active only after its mount
  succeeds, so a library that failed to load once is retried; concurrent
  opens share one mount, and unmounting mid-mount leaves nothing active.
- openNearest tries every active provider; one provider's error (no key,
  offline) no longer hides the others' imagery.
- The SINCE readout shows its cut-off date as soon as the committed value
  matches the thumb, even while the slider keeps focus.
- A floating window that was only dragged is bounded to the viewport and
  scrolls instead of running off-screen.
- getStats summarises coverage (summarizeCoverage) instead of building a
  full UI snapshot every second; disable() uses credits.hideAll(); smaller
  cleanups in the viewer host and sequence snapshot.
- Unit tests: panel docking and header double press, viewer-host mount
  retry/sharing/cancel, coverage summary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-09-25 20:56:12 +01:00
1 parent 5877f24f0b
commit 6207e4144e
12 files changed
+425 -55

No files matched your search

@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createViewerHost } from './viewerHost.js';
function harness(adapter) {
const state = {
enabled: true,
notify() {},
providers: new Map(),
street: { host: {}, renderMode: 'letterbox', follow: false },
};
const def = { id: 'mapillary', name: 'Mapillary', label: 'MAPILLARY' };
state.providers.set('mapillary', { def, instance: { viewer: adapter } });
const parts = {
marker: { set() {}, clear() {} },
follow: { followCamera() {}, lookAtPosition() {} },
};
return { state, host: createViewerHost({ state, parts }) };
}
function fakeAdapter({ failMounts = 0 } = {}) {
const calls = { mount: 0, open: [], unmount: 0, listeners: 0 };
let emit = null;
return {
calls,
async mount() {
calls.mount++;
if (calls.mount <= failMounts) throw new Error('library failed to load');
},
async open(id) {
calls.open.push(id);
emit?.({
providerId: 'mapillary',
imageId: id,
position: { lon: 1, lat: 2 },
bearing: 90,
isPano: false,
externalUrl: `https://example.test/${id}`,
});
},
close() {},
unmount() {
calls.unmount++;
},
resize() {},
onPose(listener) {
calls.listeners++;
emit = listener;
return () => {
calls.listeners--;
emit = null;
};
},
};
}
test('a failed mount is retried on the next open instead of being cached', async () => {
const adapter = fakeAdapter({ failMounts: 1 });
const { state, host } = harness(adapter);
await host.open('mapillary', 'a');
assert.equal(state.street.error, 'library failed to load');
assert.equal(adapter.calls.listeners, 0, 'the pose listener was released');
await host.open('mapillary', 'b');
assert.equal(state.street.error, null);
assert.equal(adapter.calls.mount, 2);
assert.deepEqual(adapter.calls.open, ['b']);
assert.equal(state.street.imageId, 'b');
assert.equal(state.street.externalUrl, 'https://example.test/b');
});
test('concurrent opens share one mount and one pose listener', async () => {
const adapter = fakeAdapter();
const { host } = harness(adapter);
await Promise.all([host.open('mapillary', 'a'), host.open('mapillary', 'b')]);
assert.equal(adapter.calls.mount, 1);
assert.equal(adapter.calls.listeners, 1);
});
test('unmounting while a mount is in flight does not leave it active', async () => {
const adapter = fakeAdapter();
const { state, host } = harness(adapter);
const opening = host.open('mapillary', 'a');
host.unmount();
await opening;
assert.equal(host.activeProviderId(), null);
assert.equal(adapter.calls.listeners, 0);
assert.equal(adapter.calls.unmount, 1);
assert.equal(state.street.open, false);
});