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.
This commit is contained in:
Jinjing
2026-07-20 17:58:46 -07:00
parent 3e8b548dd0
commit de272b3cb8
2 changed files with 66 additions and 15 deletions
@@ -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)
})
})
+26 -15
View File
@@ -64,6 +64,27 @@ function retryFocus(window: BrowserWindow, app: Pick<App, 'focus'>, 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<App, 'focus'>,
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<FocusExistingMainWindowOptions, 'app' | 'getWindow' | 'openWindow' | 'warn'>,
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'
}