Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/src/electron-api/class-electronapplication.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ Page to retrieve the window for.

Closes Electron application.

### option: ElectronApplication.close.timeout
* since: v1.52
- `timeout` ?<[float]>

Maximum time in milliseconds to wait for the application to close gracefully. If the timeout is exceeded, the
application process is forcefully terminated. Pass `0` to disable timeout (default). When no timeout is specified,
`close()` waits indefinitely for the application to exit.

## method: ElectronApplication.context
* since: v1.9
- returns: <[BrowserContext]>
Expand Down
10 changes: 9 additions & 1 deletion packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17076,8 +17076,16 @@ export interface ElectronApplication {

/**
* Closes Electron application.
* @param options
*/
close(): Promise<void>;
close(options?: {
/**
* Maximum time in milliseconds to wait for the application to close gracefully. If the timeout is exceeded, the
* application process is forcefully terminated. Pass `0` to disable timeout (default). When no timeout is specified,
* `close()` waits indefinitely for the application to exit.
*/
timeout?: number;
}): Promise<void>;

/**
* This method returns browser context that can be used for setting up context-wide routing, etc.
Expand Down
20 changes: 16 additions & 4 deletions packages/playwright-core/src/electron/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export class Electron implements api.Electron {
const chromeMatch = await Promise.race([chromeMatchPromise, waitForXserverError]);
const browser = await chromium.connectOverCDP(chromeMatch[1], { timeout: progress.timeUntilDeadline(), isLocal: true });

app = new ElectronApplication(worker, browser, launchedProcess);
app = new ElectronApplication(worker, browser, launchedProcess, kill);
await progress.race(app._initialize());
return app;
} catch (error) {
Expand All @@ -198,8 +198,9 @@ export class ElectronApplication extends EventEmitter implements api.ElectronApp
private _windows = new Map<Page, JSHandle<BrowserWindow> | undefined>();
private _appHandlePromise = new ManualPromise<JSHandle<ElectronAppType>>();
private _closedPromise: Promise<void> | undefined;
private _kill: () => Promise<void>;

constructor(worker: Worker, browser: Browser, process: childProcess.ChildProcess) {
constructor(worker: Worker, browser: Browser, process: childProcess.ChildProcess, kill: () => Promise<void>) {
super();

this._worker = worker;
Expand All @@ -214,6 +215,7 @@ export class ElectronApplication extends EventEmitter implements api.ElectronApp
this._context.close = () => this.close();

this._process = process;
this._kill = kill;
}

_onClose() {
Expand Down Expand Up @@ -249,15 +251,25 @@ export class ElectronApplication extends EventEmitter implements api.ElectronApp
await this.close();
}

async close() {
async close(options?: { timeout?: number }) {
if (!this._closedPromise) {
this._closedPromise = new Promise<void>(f => this.once(Events.ElectronApplication.Close, f));
await this._browser.close();
const appHandle = await this._appHandlePromise;
await appHandle.evaluate(({ app }) => app.quit()).catch(() => {});
await this._worker._disconnect();
}
await this._closedPromise;

if (options?.timeout) {
const timer = setTimeout(() => this._kill().catch(() => {}), options.timeout);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My agent did a better job than yours - you are not awaiting kill here. Closing in favor of #40613.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's rude and funny at the same time...

try {
await this._closedPromise;
} finally {
clearTimeout(timer);
}
} else {
await this._closedPromise;
}
}

async waitForEvent(event: string, optionsOrPredicate: Function | { timeout?: number, predicate?: Function } = {}): Promise<any> {
Expand Down
10 changes: 9 additions & 1 deletion packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17076,8 +17076,16 @@ export interface ElectronApplication {

/**
* Closes Electron application.
* @param options
*/
close(): Promise<void>;
close(options?: {
/**
* Maximum time in milliseconds to wait for the application to close gracefully. If the timeout is exceeded, the
* application process is forcefully terminated. Pass `0` to disable timeout (default). When no timeout is specified,
* `close()` waits indefinitely for the application to exit.
*/
timeout?: number;
}): Promise<void>;

/**
* This method returns browser context that can be used for setting up context-wide routing, etc.
Expand Down
13 changes: 13 additions & 0 deletions tests/electron/electron-app-hang-on-close.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const assert = require('node:assert/strict');
const { app } = require('electron');

assert(process.env.PWTEST_ELECTRON_USER_DATA_DIR, 'PWTEST_ELECTRON_USER_DATA_DIR env var is not set');
app.setPath('appData', process.env.PWTEST_ELECTRON_USER_DATA_DIR);

app.on('window-all-closed', e => e.preventDefault());

// Prevent quit — simulates an app that hangs on close
// (e.g. due to IPC handlers, child processes, or beforeunload).
app.on('before-quit', e => {
e.preventDefault();
});
27 changes: 27 additions & 0 deletions tests/electron/electron-app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,33 @@ test('should detach debugger on app-initiated exit', async ({ launchElectronApp
await closePromise;
});

test('should force-kill app when close timeout is exceeded', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp('electron-app-hang-on-close.js');
const events: string[] = [];
electronApp.on('close', () => events.push('application(close)'));
electronApp.process().on('exit', () => events.push('process(exit)'));

// This app prevents quit via before-quit handler, so close() without
// timeout would hang indefinitely. With a timeout, it should force-kill.
await electronApp.close({ timeout: 3000 });

events.sort();
expect(events).toEqual(['application(close)', 'process(exit)']);
});

test('should not force-kill when app closes within timeout', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp('electron-app.js');
const events: string[] = [];
electronApp.on('close', () => events.push('application(close)'));
electronApp.process().on('exit', () => events.push('process(exit)'));

// Normal app should close well within the timeout.
await electronApp.close({ timeout: 30000 });

events.sort();
expect(events).toEqual(['application(close)', 'process(exit)']);
});

test('should run pre-ready apis', async ({ launchElectronApp }) => {
await launchElectronApp('electron-app-pre-ready.js');
});
Expand Down