mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
fix: restore independent UI observer and subscription teardown
This commit is contained in:
1 parent
b119aef0ad
commit
a5af1cbfa3
5 files changed
+143
-1
No files matched your search
@@ -13,6 +13,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Restore UI observer, resize-listener and CCTV subscription cleanup after Location extraction.
|
||||
|
||||
- Extract Radio controls and tuner presentation with explicit actions and complete listener/subscription cleanup.
|
||||
|
||||
- Extract Location controls and cancellable search presentation; preserve navigation handoff and prevent delayed POI expansion after closing the row.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
## UI disposal
|
||||
|
||||
UI disposal releases the CCTV subscription, command-dock observer, legacy drag
|
||||
resize observer and window resize listener. These remain independent of the
|
||||
Location component and are included in whole-UI browser teardown acceptance.
|
||||
|
||||
## Radio controls
|
||||
|
||||
Radio panel, compact controls and tuner presentation have a dedicated owner.
|
||||
|
||||
@@ -172,5 +172,6 @@
|
||||
"src/ui/radioBindings.js",
|
||||
"src/ui/radioPresentation.js",
|
||||
"src/ui/radioTunerModel.js",
|
||||
"src/ui/radioControls.test.mjs"
|
||||
"src/ui/radioControls.test.mjs",
|
||||
"scripts/qa-ui-disposal.mjs"
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env node
|
||||
/** Exercise the real UI disposal path, including unrelated resource owners. */
|
||||
import puppeteer from 'puppeteer';
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
let failures = 0;
|
||||
function check(name, ok) {
|
||||
console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}`);
|
||||
if (!ok) failures++;
|
||||
}
|
||||
try {
|
||||
await page.goto(
|
||||
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.styleManager?._dataManager &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
const result = await page.evaluate(async () => {
|
||||
const ui = window.__godsEyeView.styleManager;
|
||||
const counts = {};
|
||||
const watchObserver = (name) => {
|
||||
const observer = ui[name];
|
||||
if (!observer) return false;
|
||||
const disconnect = observer.disconnect.bind(observer);
|
||||
counts[name] = 0;
|
||||
observer.disconnect = () => {
|
||||
counts[name]++;
|
||||
return disconnect();
|
||||
};
|
||||
return true;
|
||||
};
|
||||
const observed = [
|
||||
'_commandDockTrayObserver',
|
||||
'_draggableResizeObserver',
|
||||
'_leftStackResizeObserver',
|
||||
'_rightStackResizeObserver',
|
||||
'_leftStackMutationObserver',
|
||||
'_rightStackMutationObserver',
|
||||
].filter(watchObserver);
|
||||
const cctvUnsubscribe = ui._cctvUnsubscribe;
|
||||
counts.cctv = 0;
|
||||
if (cctvUnsubscribe)
|
||||
ui._cctvUnsubscribe = () => {
|
||||
counts.cctv++;
|
||||
return cctvUnsubscribe();
|
||||
};
|
||||
const resizeHandler = ui._windowResizeHandler;
|
||||
const removeEventListener = window.removeEventListener;
|
||||
counts.resize = 0;
|
||||
window.removeEventListener = function (type, callback, options) {
|
||||
if (type === 'resize' && callback === resizeHandler) counts.resize++;
|
||||
return removeEventListener.call(this, type, callback, options);
|
||||
};
|
||||
try {
|
||||
await ui.dispose();
|
||||
const once = JSON.stringify(counts);
|
||||
await ui.dispose();
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await new Promise((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||
);
|
||||
return {
|
||||
connected: Boolean(
|
||||
cctvUnsubscribe &&
|
||||
resizeHandler &&
|
||||
observed.includes('_commandDockTrayObserver'),
|
||||
),
|
||||
observersReleased: observed.every(
|
||||
(name) => counts[name] === 1 && ui[name] === null,
|
||||
),
|
||||
subscriptionReleased: counts.cctv === 1 && ui._cctvUnsubscribe === null,
|
||||
resizeReleased: counts.resize === 1 && ui._windowResizeHandler === null,
|
||||
controlsReleased:
|
||||
ui._radioControls.destroyed && ui._locationControls.destroyed,
|
||||
idempotent: once === JSON.stringify(counts),
|
||||
};
|
||||
} finally {
|
||||
window.removeEventListener = removeEventListener;
|
||||
}
|
||||
});
|
||||
check(
|
||||
'real UI has the expected live resources before disposal',
|
||||
result.connected,
|
||||
);
|
||||
check(
|
||||
'UI disposal disconnects each active panel observer once',
|
||||
result.observersReleased,
|
||||
);
|
||||
check(
|
||||
'UI disposal releases the CCTV state subscription',
|
||||
result.subscriptionReleased,
|
||||
);
|
||||
check(
|
||||
'UI disposal removes the registered window resize handler',
|
||||
result.resizeReleased,
|
||||
);
|
||||
check(
|
||||
'Radio and Location controls are disposed with the UI',
|
||||
result.controlsReleased,
|
||||
);
|
||||
check('repeated UI disposal is inert', result.idempotent);
|
||||
check(
|
||||
'disposal and subsequent resize produce no uncaught browser errors',
|
||||
errors.length === 0,
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
if (failures) process.exitCode = 1;
|
||||
@@ -8594,6 +8594,16 @@ export class StyleManager {
|
||||
this._dataManagerUnsubscribe?.();
|
||||
this._dataManagerUnsubscribe = null;
|
||||
|
||||
this._cctvUnsubscribe?.();
|
||||
this._cctvUnsubscribe = null;
|
||||
this._commandDockTrayObserver?.disconnect?.();
|
||||
this._commandDockTrayObserver = null;
|
||||
this._draggableResizeObserver?.disconnect();
|
||||
this._draggableResizeObserver = null;
|
||||
if (this._windowResizeHandler) {
|
||||
window.removeEventListener('resize', this._windowResizeHandler);
|
||||
this._windowResizeHandler = null;
|
||||
}
|
||||
if (this._loadingVisibilityHandler) {
|
||||
document.removeEventListener('visibilitychange', this._loadingVisibilityHandler);
|
||||
this._loadingVisibilityHandler = null;
|
||||
|
||||
Reference in new issue
Block a user