diff --git a/apps/electron-backend-e2e/src/window-controls.e2e.ts b/apps/electron-backend-e2e/src/window-controls.e2e.ts index 5e63f8db1..5d4f36c99 100644 --- a/apps/electron-backend-e2e/src/window-controls.e2e.ts +++ b/apps/electron-backend-e2e/src/window-controls.e2e.ts @@ -7,6 +7,13 @@ import { // Custom window controls are only rendered on Windows/Linux; macOS keeps // the native traffic lights. +// +// Linux CI runs Electron under xvfb, which has no window manager, so +// maximize/minimize never take effect there — those tests are skipped and +// rely on Windows CI plus local Linux/macOS runs for coverage. +const isWindowManagerlessCi = + process.platform === 'linux' && !!process.env['CI']; + test.describe('Custom window controls', () => { test.skip( process.platform === 'darwin', @@ -39,6 +46,10 @@ test.describe('Custom window controls', () => { test('@electron maximize button toggles the native window state', async ({ dataDir, }) => { + test.skip( + isWindowManagerlessCi, + 'xvfb on Linux CI has no window manager' + ); const app = await launchElectronApp(dataDir); try { @@ -67,6 +78,10 @@ test.describe('Custom window controls', () => { test('@electron reflects window state changes triggered from the main process', async ({ dataDir, }) => { + test.skip( + isWindowManagerlessCi, + 'xvfb on Linux CI has no window manager' + ); const app = await launchElectronApp(dataDir); try { @@ -91,6 +106,10 @@ test.describe('Custom window controls', () => { test('@electron minimize button minimizes the window', async ({ dataDir, }) => { + test.skip( + isWindowManagerlessCi, + 'xvfb on Linux CI has no window manager' + ); const app = await launchElectronApp(dataDir); try { diff --git a/apps/electron-backend/src/app/app.ts b/apps/electron-backend/src/app/app.ts index b970513ea..884e8a54f 100644 --- a/apps/electron-backend/src/app/app.ts +++ b/apps/electron-backend/src/app/app.ts @@ -258,6 +258,13 @@ export default class App { } private static attachWindowStateEvents(win: Electron.BrowserWindow): void { + // Only Windows/Linux render custom window controls that subscribe + // to these pushes; macOS keeps the native traffic lights, so + // sending state updates there would be dead IPC traffic. + if (process.platform === 'darwin') { + return; + } + const sendWindowState = () => { if (win.isDestroyed()) { return; diff --git a/apps/electron-backend/src/app/events/window.events.ts b/apps/electron-backend/src/app/events/window.events.ts index 4a4f5a9f2..a5e01ae2a 100644 --- a/apps/electron-backend/src/app/events/window.events.ts +++ b/apps/electron-backend/src/app/events/window.events.ts @@ -45,15 +45,26 @@ ipcMain.handle(WINDOW_MINIMIZE, (event) => { ipcMain.handle(WINDOW_TOGGLE_MAXIMIZE, (event): WindowState => { const win = getSenderWindow(event); - if (win) { - if (win.isMaximized()) { - win.unmaximize(); - } else { - win.maximize(); - } + if (!win) { + return getWindowState(win); } - return getWindowState(win); + // maximize()/unmaximize() complete asynchronously on Linux window + // managers, so re-reading isMaximized() here would race. Report the + // requested state instead; the WINDOW:STATE_CHANGED push stays the + // authoritative update once the window manager has acted. + const shouldMaximize = !win.isMaximized(); + + if (shouldMaximize) { + win.maximize(); + } else { + win.unmaximize(); + } + + return { + isMaximized: shouldMaximize, + isFullScreen: win.isFullScreen(), + }; }); // win.close() (instead of app.quit()) so the window's 'close' handler still