mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
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:
1 parent
5877f24f0b
commit
6207e4144e
12 files changed
+425
-55
No files matched your search
@@ -430,6 +430,8 @@ export class PanelChrome {
|
||||
if (
|
||||
peer !== panelEl &&
|
||||
peer.matches('[data-panel-id]') &&
|
||||
// A floating window is not part of the rail's accordion.
|
||||
!peer.classList.contains('panel-floating') &&
|
||||
!peer.hidden
|
||||
) {
|
||||
this.setPanelCollapsed(peer.id, true, {
|
||||
@@ -557,7 +559,10 @@ export class PanelChrome {
|
||||
}
|
||||
}
|
||||
panelEl.classList.toggle('collapsed', nextCollapsed);
|
||||
this._panelPosition?.onPanelCollapsed?.(panelId, nextCollapsed);
|
||||
// Only a user's own collapse docks a floating window; cockpit entry,
|
||||
// restores and accordion peers must not discard where they put it.
|
||||
if (explicit && !restore)
|
||||
this._panelPosition?.onPanelCollapsed?.(panelId, nextCollapsed);
|
||||
if (
|
||||
nextCollapsed &&
|
||||
this.cockpitView?.active &&
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { PanelPositionControls } from './panelPositionControls.js';
|
||||
|
||||
/** Just enough DOM for PanelPositionControls' portable-panel paths. */
|
||||
function fixture() {
|
||||
const saved = Object.fromEntries(
|
||||
['document', 'window', 'localStorage', 'performance'].map((key) => [
|
||||
key,
|
||||
globalThis[key],
|
||||
]),
|
||||
);
|
||||
const store = new Map();
|
||||
const element = () => {
|
||||
const classes = new Set();
|
||||
const style = {
|
||||
removeProperty(name) {
|
||||
delete this[name];
|
||||
},
|
||||
getPropertyValue: () => '',
|
||||
};
|
||||
return Object.assign(new EventTarget(), {
|
||||
id: '',
|
||||
style,
|
||||
dataset: {},
|
||||
classList: {
|
||||
add: (...names) => names.forEach((name) => classes.add(name)),
|
||||
remove: (...names) => names.forEach((name) => classes.delete(name)),
|
||||
contains: (name) => classes.has(name),
|
||||
toggle: (name, on) => (on ? classes.add(name) : classes.delete(name)),
|
||||
},
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
closest: () => null,
|
||||
removeAttribute() {},
|
||||
appendChild() {},
|
||||
getBoundingClientRect: () => ({
|
||||
left: 100,
|
||||
top: 80,
|
||||
width: 360,
|
||||
height: 420,
|
||||
right: 460,
|
||||
bottom: 500,
|
||||
}),
|
||||
});
|
||||
};
|
||||
globalThis.document = Object.assign(new EventTarget(), {
|
||||
getElementById: () => null,
|
||||
querySelectorAll: () => [],
|
||||
});
|
||||
globalThis.window = Object.assign(new EventTarget(), {
|
||||
innerWidth: 1440,
|
||||
innerHeight: 900,
|
||||
});
|
||||
globalThis.localStorage = {
|
||||
getItem: (key) => (store.has(key) ? store.get(key) : null),
|
||||
setItem: (key, value) => store.set(key, String(value)),
|
||||
removeItem: (key) => store.delete(key),
|
||||
};
|
||||
const calls = { layout: 0, resized: [] };
|
||||
const owner = new PanelPositionControls({
|
||||
syncPanelCollapseButton() {},
|
||||
layoutRightPanels: () => calls.layout++,
|
||||
syncCctvPanelViewport() {},
|
||||
showToast() {},
|
||||
onPanelResized: (id) => calls.resized.push(id),
|
||||
});
|
||||
return {
|
||||
owner,
|
||||
element,
|
||||
store,
|
||||
calls,
|
||||
restore() {
|
||||
owner.destroy();
|
||||
Object.assign(globalThis, saved);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function floatingPanel(f, id, { dockOnCollapse = false } = {}) {
|
||||
const panel = f.element();
|
||||
const handle = f.element();
|
||||
panel.id = id;
|
||||
panel.classList.add('panel-floating', 'panel-draggable');
|
||||
Object.assign(panel.style, {
|
||||
left: '100px',
|
||||
top: '80px',
|
||||
width: '360px',
|
||||
height: '420px',
|
||||
});
|
||||
f.store.set(`godsEyeView.v8.panelPos.${id}`, '{"floating":true}');
|
||||
f.owner._portablePanels.set(id, {
|
||||
panel,
|
||||
min: { width: 320, height: 280 },
|
||||
dockOnCollapse,
|
||||
});
|
||||
f.owner._makePanelDraggable(id, panel, handle);
|
||||
return { panel, handle };
|
||||
}
|
||||
|
||||
/** A left-button header press; `timeStamp` is a read-only getter on Event. */
|
||||
function press(handle, { timeStamp, clientX = 200, clientY = 90 }) {
|
||||
const event = new Event('pointerdown', { cancelable: true });
|
||||
for (const [key, value] of Object.entries({
|
||||
button: 0,
|
||||
clientX,
|
||||
clientY,
|
||||
timeStamp,
|
||||
}))
|
||||
Object.defineProperty(event, key, { value });
|
||||
handle.dispatchEvent(event);
|
||||
}
|
||||
|
||||
test('dockPanel returns a floating window to its rail and forgets its place', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const { panel } = floatingPanel(f, 'street-level-panel');
|
||||
assert.equal(f.owner.dockPanel('street-level-panel'), true);
|
||||
assert.equal(panel.classList.contains('panel-floating'), false);
|
||||
assert.equal(panel.style.width, undefined);
|
||||
assert.equal(panel.style.height, undefined);
|
||||
assert.equal(
|
||||
f.store.has('godsEyeView.v8.panelPos.street-level-panel'),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(f.calls.resized, ['street-level-panel']);
|
||||
assert.equal(
|
||||
f.owner.dockPanel('street-level-panel'),
|
||||
false,
|
||||
'already docked',
|
||||
);
|
||||
assert.equal(f.owner.dockPanel('unknown'), false);
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('only a dockOnCollapse panel docks when collapsed, and only on collapse', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const street = floatingPanel(f, 'street-level-panel', {
|
||||
dockOnCollapse: true,
|
||||
});
|
||||
const cctv = floatingPanel(f, 'cctv-panel');
|
||||
f.owner.onPanelCollapsed('street-level-panel', false);
|
||||
assert.equal(
|
||||
street.panel.classList.contains('panel-floating'),
|
||||
true,
|
||||
'expanding keeps it',
|
||||
);
|
||||
f.owner.onPanelCollapsed('cctv-panel', true);
|
||||
assert.equal(
|
||||
cctv.panel.classList.contains('panel-floating'),
|
||||
true,
|
||||
'CCTV did not opt in',
|
||||
);
|
||||
f.owner.onPanelCollapsed('street-level-panel', true);
|
||||
assert.equal(street.panel.classList.contains('panel-floating'), false);
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('two quick header presses snap a floating panel back; slow or distant ones do not', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const { panel, handle } = floatingPanel(f, 'cctv-panel');
|
||||
press(handle, { timeStamp: 1000 });
|
||||
window.dispatchEvent(new Event('pointerup'));
|
||||
press(handle, { timeStamp: 1600 });
|
||||
window.dispatchEvent(new Event('pointerup'));
|
||||
assert.equal(
|
||||
panel.classList.contains('panel-floating'),
|
||||
true,
|
||||
'600 ms apart is two clicks',
|
||||
);
|
||||
press(handle, { timeStamp: 1700, clientX: 260 });
|
||||
window.dispatchEvent(new Event('pointerup'));
|
||||
assert.equal(
|
||||
panel.classList.contains('panel-floating'),
|
||||
true,
|
||||
'60 px apart is two clicks',
|
||||
);
|
||||
press(handle, { timeStamp: 1900, clientX: 262 });
|
||||
assert.equal(
|
||||
panel.classList.contains('panel-floating'),
|
||||
false,
|
||||
'a double press docks',
|
||||
);
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
@@ -341,11 +341,19 @@ export class StreetLevelControls {
|
||||
button.classList.toggle('is-active', active);
|
||||
button.setAttribute('aria-checked', String(active));
|
||||
}
|
||||
if (el.sinceRange && document.activeElement !== el.sinceRange) {
|
||||
if (el.sinceRange) {
|
||||
// Never move the thumb under the user's hand; once the committed value
|
||||
// matches the thumb, show the full readout with its cut-off date.
|
||||
const index = String(view.since.index);
|
||||
if (el.sinceRange.value !== index) el.sinceRange.value = index;
|
||||
if (el.sinceLabel) el.sinceLabel.textContent = view.since.label;
|
||||
el.sinceRange.setAttribute('aria-valuetext', view.since.label);
|
||||
if (
|
||||
document.activeElement !== el.sinceRange &&
|
||||
el.sinceRange.value !== index
|
||||
)
|
||||
el.sinceRange.value = index;
|
||||
if (el.sinceRange.value === index) {
|
||||
if (el.sinceLabel) el.sinceLabel.textContent = view.since.label;
|
||||
el.sinceRange.setAttribute('aria-valuetext', view.since.label);
|
||||
}
|
||||
}
|
||||
if (el.legend && el.legend.childElementCount !== view.legend.length) {
|
||||
el.legend.replaceChildren(
|
||||
|
||||
@@ -426,6 +426,14 @@ button.sl-status:focus-visible {
|
||||
}
|
||||
|
||||
/* ── Floating window ─────────────────────────────────────────────────────── */
|
||||
/* A window that was only dragged keeps its natural height; bound it to the
|
||||
viewport so nothing runs off-screen (the rail rule's max-height: 100% does
|
||||
not bind an auto-height parent). Id-qualified to outrank that rule. */
|
||||
#right-context-rail > #street-level-panel.panel-floating > .street-level-panel-inner {
|
||||
max-height: calc(100vh - 12px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* A resized window (PanelPositionControls writes an inline height) hands the
|
||||
spare height to the viewer, so the image never needs scrolling to. */
|
||||
.panel-floating[style*='height'] > .street-level-panel-inner {
|
||||
|
||||
Reference in new issue
Block a user