Skip to content

Commit 1a859eb

Browse files
authored
chore(electron): fix node/browser race conditions, expose browser window asynchronously (#6381)
1 parent 6da7e70 commit 1a859eb

File tree

14 files changed

+121
-81
lines changed

14 files changed

+121
-81
lines changed

docs/src/api/class-electronapplication.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ This event is issued when the application closes.
4545
This event is issued for every window that is created **and loaded** in Electron. It contains a [Page] that can
4646
be used for Playwright automation.
4747

48+
## async method: ElectronApplication.browserWindow
49+
- returns: <[JSHandle]>
50+
51+
Returns the BrowserWindow object that corresponds to the given Playwright page.
52+
53+
### param: ElectronApplication.browserWindow.page
54+
- `page` <[Page]>
55+
56+
Page to retrieve the window for.
57+
4858
## async method: ElectronApplication.close
4959

5060
Closes Electron application.

src/client/electron.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import type { BrowserWindow } from 'electron';
1718
import * as structs from '../../types/structs';
1819
import * as api from '../../types/types';
1920
import * as channels from '../protocol/channels';
@@ -55,7 +56,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel, channels.El
5556
}
5657

5758
export class ElectronApplication extends ChannelOwner<channels.ElectronApplicationChannel, channels.ElectronApplicationInitializer> implements api.ElectronApplication {
58-
private _context?: BrowserContext;
59+
private _context: BrowserContext;
5960
private _windows = new Set<Page>();
6061
private _timeoutSettings = new TimeoutSettings();
6162

@@ -65,13 +66,11 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
6566

6667
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ElectronApplicationInitializer) {
6768
super(parent, type, guid, initializer);
68-
this._channel.on('context', ({ context }) => this._context = BrowserContext.from(context));
69-
this._channel.on('window', ({ page, browserWindow }) => {
70-
const window = Page.from(page);
71-
(window as any).browserWindow = JSHandle.from(browserWindow);
72-
this._windows.add(window);
73-
this.emit(Events.ElectronApplication.Window, window);
74-
window.once(Events.Page.Close, () => this._windows.delete(window));
69+
this._context = BrowserContext.from(initializer.context);
70+
this._context.on(Events.BrowserContext.Page, page => {
71+
this._windows.add(page);
72+
this.emit(Events.ElectronApplication.Window, page);
73+
page.once(Events.Page.Close, () => this._windows.delete(page));
7574
});
7675
this._channel.on('close', () => this.emit(Events.ElectronApplication.Close));
7776
}
@@ -109,6 +108,13 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
109108
return result;
110109
}
111110

111+
async browserWindow(page: Page): Promise<JSHandle<BrowserWindow>> {
112+
return this._wrapApiCall('electronApplication.browserWindow', async (channel: channels.ElectronApplicationChannel) => {
113+
const result = await channel.browserWindow({ page: page._channel });
114+
return JSHandle.from(result.handle);
115+
});
116+
}
117+
112118
async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<ElectronAppType, Arg, R>, arg: Arg): Promise<R> {
113119
return this._wrapApiCall('electronApplication.evaluate', async (channel: channels.ElectronApplicationChannel) => {
114120
const result = await channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });

src/dispatchers/electronDispatcher.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
18-
import { Electron, ElectronApplication, ElectronPage } from '../server/electron/electron';
17+
import { Dispatcher, DispatcherScope } from './dispatcher';
18+
import { Electron, ElectronApplication } from '../server/electron/electron';
1919
import * as channels from '../protocol/channels';
2020
import { BrowserContextDispatcher } from './browserContextDispatcher';
2121
import { PageDispatcher } from './pageDispatcher';
@@ -35,27 +35,27 @@ export class ElectronDispatcher extends Dispatcher<Electron, channels.ElectronIn
3535

3636
export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplication, channels.ElectronApplicationInitializer> implements channels.ElectronApplicationChannel {
3737
constructor(scope: DispatcherScope, electronApplication: ElectronApplication) {
38-
super(scope, electronApplication, 'ElectronApplication', {}, true);
39-
this._dispatchEvent('context', { context: new BrowserContextDispatcher(this._scope, electronApplication.context()) });
38+
super(scope, electronApplication, 'ElectronApplication', {
39+
context: new BrowserContextDispatcher(scope, electronApplication.context())
40+
}, true);
4041
electronApplication.on(ElectronApplication.Events.Close, () => {
4142
this._dispatchEvent('close');
4243
this._dispose();
4344
});
44-
electronApplication.on(ElectronApplication.Events.Window, (page: ElectronPage) => {
45-
this._dispatchEvent('window', {
46-
page: lookupDispatcher<PageDispatcher>(page),
47-
browserWindow: ElementHandleDispatcher.fromJSHandle(this._scope, page.browserWindow),
48-
});
49-
});
45+
}
46+
47+
async browserWindow(params: channels.ElectronApplicationBrowserWindowParams): Promise<channels.ElectronApplicationBrowserWindowResult> {
48+
const handle = await this._object.browserWindow((params.page as PageDispatcher).page());
49+
return { handle: ElementHandleDispatcher.fromJSHandle(this._scope, handle) };
5050
}
5151

5252
async evaluateExpression(params: channels.ElectronApplicationEvaluateExpressionParams): Promise<channels.ElectronApplicationEvaluateExpressionResult> {
53-
const handle = await this._object._nodeElectronHandlePromised;
53+
const handle = await this._object._nodeElectronHandlePromise;
5454
return { value: serializeResult(await handle.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) };
5555
}
5656

5757
async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams): Promise<channels.ElectronApplicationEvaluateExpressionHandleResult> {
58-
const handle = await this._object._nodeElectronHandlePromised;
58+
const handle = await this._object._nodeElectronHandlePromise;
5959
const result = await handle.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg));
6060
return { handle: ElementHandleDispatcher.fromJSHandle(this._scope, result) };
6161
}

