diff --git a/src/main/window/focus-existing-window.test.ts b/src/main/window/focus-existing-window.test.ts index 3d0b54d7824..e12e71ae109 100644 --- a/src/main/window/focus-existing-window.test.ts +++ b/src/main/window/focus-existing-window.test.ts @@ -229,6 +229,43 @@ describe('focusExistingMainWindow', () => { expect(raceWindow.calls.focus).toHaveBeenCalledTimes(1) }) + it('reinforces win32 activation when a window is recovered on the retry path', () => { + const app = makeFakeApp() + const timer = makeTimer() + const openedWindow = makeFakeWindow() + const warn = vi.fn() + let attempts = 0 + const openWindow = vi.fn(() => { + attempts += 1 + if (attempts < 2) { + throw new Error('transient failure') + } + return openedWindow + }) + + focusExistingMainWindow({ + app, + getWindow: () => null, + openWindow, + warn, + platform: 'win32', + setTimeout: timer.setTimeout + }) + + timer.run(300) + // Why: the retry callback must run the same win32 reinforcement + focus retry + // as the sync success path, not just show/focus. + expect(openedWindow.calls.moveTop).toHaveBeenCalledTimes(1) + expect(openedWindow.calls.setAlwaysOnTop).toHaveBeenCalledWith(true) + + timer.run(100) + expect(app.focus).toHaveBeenCalledTimes(2) + expect(openedWindow.calls.focus).toHaveBeenCalledTimes(2) + + timer.run(250) + expect(openedWindow.calls.setAlwaysOnTop).toHaveBeenLastCalledWith(false) + }) + it('gives up after exhausting reopen retries', () => { const timer = makeTimer() const warn = vi.fn() @@ -253,5 +290,8 @@ describe('focusExistingMainWindow', () => { // a further run() call must not schedule or fire a 4th attempt. expect(openWindow).toHaveBeenCalledTimes(3) expect(warn).toHaveBeenCalledTimes(3) + timer.run(300) + expect(openWindow).toHaveBeenCalledTimes(3) + expect(warn).toHaveBeenCalledTimes(3) }) }) diff --git a/src/main/window/focus-existing-window.ts b/src/main/window/focus-existing-window.ts index 874c9adbfa5..21d1fd96b94 100644 --- a/src/main/window/focus-existing-window.ts +++ b/src/main/window/focus-existing-window.ts @@ -64,6 +64,27 @@ function retryFocus(window: BrowserWindow, app: Pick, setTimer: Fo }, 100) } +// Why: shared so the sync success path and the async retry/adopt callback can't +// drift on win32 reinforcement (moveTop/pulseAlwaysOnTop) or the 100ms focus retry. +function activateWindow( + window: BrowserWindow, + app: Pick, + platform: NodeJS.Platform, + setTimer: FocusTimer +): void { + safelyFocusApp(app) + safelyRevealWindow(window) + if (platform === 'win32') { + try { + window.moveTop() + } catch { + // Older Electron versions or destroyed windows may reject this; focus retry remains. + } + pulseAlwaysOnTop(window, setTimer) + } + retryFocus(window, app, setTimer) +} + // Why: a second-instance/activate reopen can race transient startup pressure // (e.g. GPU/process churn right after another launch attempt exits); one // swallowed throw would otherwise strand the app with no window until some @@ -73,6 +94,7 @@ const REOPEN_RETRY_DELAY_MS = 300 function openWindowWithRetry( opts: Pick, + platform: NodeJS.Platform, setTimer: FocusTimer, attempt: number ): BrowserWindow | null { @@ -93,10 +115,9 @@ function openWindowWithRetry( const window = existing && !existing.isDestroyed() ? existing - : openWindowWithRetry(opts, setTimer, attempt + 1) + : openWindowWithRetry(opts, platform, setTimer, attempt + 1) if (window) { - safelyFocusApp(opts.app) - safelyRevealWindow(window) + activateWindow(window, opts.app, platform, setTimer) } }, REOPEN_RETRY_DELAY_MS) return null @@ -115,23 +136,13 @@ export function focusExistingMainWindow( if (!opts.app.isReady()) { return 'pending' } - window = openWindowWithRetry(opts, setTimer, 1) + window = openWindowWithRetry(opts, platform, setTimer, 1) if (!window) { return 'pending' } openedWindow = true } - safelyFocusApp(opts.app) - safelyRevealWindow(window) - if (platform === 'win32') { - try { - window.moveTop() - } catch { - // Older Electron versions or destroyed windows may reject this; focus retry remains. - } - pulseAlwaysOnTop(window, setTimer) - } - retryFocus(window, opts.app, setTimer) + activateWindow(window, opts.app, platform, setTimer) return openedWindow ? 'opened' : 'focused' }