fix(electron): stalling on delayed process close (#29431)

This commit is contained in:
Max Schmitt 2024-02-13 10:25:46 +01:00 committed by GitHub
parent f605a5009b
commit 30557ed28c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 20 deletions

View File

@ -39,7 +39,7 @@ const { _electron: electron } = require('playwright');
## event: ElectronApplication.close ## event: ElectronApplication.close
* since: v1.9 * since: v1.9
This event is issued when the application closes. This event is issued when the application process has been terminated.
## event: ElectronApplication.console ## event: ElectronApplication.console
* since: v1.42 * since: v1.42

View File

@ -30,7 +30,8 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
import { ManualPromise, wrapInASCIIBox } from '../../utils'; import { ManualPromise, wrapInASCIIBox } from '../../utils';
import { WebSocketTransport } from '../transport'; import { WebSocketTransport } from '../transport';
import { launchProcess, envArrayToObject } from '../../utils/processLauncher'; import { launchProcess, envArrayToObject } from '../../utils/processLauncher';
import { BrowserContext, validateBrowserContextOptions } from '../browserContext'; import type { BrowserContext } from '../browserContext';
import { validateBrowserContextOptions } from '../browserContext';
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import type { Progress } from '../progress'; import type { Progress } from '../progress';
import { ProgressController } from '../progress'; import { ProgressController } from '../progress';
@ -66,10 +67,6 @@ export class ElectronApplication extends SdkObject {
super(parent, 'electron-app'); super(parent, 'electron-app');
this._process = process; this._process = process;
this._browserContext = browser._defaultContext as CRBrowserContext; this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(BrowserContext.Events.Close, () => {
// Emit application closed after context closed.
Promise.resolve().then(() => this.emit(ElectronApplication.Events.Close));
});
this._nodeConnection = nodeConnection; this._nodeConnection = nodeConnection;
this._nodeSession = nodeConnection.rootSession; this._nodeSession = nodeConnection.rootSession;
this._nodeSession.on('Runtime.executionContextCreated', async (event: Protocol.Runtime.executionContextCreatedPayload) => { this._nodeSession.on('Runtime.executionContextCreated', async (event: Protocol.Runtime.executionContextCreatedPayload) => {
@ -86,10 +83,13 @@ export class ElectronApplication extends SdkObject {
this._nodeElectronHandlePromise.resolve(new js.JSHandle(this._nodeExecutionContext!, 'object', 'ElectronModule', remoteObject.objectId!)); this._nodeElectronHandlePromise.resolve(new js.JSHandle(this._nodeExecutionContext!, 'object', 'ElectronModule', remoteObject.objectId!));
}); });
this._nodeSession.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); this._nodeSession.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
const appClosePromise = new Promise(f => this.once(ElectronApplication.Events.Close, f));
this._browserContext.setCustomCloseHandler(async () => { this._browserContext.setCustomCloseHandler(async () => {
await this._browserContext.stopVideoRecording(); await this._browserContext.stopVideoRecording();
const electronHandle = await this._nodeElectronHandlePromise; const electronHandle = await this._nodeElectronHandlePromise;
await electronHandle.evaluate(({ app }) => app.quit()).catch(() => {}); await electronHandle.evaluate(({ app }) => app.quit()).catch(() => {});
this._nodeConnection.close();
await appClosePromise;
}); });
} }
@ -132,11 +132,8 @@ export class ElectronApplication extends SdkObject {
} }
async close() { async close() {
const progressController = new ProgressController(serverSideCallMetadata(), this); // This will call BrowserContext.setCustomCloseHandler.
const closed = progressController.run(progress => helper.waitForEvent(progress, this, ElectronApplication.Events.Close).promise);
await this._browserContext.close({ reason: 'Application exited' }); await this._browserContext.close({ reason: 'Application exited' });
this._nodeConnection.close();
await closed;
} }
async browserWindow(page: Page): Promise<js.JSHandle<BrowserWindow>> { async browserWindow(page: Page): Promise<js.JSHandle<BrowserWindow>> {
@ -218,7 +215,7 @@ export class Electron extends SdkObject {
handleSIGINT: true, handleSIGINT: true,
handleSIGTERM: true, handleSIGTERM: true,
handleSIGHUP: true, handleSIGHUP: true,
onExit: () => {}, onExit: () => app?.emit(ElectronApplication.Events.Close),
}); });
const waitForXserverError = new Promise(async (resolve, reject) => { const waitForXserverError = new Promise(async (resolve, reject) => {

View File

@ -13939,7 +13939,7 @@ export interface ElectronApplication {
*/ */
evaluateHandle<R>(pageFunction: PageFunctionOn<ElectronType, void, R>, arg?: any): Promise<SmartHandle<R>>; evaluateHandle<R>(pageFunction: PageFunctionOn<ElectronType, void, R>, arg?: any): Promise<SmartHandle<R>>;
/** /**
* This event is issued when the application closes. * This event is issued when the application process has been terminated.
*/ */
on(event: 'close', listener: () => void): this; on(event: 'close', listener: () => void): this;
@ -13986,7 +13986,7 @@ export interface ElectronApplication {
once(event: 'window', listener: (page: Page) => void): this; once(event: 'window', listener: (page: Page) => void): this;
/** /**
* This event is issued when the application closes. * This event is issued when the application process has been terminated.
*/ */
addListener(event: 'close', listener: () => void): this; addListener(event: 'close', listener: () => void): this;
@ -14048,7 +14048,7 @@ export interface ElectronApplication {
off(event: 'window', listener: (page: Page) => void): this; off(event: 'window', listener: (page: Page) => void): this;
/** /**
* This event is issued when the application closes. * This event is issued when the application process has been terminated.
*/ */
prependListener(event: 'close', listener: () => void): this; prependListener(event: 'close', listener: () => void): this;
@ -14125,7 +14125,7 @@ export interface ElectronApplication {
process(): ChildProcess; process(): ChildProcess;
/** /**
* This event is issued when the application closes. * This event is issued when the application process has been terminated.
*/ */
waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: () => boolean | Promise<boolean>, timeout?: number } | (() => boolean | Promise<boolean>)): Promise<void>; waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: () => boolean | Promise<boolean>, timeout?: number } | (() => boolean | Promise<boolean>)): Promise<void>;

View File

@ -20,16 +20,51 @@ import fs from 'fs';
import { electronTest as test, expect } from './electronTest'; import { electronTest as test, expect } from './electronTest';
import type { ConsoleMessage } from 'playwright'; import type { ConsoleMessage } from 'playwright';
test('should fire close event', async ({ launchElectronApp }) => { test('should fire close event via ElectronApplication.close();', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp('electron-app.js'); const electronApp = await launchElectronApp('electron-app.js');
const events = []; const events = [];
electronApp.on('close', () => events.push('application')); electronApp.on('close', () => events.push('application(close)'));
electronApp.context().on('close', () => events.push('context')); electronApp.context().on('close', () => events.push('context(close)'));
electronApp.process().on('exit', () => events.push('process(exit)'));
await electronApp.close(); await electronApp.close();
expect(events.join('|')).toBe('context|application'); // Close one more time - this should be a noop.
await electronApp.close();
expect(events.join('|')).toBe('process(exit)|context(close)|application(close)');
// Give it some time to fire more events - there should not be any. // Give it some time to fire more events - there should not be any.
await new Promise(f => setTimeout(f, 1000)); await new Promise(f => setTimeout(f, 1000));
expect(events.join('|')).toBe('context|application'); expect(events.join('|')).toBe('process(exit)|context(close)|application(close)');
});
test('should fire close event via BrowserContext.close()', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp('electron-app.js');
const events = [];
electronApp.on('close', () => events.push('application(close)'));
electronApp.context().on('close', () => events.push('context(close)'));
electronApp.process().on('exit', () => events.push('process(exit)'));
await electronApp.context().close();
// Close one more time - this should be a noop.
await electronApp.context().close();
expect(events.join('|')).toBe('process(exit)|context(close)|application(close)');
// Give it some time to fire more events - there should not be any.
await new Promise(f => setTimeout(f, 1000));
expect(events.join('|')).toBe('process(exit)|context(close)|application(close)');
});
test('should fire close event when the app quits itself', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp('electron-app.js');
const events = [];
electronApp.on('close', () => events.push('application(close)'));
electronApp.context().on('close', () => events.push('context(close)'));
electronApp.process().on('exit', () => events.push('process(exit)'));
{
const waitForAppClose = new Promise<void>(f => electronApp.on('close', f));
await electronApp.evaluate(({ app }) => app.quit());
await waitForAppClose;
}
expect(events.join('|')).toBe('process(exit)|context(close)|application(close)');
// Give it some time to fire more events - there should not be any.
await new Promise(f => setTimeout(f, 1000));
expect(events.join('|')).toBe('process(exit)|context(close)|application(close)');
}); });
test('should fire console events', async ({ launchElectronApp }) => { test('should fire console events', async ({ launchElectronApp }) => {