Files
gods-eye-view/scripts/qa-ui-disposal.mjs
T

137 lines
4.7 KiB
JavaScript

#!/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._cctvControls._cctvUnsubscribe;
counts.cctv = 0;
if (cctvUnsubscribe)
ui._cctvControls._cctvUnsubscribe = () => {
counts.cctv++;
return cctvUnsubscribe();
};
const contextFields = ['_contextManagerUnsubscribe', '_dataManagerVisibilityRequestUnsubscribe', '_dataManagerVisibilityGuardUnsubscribe', '_dataManagerBeforeDestroyUnsubscribe'];
const contextConnected = contextFields.every((field) => typeof ui._contextControls[field] === 'function');
for (const field of contextFields) {
const unsubscribe = ui._contextControls[field];
counts[field] = 0;
ui._contextControls[field] = () => { counts[field]++; return unsubscribe?.(); };
}
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'),
),
contextReleased: contextConnected && ui._contextControls.destroyed
&& contextFields.every((field) => counts[field] === 1 && ui._contextControls[field] === null),
observersReleased: observed.every(
(name) => counts[name] === 1 && ui[name] === null,
),
subscriptionReleased:
counts.cctv === 1 && ui._cctvControls._cctvUnsubscribe === null,
resizeReleased: counts.resize === 1 && ui._windowResizeHandler === null,
controlsReleased:
ui._radioControls.destroyed &&
ui._locationControls.destroyed &&
ui._cctvControls.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 releases all Context subscriptions and stops its controls', result.contextReleased);
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, Location and CCTV 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;