fix: retain shell feedback bindings and stop disposed panel work

This commit is contained in:
Sameh Khamis committed 2026-09-13 04:49:02 -07:00
1 parent 03ad8142e6
commit 945a809994
5 files changed
+63 -29

No files matched your search

+36 -2
View File
@@ -33,7 +33,10 @@ try {
const ui = window.__godsEyeView.styleManager; const ui = window.__godsEyeView.styleManager;
const counts = {}; const counts = {};
const watchObserver = (name) => { const watchObserver = (name) => {
const owner = name === '_draggableResizeObserver' ? ui._panelPosition : ui._panelLayout; const owner =
name === '_draggableResizeObserver'
? ui._panelPosition
: ui._panelLayout;
const observer = owner[name]; const observer = owner[name];
if (!observer) return false; if (!observer) return false;
const disconnect = observer.disconnect.bind(observer); const disconnect = observer.disconnect.bind(observer);
@@ -110,6 +113,12 @@ try {
if (type === 'resize' && callback === resizeHandler) counts.resize++; if (type === 'resize' && callback === resizeHandler) counts.resize++;
return removeEventListener.call(this, type, callback, options); return removeEventListener.call(this, type, callback, options);
}; };
const { setSplitFlapText } = await import('/src/splitFlap.js');
const feedbackLabel = document.getElementById('global-loading-label');
setSplitFlapText(feedbackLabel, 'CHECKING LIVE DATA');
setSplitFlapText(feedbackLabel, 'LOAD COMPLETE');
const permanentText =
feedbackLabel.querySelector('.gev-flap-text')?.firstChild;
try { try {
const disposal = ui.dispose(); const disposal = ui.dispose();
const focusBefore = document.activeElement; const focusBefore = document.activeElement;
@@ -167,7 +176,11 @@ try {
counts[field] === 1 && ui._contextControls[field] === null, counts[field] === 1 && ui._contextControls[field] === null,
), ),
observersReleased: observed.every( observersReleased: observed.every(
(name) => counts[name] === 1 && ui[name] === null, (name) =>
counts[name] === 1 &&
(name === '_draggableResizeObserver'
? ui._panelPosition
: ui._panelLayout)[name] === null,
), ),
subscriptionReleased: subscriptionReleased:
counts.cctv === 1 && ui._cctvControls._cctvUnsubscribe === null, counts.cctv === 1 && ui._cctvControls._cctvUnsubscribe === null,
@@ -176,6 +189,19 @@ try {
ui._radioControls.destroyed && ui._radioControls.destroyed &&
ui._locationControls.destroyed && ui._locationControls.destroyed &&
ui._cctvControls.destroyed, ui._cctvControls.destroyed,
feedbackTextPreserved:
permanentText?.nodeType === Node.TEXT_NODE &&
feedbackLabel.querySelector('.gev-flap-text')?.firstChild ===
permanentText &&
permanentText.data === 'LOAD COMPLETE' &&
!feedbackLabel.classList.contains('gev-flap-active') &&
feedbackLabel.querySelector('.gev-flap-cells')?.childNodes.length ===
0,
shellWorkReleased:
ui._lifetime.destroyed &&
ui._lifetime.frames.size === 0 &&
ui._lifetime.timers.size === 0 &&
ui._lifetime.removers.size === 0,
idempotent: once === JSON.stringify(counts), idempotent: once === JSON.stringify(counts),
}; };
} finally { } finally {
@@ -215,6 +241,14 @@ try {
'Cockpit cleanup releases listeners and returns all Display groups home', 'Cockpit cleanup releases listeners and returns all Display groups home',
result.cockpitReleased, result.cockpitReleased,
); );
check(
'feedback teardown preserves the permanent accessible text node',
result.feedbackTextPreserved,
);
check(
'shell teardown cancels deferred work and releases listeners',
result.shellWorkReleased,
);
check('repeated UI disposal is inert', result.idempotent); check('repeated UI disposal is inert', result.idempotent);
check( check(
'disposal and subsequent resize produce no uncaught browser errors', 'disposal and subsequent resize produce no uncaught browser errors',
+1 -23
View File
@@ -1,3 +1,4 @@
import { setSplitFlapText } from '../splitFlap.js';
import { UiLifetime } from './uiLifetime.js'; import { UiLifetime } from './uiLifetime.js';
import { RecordingControls } from './recordingControls.js'; import { RecordingControls } from './recordingControls.js';
import { readShellElements } from './shellElements.js'; import { readShellElements } from './shellElements.js';
@@ -86,8 +87,6 @@ import {
stampInitialShareGesture, stampInitialShareGesture,
} from '../navigationPolicy.js'; } from '../navigationPolicy.js';
/** Versioned localStorage namespace prefix to invalidate stale panel layouts. */
const SHARE_PANEL_STATE_SPECS = Object.freeze([ const SHARE_PANEL_STATE_SPECS = Object.freeze([
{ id: 'control-panel', pinnable: true }, { id: 'control-panel', pinnable: true },
{ id: 'location-bar', pinnable: true }, { id: 'location-bar', pinnable: true },
@@ -108,28 +107,7 @@ const COCKPIT_ENTRY_COLLAPSE_PANEL_IDS = Object.freeze([
'global-context-panel', 'global-context-panel',
'radio-panel', 'radio-panel',
]); ]);
/**
* Position keys are versioned separately from collapsed-state keys so layout
* default changes (e.g. right-rail origin) can reset positions without also
* resetting every panel's open/closed preference.
*/
const DETECTION_ALLOCATION_STORAGE_KEY = 'gev:detection-allocation:v1'; const DETECTION_ALLOCATION_STORAGE_KEY = 'gev:detection-allocation:v1';
/** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */
/**
* Fixed UI regions that can occupy the left accordion's vertical lane.
* Rectangles are filtered at runtime for visibility and horizontal overlap,
* so right-side/center controls do not reduce the lane unless they actually
* intersect it at the current viewport size.
*/
/**
* Fixed UI regions that can occupy the right control lane. Runtime rectangle
* filtering keeps the rail clear of whichever HUD variant is currently
* visible without tying the layout to one screen height.
*/
/** Display labels shown in the mini-status readout for each active style. */ /** Display labels shown in the mini-status readout for each active style. */
const STYLE_STATUS_LABELS = { const STYLE_STATUS_LABELS = {
normal: 'NORMAL', normal: 'NORMAL',
+11
View File
@@ -1,6 +1,12 @@
/** Own rail measurement, layout scheduling and dock tray observation. */ /** Own rail measurement, layout scheduling and dock tray observation. */
import { layoutLeftPanelRail, layoutRightPanelRail } from './panelRails.js'; import { layoutLeftPanelRail, layoutRightPanelRail } from './panelRails.js';
const COCKPIT_LAYOUT_SETTLE_MS = 240; const COCKPIT_LAYOUT_SETTLE_MS = 240;
/**
* Fixed UI regions that can occupy the left accordion's vertical lane.
* Rectangles are filtered at runtime for visibility and horizontal overlap,
* so right-side/center controls do not reduce the lane unless they actually
* intersect it at the current viewport size.
*/
const LEFT_STACK_OBSTACLE_SELECTOR = [ const LEFT_STACK_OBSTACLE_SELECTOR = [
'#cockpit-hud .cockpit-topline', '#cockpit-hud .cockpit-topline',
'#cockpit-hud .cockpit-topline > div', '#cockpit-hud .cockpit-topline > div',
@@ -26,6 +32,11 @@ const LEFT_STACK_OBSTACLE_SELECTOR = [
'#pp-toggles', '#pp-toggles',
'#param-slider-panel', '#param-slider-panel',
].join(', '); ].join(', ');
/**
* Fixed UI regions that can occupy the right control lane. Runtime rectangle
* filtering keeps the rail clear of whichever HUD variant is currently
* visible without tying the layout to one screen height.
*/
const RIGHT_STACK_OBSTACLE_SELECTOR = [ const RIGHT_STACK_OBSTACLE_SELECTOR = [
'#cockpit-hud .cockpit-topline', '#cockpit-hud .cockpit-topline',
'#cockpit-hud .cockpit-topline > div', '#cockpit-hud .cockpit-topline > div',
+10 -4
View File
@@ -1,6 +1,13 @@
/** Own panel position preferences, viewport clamping and drag listeners. */ /** Own panel position preferences, viewport clamping and drag listeners. */
/** Versioned localStorage namespace prefix to invalidate stale panel layouts. */
const PANEL_LAYOUT_STORAGE_VERSION = 'v6'; const PANEL_LAYOUT_STORAGE_VERSION = 'v6';
/**
* Position keys are versioned separately from collapsed-state keys so layout
* default changes (e.g. right-rail origin) can reset positions without also
* resetting every panel's open/closed preference.
*/
const PANEL_POSITION_STORAGE_VERSION = 'v8'; const PANEL_POSITION_STORAGE_VERSION = 'v8';
/** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */
const PANEL_Z_BASE = 100; const PANEL_Z_BASE = 100;
const PANEL_Z_MAX = 139; const PANEL_Z_MAX = 139;
export class PanelPositionControls { export class PanelPositionControls {
@@ -22,6 +29,7 @@ export class PanelPositionControls {
this.destroyed = false; this.destroyed = false;
} }
listen(target, type, callback) { listen(target, type, callback) {
if (this.destroyed || !target) return;
const listener = (event) => { const listener = (event) => {
if (!this.destroyed) callback(event); if (!this.destroyed) callback(event);
}; };
@@ -29,6 +37,7 @@ export class PanelPositionControls {
this.removers.push(() => target.removeEventListener(type, listener)); this.removers.push(() => target.removeEventListener(type, listener));
} }
_reclampDraggablePanels() { _reclampDraggablePanels() {
if (this.destroyed) return;
const el = this._ppToggles; const el = this._ppToggles;
if (!el || !el.style.top || el.style.top === 'auto') return; if (!el || !el.style.top || el.style.top === 'auto') return;
const top = parseInt(el.style.top, 10); const top = parseInt(el.style.top, 10);
@@ -56,6 +65,7 @@ export class PanelPositionControls {
} }
_initPanelDrag() { _initPanelDrag() {
if (this.destroyed) return;
const dragSpecs = [ const dragSpecs = [
{ {
id: 'pp-toggles', id: 'pp-toggles',
@@ -258,10 +268,6 @@ export class PanelPositionControls {
}; };
const onUp = () => { const onUp = () => {
cancel(); cancel();
panelEl.classList.remove('panel-dragging');
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
window.removeEventListener('pointercancel', onUp);
if (panelId === 'pp-toggles') { if (panelId === 'pp-toggles') {
this._pinPanelToRight(panelEl); this._pinPanelToRight(panelEl);
} }
+5
View File
@@ -201,6 +201,11 @@ test('panel disposal cancels a live drag without saving or accepting later point
window.dispatchEvent(new Event('pointerup')); window.dispatchEvent(new Event('pointerup'));
assert.deepEqual(panel.style, before); assert.deepEqual(panel.style, before);
assert.equal(panel.classList.contains('panel-dragging'), false); assert.equal(panel.classList.contains('panel-dragging'), false);
owner._makePanelDraggable('sample', panel, handle);
owner._initPanelDrag();
owner._ppToggles.style.top = '10000px';
owner._reclampDraggablePanels();
assert.equal(owner._ppToggles.style.top, '10000px');
assert.equal(owner.removers.length, 0); assert.equal(owner.removers.length, 0);
assert.deepEqual(f.writes, []); assert.deepEqual(f.writes, []);
} finally { } finally {