diff --git a/apps/electron-backend-e2e/src/electron-unautomated-launch.ts b/apps/electron-backend-e2e/src/electron-unautomated-launch.ts index 4154adba2..ee29ff715 100644 --- a/apps/electron-backend-e2e/src/electron-unautomated-launch.ts +++ b/apps/electron-backend-e2e/src/electron-unautomated-launch.ts @@ -26,6 +26,13 @@ interface CdpSocket { close(): void; } +const EXIT_WAIT_MS = 5_000; +/** + * `electron` for main-process evaluation. `process.mainModule` exists only + * when the app entry is CommonJS; a require created from the core `module` + * builtin resolves Electron's built-in module either way. + */ +const MAIN_PROCESS_ELECTRON = `process.getBuiltinModule('node:module').createRequire(process.execPath)('electron')`; const DEVTOOLS_PATTERN = /DevTools listening on (ws:\/\/[^\s]+)/; const INSPECTOR_PATTERN = /Debugger listening on (ws:\/\/[^\s]+)/; const STARTUP_TIMEOUT_MS = 30_000; @@ -115,6 +122,12 @@ async function evaluate(socket: CdpSocket, expression: string): Promise { awaitPromise: true, returnByValue: true, }); + const protocolError = response['error'] as { message?: string } | undefined; + if (protocolError) { + throw new Error( + `CDP Runtime.evaluate: ${protocolError.message ?? 'failed'}` + ); + } const result = response['result'] as { result?: { value?: T }; exceptionDetails?: { text?: string }; @@ -125,6 +138,40 @@ async function evaluate(socket: CdpSocket, expression: string): Promise { return result?.result?.value as T; } +function waitForExit( + child: ChildProcess, + exited: Promise, + timeoutMs: number +): Promise { + if (child.exitCode !== null || child.signalCode !== null) { + return Promise.resolve(true); + } + let timer: ReturnType | undefined; + return Promise.race([ + exited.then(() => true), + new Promise((resolve) => { + timer = setTimeout(() => resolve(false), timeoutMs); + }), + ]).finally(() => clearTimeout(timer)); +} + +/** + * Bounded like `closeElectronApplicationAndConfirmExit`: a stuck Electron + * must neither stall the worker nor keep holding the test profile. + */ +async function stopElectron( + child: ChildProcess, + exited: Promise +): Promise { + for (const signal of ['SIGTERM', 'SIGKILL'] as const) { + terminateElectronProcess(child, signal); + if (await waitForExit(child, exited, EXIT_WAIT_MS)) return; + } + throw new Error( + `Electron (pid ${child.pid}) did not exit after SIGTERM and SIGKILL` + ); +} + export async function launchUnautomatedElectronApp( dataDir: string ): Promise { @@ -154,19 +201,17 @@ export async function launchUnautomatedElectronApp( evaluateInMain: (body) => evaluate( main, - `(async (electron) => { ${body} })(process.mainModule.require('electron'))` + `(async (electron) => { ${body} })(${MAIN_PROCESS_ELECTRON})` ), evaluateInPage: (expression) => evaluate(page, expression), close: async () => { page.close(); main.close(); - terminateElectronProcess(child); - await exited; + await stopElectron(child, exited); }, }; } catch (error) { - terminateElectronProcess(child, 'SIGKILL'); - await exited; + await stopElectron(child, exited); throw error; } } diff --git a/apps/electron-backend-e2e/src/window-visibility.e2e.ts b/apps/electron-backend-e2e/src/window-visibility.e2e.ts index 254e57b3e..5810607c1 100644 --- a/apps/electron-backend-e2e/src/window-visibility.e2e.ts +++ b/apps/electron-backend-e2e/src/window-visibility.e2e.ts @@ -20,6 +20,23 @@ const setWindowState = ( `electron.BrowserWindow.getAllWindows()[0].${action}();` ); +/** + * The app creates its window with `show: false` and shows it on + * `ready-to-show`; a `hide()` sent earlier would be undone by that `show()`. + */ +async function waitUntilShown(app: UnautomatedElectronApp): Promise { + await expect + .poll( + () => + app.evaluateInMain( + 'return electron.BrowserWindow.getAllWindows()[0]?.isVisible() ?? false;' + ), + { timeout: 30_000 } + ) + .toBe(true); + await expect.poll(() => visibility(app)).toBe('visible'); +} + /** * The renderer must see a hidden or minimized window as hidden: idle timers * pause on `visibilitychange`, and the playback keep-awake gate releases the @@ -35,7 +52,7 @@ test.describe('Main window visibility', () => { }) => { const app = await launchUnautomatedElectronApp(dataDir); try { - await expect.poll(() => visibility(app)).toBe('visible'); + await waitUntilShown(app); await setWindowState(app, 'hide'); await expect @@ -60,7 +77,7 @@ test.describe('Main window visibility', () => { ); const app = await launchUnautomatedElectronApp(dataDir); try { - await expect.poll(() => visibility(app)).toBe('visible'); + await waitUntilShown(app); await setWindowState(app, 'minimize'); await expect