mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix: prevent window re-maximize when opening browser tab (#666)
This commit is contained in:
parent
66456acf8d
commit
81144f29db
5 changed files with 166 additions and 4 deletions
|
|
@ -24,10 +24,30 @@ afterEach(() => {
|
|||
})
|
||||
|
||||
describe('configureDevUserDataPath', () => {
|
||||
it('uses an explicit dev userData override when provided', async () => {
|
||||
const { app } = await import('electron')
|
||||
const { configureDevUserDataPath } = await import('./configure-process')
|
||||
const originalOverride = process.env.ORCA_DEV_USER_DATA_PATH
|
||||
process.env.ORCA_DEV_USER_DATA_PATH = '/tmp/orca-dev-repro'
|
||||
|
||||
try {
|
||||
configureDevUserDataPath(true)
|
||||
} finally {
|
||||
if (originalOverride === undefined) {
|
||||
delete process.env.ORCA_DEV_USER_DATA_PATH
|
||||
} else {
|
||||
process.env.ORCA_DEV_USER_DATA_PATH = originalOverride
|
||||
}
|
||||
}
|
||||
|
||||
expect(app.setPath).toHaveBeenCalledWith('userData', '/tmp/orca-dev-repro')
|
||||
})
|
||||
|
||||
it('moves dev runs onto an orca-dev userData path', async () => {
|
||||
const { app } = await import('electron')
|
||||
const { configureDevUserDataPath } = await import('./configure-process')
|
||||
|
||||
delete process.env.ORCA_DEV_USER_DATA_PATH
|
||||
configureDevUserDataPath(true)
|
||||
|
||||
// Why: production code uses path.join(app.getPath('appData'), 'orca-dev')
|
||||
|
|
@ -159,3 +179,18 @@ describe('installDevParentWatchdog', () => {
|
|||
expect(setIntervalSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('enableMainProcessGpuFeatures', () => {
|
||||
it('appends Orca GPU flags by default', async () => {
|
||||
const { app } = await import('electron')
|
||||
const { enableMainProcessGpuFeatures } = await import('./configure-process')
|
||||
|
||||
enableMainProcessGpuFeatures()
|
||||
|
||||
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
|
||||
'enable-features',
|
||||
'Vulkan,UseSkiaGraphite'
|
||||
)
|
||||
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('enable-unsafe-webgpu')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -66,6 +66,14 @@ export function configureDevUserDataPath(isDev: boolean): void {
|
|||
if (!isDev) {
|
||||
return
|
||||
}
|
||||
const overrideUserDataPath = process.env.ORCA_DEV_USER_DATA_PATH
|
||||
if (overrideUserDataPath) {
|
||||
// Why: automated Electron repros need an isolated profile so persisted
|
||||
// tabs/worktrees from the developer's normal `orca-dev` session do not
|
||||
// change startup behavior and hide or create window-management bugs.
|
||||
app.setPath('userData', overrideUserDataPath)
|
||||
return
|
||||
}
|
||||
// Why: development runs share the same machine as packaged Orca, and both
|
||||
// publish runtime bootstrap files under userData. Without a dev-only path,
|
||||
// `pnpm dev` can overwrite the packaged app's runtime pointer and make the
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ vi.mock('electron', () => ({
|
|||
BrowserWindow: browserWindowMock,
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
|
||||
nativeTheme: { shouldUseDarkColors: false },
|
||||
screen: {
|
||||
getPrimaryDisplay: () => ({ workAreaSize: { width: 1440, height: 900 } })
|
||||
},
|
||||
shell: { openExternal: openExternalMock }
|
||||
}))
|
||||
|
||||
|
|
@ -44,6 +47,7 @@ describe('createMainWindow', () => {
|
|||
isMock.dev = false
|
||||
vi.mocked(ipcMain.on).mockReset()
|
||||
vi.mocked(ipcMain.removeListener).mockReset()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('enables renderer sandboxing and opens external links safely', () => {
|
||||
|
|
@ -87,6 +91,14 @@ describe('createMainWindow', () => {
|
|||
webPreferences: expect.objectContaining({ sandbox: true })
|
||||
})
|
||||
)
|
||||
const browserWindowOptions = browserWindowMock.mock.calls[0]?.[0]
|
||||
if (process.platform === 'darwin') {
|
||||
expect(browserWindowOptions).toMatchObject({
|
||||
titleBarStyle: 'hiddenInset'
|
||||
})
|
||||
} else {
|
||||
expect(browserWindowOptions.titleBarStyle).toBeUndefined()
|
||||
}
|
||||
|
||||
expect(windowHandlers.windowOpen({ url: 'https://example.com' })).toEqual({ action: 'deny' })
|
||||
expect(windowHandlers.windowOpen({ url: 'localhost:3000' })).toEqual({ action: 'deny' })
|
||||
|
|
@ -487,4 +499,49 @@ describe('createMainWindow', () => {
|
|||
|
||||
expect(browserWindowInstance.setWindowButtonPosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores duplicate ready-to-show events after startup maximize has already run', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
on: vi.fn(),
|
||||
setZoomLevel: vi.fn(),
|
||||
setBackgroundThrottling: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
setWindowOpenHandler: vi.fn(),
|
||||
send: vi.fn()
|
||||
}
|
||||
const browserWindowInstance = {
|
||||
webContents,
|
||||
on: vi.fn((event, handler) => {
|
||||
windowHandlers[event] = handler
|
||||
}),
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isMaximized: vi.fn(() => false),
|
||||
isFullScreen: vi.fn(() => false),
|
||||
getSize: vi.fn(() => [1200, 800]),
|
||||
setSize: vi.fn(),
|
||||
setWindowButtonPosition: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
show: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
loadURL: vi.fn()
|
||||
}
|
||||
browserWindowMock.mockImplementation(function () {
|
||||
return browserWindowInstance
|
||||
})
|
||||
|
||||
createMainWindow({
|
||||
getUI: () =>
|
||||
({
|
||||
windowMaximized: true
|
||||
}) as never,
|
||||
updateUI: vi.fn()
|
||||
} as never)
|
||||
|
||||
windowHandlers['ready-to-show']()
|
||||
windowHandlers['ready-to-show']()
|
||||
|
||||
expect(browserWindowInstance.maximize).toHaveBeenCalledTimes(1)
|
||||
expect(browserWindowInstance.show).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BrowserWindow, ipcMain, nativeTheme, shell } from 'electron'
|
||||
import { BrowserWindow, ipcMain, nativeTheme, screen, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import icon from '../../../resources/icon.png?asset'
|
||||
|
|
@ -61,9 +61,24 @@ export function createMainWindow(
|
|||
store: Store | null,
|
||||
opts?: CreateMainWindowOptions
|
||||
): BrowserWindow {
|
||||
const savedBounds = store?.getUI().windowBounds
|
||||
const savedMaximized = store?.getUI().windowMaximized ?? false
|
||||
// Why: on first launch (no saved bounds), fill the primary display work area
|
||||
// so the window feels spacious without calling maximize(). Saved bounds still
|
||||
// win on subsequent launches.
|
||||
const defaultBounds = (() => {
|
||||
try {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
return { width, height }
|
||||
} catch {
|
||||
return { width: 1200, height: 800 }
|
||||
}
|
||||
})()
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
width: savedBounds?.width ?? defaultBounds.width,
|
||||
height: savedBounds?.height ?? defaultBounds.height,
|
||||
...(savedBounds ? { x: savedBounds.x, y: savedBounds.y } : {}),
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
show: false,
|
||||
|
|
@ -114,11 +129,53 @@ export function createMainWindow(
|
|||
}
|
||||
})
|
||||
|
||||
// Why: on macOS + Electron 41, creating a webview guest process can re-emit
|
||||
// ready-to-show on the same BrowserWindow. Without a one-shot guard the
|
||||
// handler re-runs maximize() from the persisted savedMaximized flag, snapping
|
||||
// the window back to full-screen after the user already resized it (#591).
|
||||
let handledInitialReadyToShow = false
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.maximize()
|
||||
if (handledInitialReadyToShow) {
|
||||
return
|
||||
}
|
||||
handledInitialReadyToShow = true
|
||||
|
||||
if (savedMaximized) {
|
||||
mainWindow.maximize()
|
||||
}
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
// Why: persist window bounds so the app restores to the user's last
|
||||
// position/size instead of maximizing on every launch. Debounce to avoid
|
||||
// hammering the persistence layer during continuous resize drags.
|
||||
let boundsTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const saveBounds = (): void => {
|
||||
if (boundsTimer) {
|
||||
clearTimeout(boundsTimer)
|
||||
}
|
||||
boundsTimer = setTimeout(() => {
|
||||
boundsTimer = null
|
||||
if (mainWindow.isDestroyed() || mainWindow.isFullScreen()) {
|
||||
return
|
||||
}
|
||||
const isMaximized = mainWindow.isMaximized()
|
||||
store?.updateUI({ windowMaximized: isMaximized })
|
||||
if (!isMaximized) {
|
||||
store?.updateUI({ windowBounds: mainWindow.getBounds() })
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
mainWindow.on('resize', saveBounds)
|
||||
mainWindow.on('move', saveBounds)
|
||||
|
||||
mainWindow.on('maximize', () => {
|
||||
store?.updateUI({ windowMaximized: true })
|
||||
})
|
||||
mainWindow.on('unmaximize', () => {
|
||||
store?.updateUI({ windowMaximized: false, windowBounds: mainWindow.getBounds() })
|
||||
})
|
||||
|
||||
mainWindow.on('enter-full-screen', () => {
|
||||
mainWindow.webContents.send('window:fullscreen-changed', true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -575,6 +575,11 @@ export type PersistedUIState = {
|
|||
/** URL to navigate to when a new browser tab is opened. Null means blank tab.
|
||||
* Phase 3 will expand this to a full BrowserSessionProfile per workspace. */
|
||||
browserDefaultUrl?: string | null
|
||||
/** Saved window bounds so the app restores to the user's last position/size
|
||||
* instead of maximizing on every launch. */
|
||||
windowBounds?: { x: number; y: number; width: number; height: number } | null
|
||||
/** Whether the window was maximized when it was last closed. */
|
||||
windowMaximized?: boolean
|
||||
}
|
||||
|
||||
// ─── Persistence shape ──────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in a new issue