mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-11 02:46:56 +08:00
fix(desktop): stabilize macOS window recovery
This commit is contained in:
@@ -51,6 +51,7 @@ export class ElectronShellGeneration {
|
||||
private mounted = false
|
||||
private released = false
|
||||
private attentionCount = 0
|
||||
private cancelPendingFullscreenHide: (() => void) | undefined
|
||||
private cleanupListeners: (() => void) | undefined
|
||||
|
||||
constructor(private readonly options: ElectronShellGenerationOptions) {}
|
||||
@@ -78,9 +79,28 @@ export class ElectronShellGeneration {
|
||||
if (applicationNeedsReveal(window, platform.platform)) this.show()
|
||||
}
|
||||
const clearAttention = (): void => { this.clearAttention() }
|
||||
let fullscreenHidePending = false
|
||||
const hideAfterFullscreen = (): void => {
|
||||
if (!fullscreenHidePending) return
|
||||
fullscreenHidePending = false
|
||||
if (!window.isDestroyed()) window.hide()
|
||||
}
|
||||
const cancelPendingFullscreenHide = (): void => {
|
||||
if (!fullscreenHidePending) return
|
||||
fullscreenHidePending = false
|
||||
window.off('leave-full-screen', hideAfterFullscreen)
|
||||
}
|
||||
this.cancelPendingFullscreenHide = cancelPendingFullscreenHide
|
||||
const close = (event: Electron.Event): void => {
|
||||
if (this.options.isQuitting()) return
|
||||
event.preventDefault()
|
||||
if (platform.platform === 'darwin' && window.isFullScreen()) {
|
||||
if (fullscreenHidePending) return
|
||||
fullscreenHidePending = true
|
||||
window.once('leave-full-screen', hideAfterFullscreen)
|
||||
window.setFullScreen(false)
|
||||
return
|
||||
}
|
||||
window.hide()
|
||||
}
|
||||
const preserveBlankTitle = (event: Electron.Event): void => { event.preventDefault() }
|
||||
@@ -172,6 +192,7 @@ export class ElectronShellGeneration {
|
||||
window.off('focus', clearAttention)
|
||||
window.off('page-title-updated', preserveBlankTitle)
|
||||
window.off('ready-to-show', show)
|
||||
cancelPendingFullscreenHide()
|
||||
window.webContents.off('before-input-event', handleZoomShortcut)
|
||||
window.webContents.off('will-frame-navigate', navigate)
|
||||
window.webContents.off('will-redirect', redirect)
|
||||
@@ -199,6 +220,7 @@ export class ElectronShellGeneration {
|
||||
show(): void {
|
||||
const window = this.window
|
||||
if (window === undefined || window.isDestroyed()) return
|
||||
this.cancelPendingFullscreenHide?.()
|
||||
this.clearAttention()
|
||||
revealApplication(window, this.options.platform.platform)
|
||||
}
|
||||
@@ -243,6 +265,7 @@ export class ElectronShellGeneration {
|
||||
this.clearAttention()
|
||||
this.window = undefined
|
||||
this.tray = undefined
|
||||
this.cancelPendingFullscreenHide = undefined
|
||||
if (window === undefined) return
|
||||
|
||||
this.cleanupListeners?.()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { revealApplication } from './electron-reveal.ts'
|
||||
import type { DesktopLocale } from './runtime.ts'
|
||||
|
||||
const PROFILE_CREATE_SCHEME = 'dsh-profile-create:'
|
||||
@@ -79,8 +80,7 @@ export class ProfileCreateWindow {
|
||||
open(): void {
|
||||
const existing = this.window
|
||||
if (existing !== undefined && !existing.isDestroyed()) {
|
||||
existing.show()
|
||||
existing.focus()
|
||||
revealApplication(existing)
|
||||
return
|
||||
}
|
||||
this.disposed = false
|
||||
@@ -118,7 +118,7 @@ export class ProfileCreateWindow {
|
||||
window.webContents.on('will-navigate', navigate)
|
||||
window.webContents.on('will-redirect', navigate)
|
||||
window.once('ready-to-show', () => {
|
||||
if (!this.disposed && this.window === window && !window.isDestroyed()) window.show()
|
||||
if (!this.disposed && this.window === window && !window.isDestroyed()) revealApplication(window)
|
||||
})
|
||||
window.on('closed', () => {
|
||||
// Electron destroys webContents before emitting BrowserWindow's `closed`.
|
||||
|
||||
@@ -112,6 +112,7 @@ const electron = vi.hoisted(() => {
|
||||
readonly isFocused = vi.fn(() => false)
|
||||
readonly isVisible = vi.fn(() => false)
|
||||
readonly isMinimized = vi.fn(() => false)
|
||||
readonly isFullScreen = vi.fn(() => false)
|
||||
readonly flashFrame = vi.fn()
|
||||
readonly restore = vi.fn()
|
||||
readonly show = vi.fn()
|
||||
@@ -124,6 +125,7 @@ const electron = vi.hoisted(() => {
|
||||
readonly loadURL = loadURL
|
||||
readonly removeMenu = vi.fn()
|
||||
readonly setBackgroundMaterial = vi.fn()
|
||||
readonly setFullScreen = vi.fn()
|
||||
}
|
||||
|
||||
class Tray {
|
||||
@@ -870,6 +872,61 @@ describe('Electron desktop runtime', () => {
|
||||
await release()
|
||||
})
|
||||
|
||||
it('leaves macOS fullscreen before hiding and ignores repeated close events during the transition', async () => {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
||||
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
|
||||
const runtime = new ElectronDesktopRuntime(async () => {})
|
||||
const release = runtime.schedule(spec)
|
||||
await runtime.mountScheduled()
|
||||
|
||||
const window = electron.browserWindows[0]
|
||||
window?.isFullScreen.mockReturnValue(true)
|
||||
const close = electron.browserWindowOn.mock.calls.find(([event]) => event === 'close')?.[1]
|
||||
expect(close).toEqual(expect.any(Function))
|
||||
|
||||
const first = { preventDefault: vi.fn() }
|
||||
const second = { preventDefault: vi.fn() }
|
||||
close(first)
|
||||
close(second)
|
||||
|
||||
expect(first.preventDefault).toHaveBeenCalledOnce()
|
||||
expect(second.preventDefault).toHaveBeenCalledOnce()
|
||||
expect(window?.setFullScreen).toHaveBeenCalledOnce()
|
||||
expect(window?.setFullScreen).toHaveBeenCalledWith(false)
|
||||
expect(window?.hide).not.toHaveBeenCalled()
|
||||
const leaveFullscreen = window?.once.mock.calls.find(([event]) => event === 'leave-full-screen')?.[1]
|
||||
expect(leaveFullscreen).toEqual(expect.any(Function))
|
||||
|
||||
leaveFullscreen()
|
||||
expect(window?.hide).toHaveBeenCalledOnce()
|
||||
|
||||
await release()
|
||||
})
|
||||
|
||||
it('cancels a pending fullscreen hide when the user explicitly reveals the macOS window', async () => {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
||||
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
|
||||
const runtime = new ElectronDesktopRuntime(async () => {})
|
||||
const release = runtime.schedule(spec)
|
||||
await runtime.mountScheduled()
|
||||
|
||||
const window = electron.browserWindows[0]
|
||||
window?.isFullScreen.mockReturnValue(true)
|
||||
const close = electron.browserWindowOn.mock.calls.find(([event]) => event === 'close')?.[1]
|
||||
close({ preventDefault: vi.fn() })
|
||||
const leaveFullscreen = window?.once.mock.calls.find(([event]) => event === 'leave-full-screen')?.[1]
|
||||
|
||||
runtime.show()
|
||||
leaveFullscreen()
|
||||
|
||||
expect(window?.off).toHaveBeenCalledWith('leave-full-screen', expect.any(Function))
|
||||
expect(window?.hide).not.toHaveBeenCalled()
|
||||
expect(window?.show).toHaveBeenCalledOnce()
|
||||
expect(window?.focus).toHaveBeenCalledOnce()
|
||||
|
||||
await release()
|
||||
})
|
||||
|
||||
it('flashes the Windows taskbar and clears attention on focus and release', async () => {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
||||
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
|
||||
|
||||
@@ -6,8 +6,10 @@ const electron = vi.hoisted(() => {
|
||||
loadURL: ReturnType<typeof vi.fn>
|
||||
show: ReturnType<typeof vi.fn>
|
||||
focus: ReturnType<typeof vi.fn>
|
||||
restore: ReturnType<typeof vi.fn>
|
||||
close: ReturnType<typeof vi.fn>
|
||||
isDestroyed: ReturnType<typeof vi.fn>
|
||||
isMinimized: ReturnType<typeof vi.fn>
|
||||
loadFile: ReturnType<typeof vi.fn>
|
||||
once: ReturnType<typeof vi.fn>
|
||||
on: ReturnType<typeof vi.fn>
|
||||
@@ -22,8 +24,10 @@ const electron = vi.hoisted(() => {
|
||||
executeJavaScript: vi.fn(async () => {}),
|
||||
}
|
||||
readonly isDestroyed = vi.fn(() => false)
|
||||
readonly isMinimized = vi.fn(() => false)
|
||||
readonly show = vi.fn()
|
||||
readonly focus = vi.fn()
|
||||
readonly restore = vi.fn()
|
||||
readonly close = vi.fn()
|
||||
readonly loadFile = vi.fn(async () => {})
|
||||
readonly loadURL = vi.fn(async () => {})
|
||||
@@ -35,10 +39,17 @@ const electron = vi.hoisted(() => {
|
||||
windows.push(this as unknown as typeof windows[number])
|
||||
}
|
||||
}
|
||||
return { BrowserWindow, windows }
|
||||
return {
|
||||
app: {
|
||||
isHidden: vi.fn(() => false),
|
||||
show: vi.fn(),
|
||||
},
|
||||
BrowserWindow,
|
||||
windows,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('electron', () => ({ BrowserWindow: electron.BrowserWindow }))
|
||||
vi.mock('electron', () => ({ app: electron.app, BrowserWindow: electron.BrowserWindow }))
|
||||
|
||||
import {
|
||||
ProfileCreateWindow,
|
||||
@@ -46,7 +57,10 @@ import {
|
||||
} from '../src/profile-create-window.ts'
|
||||
|
||||
describe('ProfileCreateWindow', () => {
|
||||
beforeEach(() => { electron.windows.length = 0 })
|
||||
beforeEach(() => {
|
||||
electron.windows.length = 0
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('parses only local submit and cancel actions', () => {
|
||||
expect(parseProfileCreateAction('dsh-profile-create://submit?name=work')).toEqual({ action: 'submit', name: 'work' })
|
||||
@@ -83,6 +97,23 @@ describe('ProfileCreateWindow', () => {
|
||||
expect(electron.windows[0]?.options).not.toHaveProperty('parent')
|
||||
})
|
||||
|
||||
it('restores the hidden macOS application before showing the first profile window', () => {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
||||
electron.app.isHidden.mockReturnValue(true)
|
||||
const creator = new ProfileCreateWindow({ locale: 'en', onSubmit: async () => {} })
|
||||
creator.open()
|
||||
const window = electron.windows[0]
|
||||
window?.isMinimized.mockReturnValue(true)
|
||||
const ready = window?.once.mock.calls.find(([event]) => event === 'ready-to-show')?.[1]
|
||||
|
||||
ready()
|
||||
|
||||
expect(electron.app.show).toHaveBeenCalledOnce()
|
||||
expect(window?.restore).toHaveBeenCalledOnce()
|
||||
expect(window?.show).toHaveBeenCalledOnce()
|
||||
expect(window?.focus).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not access destroyed web contents from the closed callback', () => {
|
||||
const creator = new ProfileCreateWindow({ locale: 'en', onSubmit: async () => {} })
|
||||
creator.open()
|
||||
|
||||
Reference in New Issue
Block a user