feat(electron): expose app process(), detach on exit (#13280)

This commit is contained in:
Pavel Feldman 2022-04-04 10:50:46 -08:00 committed by GitHub
parent 3636d8548f
commit 8232497c88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 46 additions and 3 deletions

View file

@ -112,6 +112,11 @@ Typically your script will start with:
// ... // ...
``` ```
## method: ElectronApplication.process
- returns: <[ChildProcess]>
Returns the main process for this Electron Application.
## async method: ElectronApplication.waitForEvent ## async method: ElectronApplication.waitForEvent
- returns: <[any]> - returns: <[any]>

View file

@ -135,6 +135,10 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
} }
} }
_toImpl(): any {
return this._connection.toImpl?.(this);
}
private toJSON() { private toJSON() {
// Jest's expect library tries to print objects sometimes. // Jest's expect library tries to print objects sometimes.
// RPC objects can contain links to lots of other objects, // RPC objects can contain links to lots of other objects,

View file

@ -67,6 +67,8 @@ export class Connection extends EventEmitter {
private _rootObject: Root; private _rootObject: Root;
private _closedErrorMessage: string | undefined; private _closedErrorMessage: string | undefined;
private _isRemote = false; private _isRemote = false;
// Some connections allow resolving in-process dispatchers.
toImpl: ((client: ChannelOwner) => any) | undefined;
constructor() { constructor() {
super(); super();

View file

@ -15,6 +15,7 @@
*/ */
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import * as childProcess from 'child_process';
import * as structs from '../../types/structs'; import * as structs from '../../types/structs';
import * as api from '../../types/types'; import * as api from '../../types/types';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
@ -75,6 +76,10 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
this._channel.on('close', () => this.emit(Events.ElectronApplication.Close)); this._channel.on('close', () => this.emit(Events.ElectronApplication.Close));
} }
process(): childProcess.ChildProcess {
return this._toImpl().process();
}
_onPage(page: Page) { _onPage(page: Page) {
this._windows.add(page); this._windows.add(page);
this.emit(Events.ElectronApplication.Window, page); this.emit(Events.ElectronApplication.Window, page);

View file

@ -44,6 +44,7 @@ export function createInProcessPlaywright(): PlaywrightAPI {
dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message)); dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message));
clientConnection.onmessage = message => setImmediate(() => dispatcherConnection.dispatch(message)); clientConnection.onmessage = message => setImmediate(() => dispatcherConnection.dispatch(message));
(playwrightAPI as any)._toImpl = (x: any) => dispatcherConnection._dispatchers.get(x._guid)!._object; clientConnection.toImpl = (x: any) => dispatcherConnection._dispatchers.get(x._guid)!._object;
(playwrightAPI as any)._toImpl = clientConnection.toImpl;
return playwrightAPI; return playwrightAPI;
} }

View file

@ -52,9 +52,11 @@ export class ElectronApplication extends SdkObject {
private _nodeExecutionContext: js.ExecutionContext | undefined; private _nodeExecutionContext: js.ExecutionContext | undefined;
_nodeElectronHandlePromise: Promise<js.JSHandle<any>>; _nodeElectronHandlePromise: Promise<js.JSHandle<any>>;
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
private _process: childProcess.ChildProcess;
constructor(parent: SdkObject, browser: CRBrowser, nodeConnection: CRConnection) { constructor(parent: SdkObject, browser: CRBrowser, nodeConnection: CRConnection, process: childProcess.ChildProcess) {
super(parent, 'electron-app'); super(parent, 'electron-app');
this._process = process;
this._browserContext = browser._defaultContext as CRBrowserContext; this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(BrowserContext.Events.Close, () => { this._browserContext.on(BrowserContext.Events.Close, () => {
// Emit application closed after context closed. // Emit application closed after context closed.
@ -77,6 +79,10 @@ export class ElectronApplication extends SdkObject {
this._nodeSession.send('Runtime.enable', {}).catch(e => {}); this._nodeSession.send('Runtime.enable', {}).catch(e => {});
} }
process(): childProcess.ChildProcess {
return this._process;
}
context(): BrowserContext { context(): BrowserContext {
return this._browserContext; return this._browserContext;
} }
@ -166,6 +172,10 @@ export class Electron extends SdkObject {
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger(), browserLogsCollector); const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger(), browserLogsCollector);
// Immediately release exiting process under debug.
waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./).then(() => {
nodeTransport.close();
}).catch(() => {});
const chromeMatch = await Promise.race([ const chromeMatch = await Promise.race([
waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/), waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/),
waitForXserverError, waitForXserverError,
@ -196,7 +206,7 @@ export class Electron extends SdkObject {
}; };
validateBrowserContextOptions(contextOptions, browserOptions); validateBrowserContextOptions(contextOptions, browserOptions);
const browser = await CRBrowser.connect(chromeTransport, browserOptions); const browser = await CRBrowser.connect(chromeTransport, browserOptions);
app = new ElectronApplication(this, browser, nodeConnection); app = new ElectronApplication(this, browser, nodeConnection, launchedProcess);
return app; return app;
}, TimeoutSettings.timeout(options)); }, TimeoutSettings.timeout(options));
} }

View file

@ -10921,6 +10921,11 @@ export interface ElectronApplication {
*/ */
firstWindow(): Promise<Page>; firstWindow(): Promise<Page>;
/**
* Returns the main process for this Electron Application.
*/
process(): ChildProcess;
/** /**
* This event is issued when the application closes. * This event is issued when the application closes.
*/ */

View file

@ -180,3 +180,14 @@ test('should record video', async ({ playwright }, testInfo) => {
const videoPath = await page.video().path(); const videoPath = await page.video().path();
expect(fs.statSync(videoPath).size).toBeGreaterThan(0); expect(fs.statSync(videoPath).size).toBeGreaterThan(0);
}); });
test('should detach debugger on app-initiated exit', async ({ playwright }) => {
const electronApp = await playwright._electron.launch({
args: [path.join(__dirname, 'electron-app.js')],
});
const closePromise = new Promise(f => electronApp.process().on('close', f));
await electronApp.evaluate(({ app }) => {
app.quit();
});
await closePromise;
});