From de272b3cb8f53bba79d9a7b5eefd4fc56cccf237 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:58:46 -0700 Subject: [PATCH] Unify win32 window activation for sync and retry paths Extract activateWindow to prevent drift between sync and async paths on win32 reinforcement (moveTop, pulseAlwaysOnTop, retry focus). Adds test coverage for window recovery on retry. --- src/main/window/focus-existing-window.test.ts | 40 ++++++++++++++++++ src/main/window/focus-existing-window.ts | 41 ++++++++++++------- 2 files changed, 66 insertions(+), 15 deletions(-) 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' }