import type { ElectronApplication, Page } from '@playwright/test'; import { mkdir } from 'node:fs/promises'; import { createServer } from 'node:http'; import { join } from 'node:path'; import type { AgentBrowserSnapshot } from '../../shared/agent-browser'; import { expect, getStableWindow, test } from './fixtures/electron'; async function hostRequest(page: Page, path: string, body?: unknown): Promise { const response = await page.evaluate(async (request) => { return await window.electron.ipcRenderer.invoke('hostapi:fetch', request) as { ok: boolean; data?: { ok: boolean; status: number; json?: unknown }; error?: unknown; }; }, { path, method: body === undefined ? 'GET' : 'POST', body }); expect(response, path).toMatchObject({ ok: true, data: { ok: true } }); return response.data!.json as T; } async function mainWindowState(app: ElectronApplication) { return await app.evaluate(({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; return win ? { id: win.id, minimized: win.isMinimized(), maximized: win.isMaximized(), fullScreen: win.isFullScreen(), visible: win.isVisible(), } : null; }); } async function reopenMainWindow(app: ElectronApplication): Promise { const opened = app.waitForEvent('window'); await app.evaluate(({ app: electronApp }) => { electronApp.emit('activate'); }); await opened; const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await expect.poll(async () => (await mainWindowState(app))?.visible).toBe(true); return page; } async function quitApplication(app: ElectronApplication): Promise { const child = app.process(); const closed = app.waitForEvent('close', { timeout: 15_000 }); // The native Cmd+Q menu role calls this same Electron quit lifecycle. await app.evaluate(({ app: electronApp }) => { electronApp.quit(); }); await closed; await expect.poll(() => child.exitCode).not.toBeNull(); expect(child.exitCode).toBe(0); } test('macOS closes and reopens the real window with working controls and a newly attached preview', async ({ launchElectronApp, electronTestResources, }) => { test.skip(process.platform !== 'darwin', 'macOS keeps the application running after its last window closes'); const projectPath = join(electronTestResources.rootDir, 'window-lifecycle-project'); await mkdir(projectPath); const server = createServer((_request, response) => { response.setHeader('Content-Type', 'text/html; charset=utf-8'); response.end('Window lifecycle preview

Isolated local preview

'); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); }); const address = server.address(); if (!address || typeof address === 'string') throw new Error('Missing local preview port'); const url = `http://127.0.0.1:${address.port}/`; try { const app = await launchElectronApp({ skipSetup: true }); let page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); const pid = app.process().pid; expect(await app.evaluate(({ app: electronApp }) => electronApp.getPath('userData'))) .toBe(electronTestResources.userDataDir); const { project } = await hostRequest<{ project: { id: string } }>(page, '/api/coding/projects/open', { projectPath }); const windowIds = new Set(); for (let cycle = 0; cycle < 3; cycle += 1) { const state = await mainWindowState(app); expect(state).not.toBeNull(); expect(windowIds.has(state!.id)).toBe(false); windowIds.add(state!.id); expect(app.process().pid).toBe(pid); expect(app.process().exitCode).toBeNull(); expect(await page.evaluate(() => window.electron.ipcRenderer.invoke('app:platform'))).toBe('darwin'); expect(await page.evaluate(() => window.electron.ipcRenderer.invoke('window:isMaximized'))).toBe(false); await page.evaluate(() => window.electron.ipcRenderer.invoke('window:minimize')); await expect.poll(async () => (await mainWindowState(app))?.minimized).toBe(true); await app.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0].restore(); }); await expect.poll(async () => (await mainWindowState(app))?.minimized).toBe(false); await page.evaluate(() => window.electron.ipcRenderer.invoke('window:maximize')); await expect.poll(async () => (await mainWindowState(app))?.maximized).toBe(true); await page.evaluate(() => window.electron.ipcRenderer.invoke('window:maximize')); await expect.poll(async () => (await mainWindowState(app))?.maximized).toBe(false); // Hold only this renderer's test lease while inspecting a preview outside // the Code screen; closing the renderer must release it automatically. await page.evaluate(async () => { await window.electron.ipcRenderer.invoke('lifecycle:lease', { id: 'window-lifecycle-preview', kind: 'test-preview', active: true, }); const state = window as typeof window & { __windowLifecycleBrowserEvents?: unknown[] }; state.__windowLifecycleBrowserEvents = []; window.electron.ipcRenderer.on('agent-browser:show', (...args) => { state.__windowLifecycleBrowserEvents!.push(args.at(-1)); }); }); const result = await hostRequest<{ browser: AgentBrowserSnapshot }>(page, '/api/agent-browser/open', { project_id: project.id, url, visible: true, bounds: { x: 20, y: 70, width: 420, height: 300 }, }); expect(result.browser).toMatchObject({ state: 'attached', visible: true, url }); await expect.poll(async () => await page.evaluate(() => ( window as typeof window & { __windowLifecycleBrowserEvents?: unknown[] } ).__windowLifecycleBrowserEvents?.length)).toBeGreaterThan(0); const previewId = await app.evaluate(({ BrowserWindow, WebContentsView }, previewUrl) => { const win = BrowserWindow.getAllWindows()[0]; const view = win.contentView.children.find((child) => ( child instanceof WebContentsView && child.webContents.getURL() === previewUrl )); if (!(view instanceof WebContentsView)) throw new Error('Preview was not attached to the current window'); if (!view.getVisible()) throw new Error('Attached preview was not visible'); return view.webContents.id; }, url); if (cycle === 2) break; const closed = page.waitForEvent('close'); if (cycle === 0) { // BrowserWindow.close follows the native red traffic-light close path. await app.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0].close(); }); } else { await page.evaluate(() => { void window.electron.ipcRenderer.invoke('window:close').catch(() => undefined); }); } await closed; await expect.poll(async () => await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows().length)).toBe(0); await expect.poll(async () => await app.evaluate(({ webContents }, id) => Boolean(webContents.fromId(id)), previewId)).toBe(false); expect(app.process().exitCode).toBeNull(); page = await reopenMainWindow(app); } await quitApplication(app); } finally { server.closeAllConnections(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); test('macOS leaves native fullscreen before closing and can reopen and quit normally', async ({ launchElectronApp }) => { test.skip(process.platform !== 'darwin', 'Native macOS fullscreen transition'); const app = await launchElectronApp({ skipSetup: true }); const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await app.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; const entered = new Promise((resolve) => { win.once('enter-full-screen', () => resolve()); }); win.setFullScreen(true); await entered; }); expect((await mainWindowState(app))?.fullScreen).toBe(true); const events = await app.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; const sequence: string[] = []; return await new Promise((resolve) => { // Record the native event before the production listener is allowed to // synchronously finish closing the window in response to it. win.prependOnceListener('leave-full-screen', () => { sequence.push('leave-full-screen'); }); win.once('closed', () => { sequence.push('closed'); resolve(sequence); }); win.close(); }); }); expect(events).toEqual(['leave-full-screen', 'closed']); await expect.poll(async () => await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows().length)).toBe(0); expect(app.process().exitCode).toBeNull(); await reopenMainWindow(app); expect(await mainWindowState(app)).toMatchObject({ fullScreen: false, minimized: false, visible: true }); await quitApplication(app); });