mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-20 12:15:26 -07:00
fix(overlay): recycle Windows modal windows after close
- Refresh the hidden modal renderer between Windows sessions - Add regression coverage and stabilize launcher completion testing
This commit is contained in:
@@ -1010,44 +1010,85 @@ test('sendToActiveOverlayWindow flushes every queued load and ready listener bef
|
||||
assert.deepEqual(window.sent, [['runtime-options:open'], ['session-help:open']]);
|
||||
});
|
||||
|
||||
for (const platform of ['darwin', 'win32'] as const) {
|
||||
test(`modal reopen reuses the warm window and shows it immediately on ${platform}`, () => {
|
||||
const modalWindow = createMockWindow();
|
||||
let createCalls = 0;
|
||||
test('modal reopen reuses the warm window and shows it immediately on macOS', () => {
|
||||
const modalWindow = createMockWindow();
|
||||
let createCalls = 0;
|
||||
|
||||
const runtime = createOverlayModalRuntimeService(
|
||||
{
|
||||
getMainWindow: () => null,
|
||||
getModalWindow: () => modalWindow as never,
|
||||
createModalWindow: () => {
|
||||
createCalls += 1;
|
||||
return modalWindow as never;
|
||||
},
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
const runtime = createOverlayModalRuntimeService(
|
||||
{
|
||||
getMainWindow: () => null,
|
||||
getModalWindow: () => modalWindow as never,
|
||||
createModalWindow: () => {
|
||||
createCalls += 1;
|
||||
return modalWindow as never;
|
||||
},
|
||||
{ platform },
|
||||
);
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
},
|
||||
{ platform: 'darwin' },
|
||||
);
|
||||
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
|
||||
assert.equal(modalWindow.isDestroyed(), false);
|
||||
assert.equal(modalWindow.isVisible(), false);
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
|
||||
assert.equal(sent, true);
|
||||
assert.equal(createCalls, 0);
|
||||
assert.equal(modalWindow.isVisible(), true);
|
||||
assert.equal(modalWindow.getShowCount(), 2);
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
}
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
|
||||
assert.equal(modalWindow.isDestroyed(), false);
|
||||
assert.equal(modalWindow.isVisible(), false);
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
|
||||
assert.equal(sent, true);
|
||||
assert.equal(createCalls, 0);
|
||||
assert.equal(modalWindow.isVisible(), true);
|
||||
assert.equal(modalWindow.getShowCount(), 2);
|
||||
});
|
||||
|
||||
test('modal reopen on Windows uses a fresh prewarmed interactive window', () => {
|
||||
const firstWindow = createMockWindow();
|
||||
const replacementWindow = createMockWindow();
|
||||
let currentModal = firstWindow;
|
||||
let createCalls = 0;
|
||||
|
||||
const runtime = createOverlayModalRuntimeService(
|
||||
{
|
||||
getMainWindow: () => null,
|
||||
getModalWindow: () => currentModal as never,
|
||||
createModalWindow: () => {
|
||||
createCalls += 1;
|
||||
currentModal = replacementWindow;
|
||||
return replacementWindow as never;
|
||||
},
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
},
|
||||
{ platform: 'win32' },
|
||||
);
|
||||
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
|
||||
assert.equal(firstWindow.isDestroyed(), true);
|
||||
assert.equal(currentModal, replacementWindow);
|
||||
assert.equal(replacementWindow.isVisible(), false);
|
||||
assert.equal(createCalls, 1);
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('session-help:open', undefined, {
|
||||
restoreOnModalClose: 'session-help',
|
||||
});
|
||||
|
||||
assert.equal(sent, true);
|
||||
assert.equal(createCalls, 1);
|
||||
assert.equal(replacementWindow.isVisible(), true);
|
||||
assert.equal(replacementWindow.ignoreMouseEvents, false);
|
||||
assert.deepEqual(replacementWindow.sent, [['session-help:open']]);
|
||||
});
|
||||
|
||||
test('modal reopen on the warm window notifies state change for each lifecycle', () => {
|
||||
const modalWindow = createMockWindow();
|
||||
|
||||
@@ -87,7 +87,8 @@ export function createOverlayModalRuntimeService(
|
||||
const modalWindowBoundsReconcileGenerations = new WeakMap<BrowserWindow, number>();
|
||||
const modalWindowPrimeListenersRegistered = new WeakSet<BrowserWindow>();
|
||||
const platform = options.platform ?? process.platform;
|
||||
const keepModalWindowWarm = platform === 'darwin' || platform === 'win32';
|
||||
const shouldPrimeModalWindow = platform === 'darwin' || platform === 'win32';
|
||||
const reuseModalWindowAfterClose = platform === 'darwin';
|
||||
const focusApplication = options.focusApplication ?? requestOverlayApplicationFocus;
|
||||
const scheduleRevealFallback = (callback: () => void, delayMs: number): RevealFallbackHandle =>
|
||||
(options.scheduleRevealFallback ?? globalThis.setTimeout)(callback, delayMs);
|
||||
@@ -181,7 +182,7 @@ export function createOverlayModalRuntimeService(
|
||||
};
|
||||
|
||||
const primeModalWindow = (): boolean => {
|
||||
if (!keepModalWindowWarm) {
|
||||
if (!shouldPrimeModalWindow) {
|
||||
return false;
|
||||
}
|
||||
const modalWindow = resolveModalWindow();
|
||||
@@ -515,13 +516,19 @@ export function createOverlayModalRuntimeService(
|
||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||
clearPendingModalWindowReveal();
|
||||
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||
if (keepModalWindowWarm) {
|
||||
if (reuseModalWindowAfterClose) {
|
||||
modalWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
modalWindow.hide();
|
||||
markModalWindowPrimed(modalWindow);
|
||||
} else {
|
||||
modalWindow.destroy();
|
||||
modalWindowPrimedForImmediateShow = false;
|
||||
// Reusing a transparent click-through BrowserWindow can leave later modal sessions
|
||||
// non-interactive on Windows. Recycle the renderer after every close, then warm its
|
||||
// replacement so the next shortcut still opens promptly.
|
||||
if (platform === 'win32') {
|
||||
primeModalWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
mainWindowMousePassthroughForcedByModal = false;
|
||||
|
||||
@@ -31,6 +31,20 @@ function makeSpawn(): { spawn: SyncLauncherSpawn; children: FakeChild[]; command
|
||||
return { spawn, children, commands };
|
||||
}
|
||||
|
||||
async function waitForResult<T>(promise: Promise<T>, timeoutMs = 3000): Promise<T> {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
timeout = setTimeout(() => reject(new Error('Timed out waiting for result.')), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timeout !== null) clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
test('runSyncLauncher parses NDJSON events across chunk boundaries', async () => {
|
||||
const { spawn, children, commands } = makeSpawn();
|
||||
const events: SyncProgressEvent[] = [];
|
||||
@@ -96,7 +110,9 @@ test('runSyncLauncher settles after exit when close never arrives', async () =>
|
||||
// so `close` never fires.
|
||||
child.emit('exit', 1, null);
|
||||
|
||||
const result = await handle.done;
|
||||
// Keep the isolated Bun test process alive while the production drain timer
|
||||
// remains unref'ed, and fail instead of hanging if the result never settles.
|
||||
const result = await waitForResult(handle.done);
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error ?? '', /remote refused/);
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ export function runSyncLauncher(options: {
|
||||
spawn?: SyncLauncherSpawn;
|
||||
timeoutMs?: number;
|
||||
}): SyncLauncherRunHandle {
|
||||
const spawn =
|
||||
const spawn: SyncLauncherSpawn =
|
||||
options.spawn ??
|
||||
((command, args) => {
|
||||
// The child must boot as a full Electron app (its entry handles
|
||||
|
||||
Reference in New Issue
Block a user