mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
- Google 3D: the OPEN button re-measures a ground whose tiles are still loading (a few times, a second apart) and on a map change, not only after the next camera move. - Mapillary: two mounts on one host during the first download share one build instead of leaking a second WebGL viewer. - Retired floating-panel keys (v8 panelPos, panelFloatHintShown) are dropped from localStorage; dead inline-position resets, the drag JSDoc and the dead qa-ui-disposal observer branch are gone. - Docs: z ladder, panel chrome, position storage and the fixture gate's actual coverage; changelog fragment for #974 (old .gev-cache/mapillary can be deleted). - ru/es: the layer is named as everywhere else in the UI. - Fixtures and test fakes: unused exports removed; a fake credit removal of an absent credit no longer drops another one; the tile test reset frees upstream slots. Co-Authored-By: Claude Opus 5.5 <[email protected]>
280 lines
9.4 KiB
JavaScript
280 lines
9.4 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 = {};
|
|
document.dispatchEvent(
|
|
new KeyboardEvent('keydown', {
|
|
key: '`',
|
|
code: 'Backquote',
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}),
|
|
);
|
|
const fpsReadout = document.querySelector('.frame-rate-readout');
|
|
const fpsWasVisible = fpsReadout && !fpsReadout.hidden;
|
|
const watchObserver = (name) => {
|
|
const observer = ui._panelLayout[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',
|
|
'_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 cockpit = ui.cockpitView;
|
|
const portal = ui._cockpitDisplayPortal;
|
|
const cockpitListenerCount = cockpit._listenerRemovers.length;
|
|
counts.cockpit = 0;
|
|
cockpit._listenerRemovers = cockpit._listenerRemovers.map(
|
|
(remove) => () => {
|
|
counts.cockpit++;
|
|
return remove();
|
|
},
|
|
);
|
|
const portalHomes = portal.records.map((record) => ({
|
|
group: record.group,
|
|
home: record.anchor.parentNode,
|
|
anchor: record.anchor,
|
|
}));
|
|
portal.setActive(true);
|
|
let releaseRestoration;
|
|
const restorationGate = new Promise((resolve) => {
|
|
releaseRestoration = resolve;
|
|
});
|
|
const restoreForDisposal = ui._contextControls.restoreForDisposal.bind(
|
|
ui._contextControls,
|
|
);
|
|
ui._contextControls.restoreForDisposal = async () => {
|
|
await restorationGate;
|
|
return restoreForDisposal();
|
|
};
|
|
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);
|
|
};
|
|
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;
|
|
let stateNotifications = 0;
|
|
ui.subscribeShareState(() => stateNotifications++, { emitCurrent: false });
|
|
ui.subscribeLocationSearch(() => stateNotifications++, {
|
|
emitCurrent: false,
|
|
});
|
|
try {
|
|
const disposal = ui.dispose();
|
|
const focusBefore = document.activeElement;
|
|
const stoppedBeforeRestoration =
|
|
ui._panelLayout.destroyed &&
|
|
ui._feedback.destroyed &&
|
|
ui._recording.destroyed &&
|
|
cockpit.destroyed &&
|
|
portal.stopped &&
|
|
cockpit._listenerRemovers.length === 0 &&
|
|
portal.frames.size === 0 &&
|
|
cockpit.enter() === false &&
|
|
cockpit.navigateContext(1) === false;
|
|
await new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
const noDeferredFocus = document.activeElement === focusBefore;
|
|
releaseRestoration();
|
|
await disposal;
|
|
ui._syncShareState();
|
|
ui.subscribeShareState(() => stateNotifications++);
|
|
ui.subscribeLocationSearch(() => stateNotifications++);
|
|
const once = JSON.stringify(counts);
|
|
await ui.dispose();
|
|
window.dispatchEvent(new Event('resize'));
|
|
await new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
return {
|
|
cockpitStoppedBeforeRestoration:
|
|
stoppedBeforeRestoration && noDeferredFocus,
|
|
cockpitReleased:
|
|
cockpit.disposed &&
|
|
cockpit.destroyed &&
|
|
counts.cockpit === cockpitListenerCount &&
|
|
cockpitListenerCount > 0 &&
|
|
portal.destroyed &&
|
|
portal.listeners.signal.aborted &&
|
|
portal.frames.size === 0 &&
|
|
ui._cockpitDisplayPortal === null &&
|
|
portalHomes.length === 4 &&
|
|
portalHomes.every(
|
|
(record) =>
|
|
record.group.parentNode === record.home &&
|
|
!record.anchor.isConnected,
|
|
),
|
|
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._panelLayout[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,
|
|
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,
|
|
frameRateReleased:
|
|
fpsWasVisible && !fpsReadout.isConnected && fpsReadout.hidden,
|
|
stateStopped: stateNotifications === 0,
|
|
idempotent: once === JSON.stringify(counts),
|
|
};
|
|
} finally {
|
|
releaseRestoration();
|
|
window.removeEventListener = removeEventListener;
|
|
}
|
|
});
|
|
check(
|
|
'visible frame-rate monitor is removed on UI disposal',
|
|
result.frameRateReleased,
|
|
);
|
|
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(
|
|
'Cockpit actions and queued portal work stop before layer restoration',
|
|
result.cockpitStoppedBeforeRestoration,
|
|
);
|
|
check(
|
|
'Cockpit cleanup releases listeners and returns all Display groups home',
|
|
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(
|
|
'state subscriptions stop synchronously with UI disposal',
|
|
result.stateStopped,
|
|
);
|
|
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;
|