src/dispatchers/pageDispatcher.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
9393
this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(page._video) });
9494
}
9595

96+
page(): Page {
97+
return this._page;
98+
}
99+
96100
async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams, metadata: CallMetadata): Promise<void> {
97101
this._page.setDefaultNavigationTimeout(params.timeout);
98102
}

src/protocol/channels.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2564,22 +2564,25 @@ export type ElectronLaunchResult = {
25642564
};
25652565

25662566
// ----------- ElectronApplication -----------
2567-
export type ElectronApplicationInitializer = {};
2567+
export type ElectronApplicationInitializer = {
2568+
context: BrowserContextChannel,
2569+
};
25682570
export interface ElectronApplicationChannel extends Channel {
2569-
on(event: 'context', callback: (params: ElectronApplicationContextEvent) => void): this;
25702571
on(event: 'close', callback: (params: ElectronApplicationCloseEvent) => void): this;
2571-
on(event: 'window', callback: (params: ElectronApplicationWindowEvent) => void): this;
2572+
browserWindow(params: ElectronApplicationBrowserWindowParams, metadata?: Metadata): Promise<ElectronApplicationBrowserWindowResult>;
25722573
evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, metadata?: Metadata): Promise<ElectronApplicationEvaluateExpressionResult>;
25732574
evaluateExpressionHandle(params: ElectronApplicationEvaluateExpressionHandleParams, metadata?: Metadata): Promise<ElectronApplicationEvaluateExpressionHandleResult>;
25742575
close(params?: ElectronApplicationCloseParams, metadata?: Metadata): Promise<ElectronApplicationCloseResult>;
25752576
}
2576-
export type ElectronApplicationContextEvent = {
2577-
context: BrowserContextChannel,
2578-
};
25792577
export type ElectronApplicationCloseEvent = {};
2580-
export type ElectronApplicationWindowEvent = {
2578+
export type ElectronApplicationBrowserWindowParams = {
25812579
page: PageChannel,
2582-
browserWindow: JSHandleChannel,
2580+
};
2581+
export type ElectronApplicationBrowserWindowOptions = {
2582+
2583+
};
2584+
export type ElectronApplicationBrowserWindowResult = {
2585+
handle: JSHandleChannel,
25832586
};
25842587
export type ElectronApplicationEvaluateExpressionParams = {
25852588
expression: string,

src/protocol/protocol.yml

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,8 +2097,17 @@ Electron:
20972097
ElectronApplication:
20982098
type: interface
20992099

2100+
initializer:
2101+
context: BrowserContext
2102+
21002103
commands:
21012104

2105+
browserWindow:
2106+
parameters:
2107+
page: Page
2108+
returns:
2109+
handle: JSHandle
2110+
21022111
evaluateExpression:
21032112
parameters:
21042113
expression: string
@@ -2118,20 +2127,8 @@ ElectronApplication:
21182127
close:
21192128

21202129
events:
2121-
2122-
# This event happens once immediately after creation.
2123-
context:
2124-
parameters:
2125-
context: BrowserContext
2126-
21272130
close:
21282131

2129-
window:
2130-
parameters:
2131-
page: Page
2132-
browserWindow: JSHandle
2133-
2134-
21352132

21362133
Android:
21372134
type: interface

src/protocol/validator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
974974
env: tOptional(tArray(tType('NameValue'))),
975975
timeout: tOptional(tNumber),
976976
});
977+
scheme.ElectronApplicationBrowserWindowParams = tObject({
978+
page: tChannel('Page'),
979+
});
977980
scheme.ElectronApplicationEvaluateExpressionParams = tObject({
978981
expression: tString,
979982
isFunction: tOptional(tBoolean),

src/server/electron/electron.ts

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,16 @@ export type ElectronLaunchOptionsBase = {
4343
timeout?: number,
4444
};
4545

46-
export interface ElectronPage extends Page {
47-
browserWindow: js.JSHandle<BrowserWindow>;
48-
_browserWindowId: number;
49-
}
50-
5146
export class ElectronApplication extends SdkObject {
5247
static Events = {
5348
Close: 'close',
54-
Window: 'window',
5549
};
5650

5751
private _browserContext: CRBrowserContext;
5852
private _nodeConnection: CRConnection;
5953
private _nodeSession: CRSession;
6054
private _nodeExecutionContext: js.ExecutionContext | undefined;
61-
_nodeElectronHandlePromised: Promise<js.JSHandle<any>>;
62-
private _resolveNodeElectronHandle!: (handle: js.JSHandle<any>) => void;
63-
private _windows = new Set<ElectronPage>();
55+
_nodeElectronHandlePromise: Promise<js.JSHandle<any>>;
6456
private _lastWindowId = 0;
6557
readonly _timeoutSettings = new TimeoutSettings();
6658

@@ -74,28 +66,21 @@ export class ElectronApplication extends SdkObject {
7466
this._browserContext.on(BrowserContext.Events.Page, event => this._onPage(event));
7567
this._nodeConnection = nodeConnection;
7668
this._nodeSession = nodeConnection.rootSession;
77-
this._nodeElectronHandlePromised = new Promise(resolve => this._resolveNodeElectronHandle = resolve);
69+
this._nodeElectronHandlePromise = new Promise(f => {
70+
this._nodeSession.on('Runtime.executionContextCreated', async (event: any) => {
71+
if (event.context.auxData && event.context.auxData.isDefault) {
72+
this._nodeExecutionContext = new js.ExecutionContext(this, new CRExecutionContext(this._nodeSession, event.context));
73+
f(await js.evaluate(this._nodeExecutionContext, false /* returnByValue */, `process.mainModule.require('electron')`));
74+
}
75+
});
76+
});
77+
this._nodeSession.send('Runtime.enable', {}).catch(e => {});
7878
}
7979

80-
private async _onPage(page: ElectronPage) {
80+
private async _onPage(page: Page) {
8181
// Needs to be sync.
8282
const windowId = ++this._lastWindowId;
83-
page.on(Page.Events.Close, () => {
84-
if (page.browserWindow)
85-
page.browserWindow.dispose();
86-
this._windows.delete(page);
87-
});
88-
page._browserWindowId = windowId;
89-
this._windows.add(page);
90-
91-
// Below is async.
92-
const handle = await (await this._nodeElectronHandlePromised).evaluateHandle(({ BrowserWindow }, windowId) => BrowserWindow.fromId(windowId), windowId).catch(e => {});
93-
if (!handle)
94-
return;
95-
page.browserWindow = handle;
96-
const controller = new ProgressController(internalCallMetadata(), this);
97-
await controller.run(progress => page.mainFrame()._waitForLoadState(progress, 'domcontentloaded'), page._timeoutSettings.navigationTimeout({})).catch(e => {}); // can happen after detach
98-
this.emit(ElectronApplication.Events.Window, page);
83+
(page as any)._browserWindowId = windowId;
9984
}
10085

10186
context(): BrowserContext {
@@ -105,19 +90,15 @@ export class ElectronApplication extends SdkObject {
10590
async close() {
10691
const progressController = new ProgressController(internalCallMetadata(), this);
10792
const closed = progressController.run(progress => helper.waitForEvent(progress, this, ElectronApplication.Events.Close).promise, this._timeoutSettings.timeout({}));
108-
await (await this._nodeElectronHandlePromised).evaluate(({ app }) => app.quit());
93+
const electronHandle = await this._nodeElectronHandlePromise;
94+
await electronHandle.evaluate(({ app }) => app.quit());
10995
this._nodeConnection.close();
11096
await closed;
11197
}
11298

113-
async _init() {
114-
this._nodeSession.on('Runtime.executionContextCreated', (event: any) => {
115-
if (event.context.auxData && event.context.auxData.isDefault)
116-
this._nodeExecutionContext = new js.ExecutionContext(this, new CRExecutionContext(this._nodeSession, event.context));
117-
});
118-
await this._nodeSession.send('Runtime.enable', {}).catch(e => {});
119-
js.evaluate(this._nodeExecutionContext!, false /* returnByValue */, `process.mainModule.require('electron')`)
120-
.then(this._resolveNodeElectronHandle);
99+
async browserWindow(page: Page): Promise<js.JSHandle<BrowserWindow>> {
100+
const electronHandle = await this._nodeElectronHandlePromise;
101+
return await electronHandle.evaluateHandle(({ BrowserWindow }, windowId) => BrowserWindow.fromId(windowId), (page as any)._browserWindowId);
121102
}
122103
}
123104

@@ -188,7 +169,6 @@ export class Electron extends SdkObject {
188169
};
189170
const browser = await CRBrowser.connect(chromeTransport, browserOptions);
190171
app = new ElectronApplication(this, browser, nodeConnection);
191-
await app._init();
192172
return app;
193173
}, TimeoutSettings.timeout(options));
194174
}

tests/config/electron-app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
const { app, BrowserWindow } = require('electron');
1+
const { app } = require('electron');
22

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

tests/config/electron-window-app.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { app, BrowserWindow } = require('electron');
2+
3+
app.whenReady().then(() => {
4+
const win = new BrowserWindow({
5+
width: 800,
6+
height: 600,
7+
});
8+
win.loadURL('about:blank');
9+
})
10+
11+
app.on('window-all-closed', e => e.preventDefault());

0 commit comments

Comments
 (0)