Skip to content
Merged
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
10 changes: 10 additions & 0 deletions docs/src/api/class-electronapplication.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ This event is issued when the application closes.
This event is issued for every window that is created **and loaded** in Electron. It contains a [Page] that can
be used for Playwright automation.

## async method: ElectronApplication.browserWindow
- returns: <[JSHandle]>

Returns the BrowserWindow object that corresponds to the given Playwright page.

### param: ElectronApplication.browserWindow.page
- `page` <[Page]>

Page to retrieve the window for.

## async method: ElectronApplication.close

Closes Electron application.
Expand Down
22 changes: 14 additions & 8 deletions src/client/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import type { BrowserWindow } from 'electron';
import * as structs from '../../types/structs';
import * as api from '../../types/types';
import * as channels from '../protocol/channels';
Expand Down Expand Up @@ -55,7 +56,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel, channels.El
}

export class ElectronApplication extends ChannelOwner<channels.ElectronApplicationChannel, channels.ElectronApplicationInitializer> implements api.ElectronApplication {
private _context?: BrowserContext;
private _context: BrowserContext;
private _windows = new Set<Page>();
private _timeoutSettings = new TimeoutSettings();

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

constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ElectronApplicationInitializer) {
super(parent, type, guid, initializer);
this._channel.on('context', ({ context }) => this._context = BrowserContext.from(context));
this._channel.on('window', ({ page, browserWindow }) => {
const window = Page.from(page);
(window as any).browserWindow = JSHandle.from(browserWindow);
this._windows.add(window);
this.emit(Events.ElectronApplication.Window, window);
window.once(Events.Page.Close, () => this._windows.delete(window));
this._context = BrowserContext.from(initializer.context);
this._context.on(Events.BrowserContext.Page, page => {
this._windows.add(page);
this.emit(Events.ElectronApplication.Window, page);
page.once(Events.Page.Close, () => this._windows.delete(page));
});
this._channel.on('close', () => this.emit(Events.ElectronApplication.Close));
}
Expand Down Expand Up @@ -109,6 +108,13 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
return result;
}

async browserWindow(page: Page): Promise<JSHandle<BrowserWindow>> {
return this._wrapApiCall('electronApplication.browserWindow', async (channel: channels.ElectronApplicationChannel) => {
const result = await channel.browserWindow({ page: page._channel });
Copy link
Contributor

Choose a reason for hiding this comment

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

I‘m interested that what will we get if page is actually an Electron BrowserView?

return JSHandle.from(result.handle);
});
}

async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<ElectronAppType, Arg, R>, arg: Arg): Promise<R> {
return this._wrapApiCall('electronApplication.evaluate', async (channel: channels.ElectronApplicationChannel) => {
const result = await channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
Expand Down
24 changes: 12 additions & 12 deletions src/dispatchers/electronDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/

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

export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplication, channels.ElectronApplicationInitializer> implements channels.ElectronApplicationChannel {
constructor(scope: DispatcherScope, electronApplication: ElectronApplication) {
super(scope, electronApplication, 'ElectronApplication', {}, true);
this._dispatchEvent('context', { context: new BrowserContextDispatcher(this._scope, electronApplication.context()) });
super(scope, electronApplication, 'ElectronApplication', {
context: new BrowserContextDispatcher(scope, electronApplication.context())
}, true);
electronApplication.on(ElectronApplication.Events.Close, () => {
this._dispatchEvent('close');
this._dispose();
});
electronApplication.on(ElectronApplication.Events.Window, (page: ElectronPage) => {
this._dispatchEvent('window', {
page: lookupDispatcher<PageDispatcher>(page),
browserWindow: ElementHandleDispatcher.fromJSHandle(this._scope, page.browserWindow),
});
});
}

async browserWindow(params: channels.ElectronApplicationBrowserWindowParams): Promise<channels.ElectronApplicationBrowserWindowResult> {
const handle = await this._object.browserWindow((params.page as PageDispatcher).page());
return { handle: ElementHandleDispatcher.fromJSHandle(this._scope, handle) };
}

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

async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams): Promise<channels.ElectronApplicationEvaluateExpressionHandleResult> {
const handle = await this._object._nodeElectronHandlePromised;
const handle = await this._object._nodeElectronHandlePromise;
const result = await handle.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg));
return { handle: ElementHandleDispatcher.fromJSHandle(this._scope, result) };
}
Expand Down
4 changes: 4 additions & 0 deletions src/dispatchers/pageDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(page._video) });
}

page(): Page {
return this._page;
}

async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.setDefaultNavigationTimeout(params.timeout);
}
Expand Down
19 changes: 11 additions & 8 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2564,22 +2564,25 @@ export type ElectronLaunchResult = {
};

// ----------- ElectronApplication -----------
export type ElectronApplicationInitializer = {};
export type ElectronApplicationInitializer = {
context: BrowserContextChannel,
};
export interface ElectronApplicationChannel extends Channel {
on(event: 'context', callback: (params: ElectronApplicationContextEvent) => void): this;
on(event: 'close', callback: (params: ElectronApplicationCloseEvent) => void): this;
on(event: 'window', callback: (params: ElectronApplicationWindowEvent) => void): this;
browserWindow(params: ElectronApplicationBrowserWindowParams, metadata?: Metadata): Promise<ElectronApplicationBrowserWindowResult>;
evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, metadata?: Metadata): Promise<ElectronApplicationEvaluateExpressionResult>;
evaluateExpressionHandle(params: ElectronApplicationEvaluateExpressionHandleParams, metadata?: Metadata): Promise<ElectronApplicationEvaluateExpressionHandleResult>;
close(params?: ElectronApplicationCloseParams, metadata?: Metadata): Promise<ElectronApplicationCloseResult>;
}
export type ElectronApplicationContextEvent = {
context: BrowserContextChannel,
};
export type ElectronApplicationCloseEvent = {};
export type ElectronApplicationWindowEvent = {
export type ElectronApplicationBrowserWindowParams = {
page: PageChannel,
browserWindow: JSHandleChannel,
};
export type ElectronApplicationBrowserWindowOptions = {

};
export type ElectronApplicationBrowserWindowResult = {
handle: JSHandleChannel,
};
export type ElectronApplicationEvaluateExpressionParams = {
expression: string,
Expand Down
21 changes: 9 additions & 12 deletions src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2097,8 +2097,17 @@ Electron:
ElectronApplication:
type: interface

initializer:
context: BrowserContext

commands:

browserWindow:
parameters:
page: Page
returns:
handle: JSHandle

evaluateExpression:
parameters:
expression: string
Expand All @@ -2118,20 +2127,8 @@ ElectronApplication:
close:

events:

# This event happens once immediately after creation.
context:
parameters:
context: BrowserContext

close:

window:
parameters:
page: Page
browserWindow: JSHandle



Android:
type: interface
Expand Down
3 changes: 3 additions & 0 deletions src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
env: tOptional(tArray(tType('NameValue'))),
timeout: tOptional(tNumber),
});
scheme.ElectronApplicationBrowserWindowParams = tObject({
page: tChannel('Page'),
});
scheme.ElectronApplicationEvaluateExpressionParams = tObject({
expression: tString,
isFunction: tOptional(tBoolean),
Expand Down
54 changes: 17 additions & 37 deletions src/server/electron/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,16 @@ export type ElectronLaunchOptionsBase = {
timeout?: number,
};

export interface ElectronPage extends Page {
browserWindow: js.JSHandle<BrowserWindow>;
_browserWindowId: number;
}

export class ElectronApplication extends SdkObject {
static Events = {
Close: 'close',
Window: 'window',
};

private _browserContext: CRBrowserContext;
private _nodeConnection: CRConnection;
private _nodeSession: CRSession;
private _nodeExecutionContext: js.ExecutionContext | undefined;
_nodeElectronHandlePromised: Promise<js.JSHandle<any>>;
private _resolveNodeElectronHandle!: (handle: js.JSHandle<any>) => void;
private _windows = new Set<ElectronPage>();
_nodeElectronHandlePromise: Promise<js.JSHandle<any>>;
private _lastWindowId = 0;
readonly _timeoutSettings = new TimeoutSettings();

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

private async _onPage(page: ElectronPage) {
private async _onPage(page: Page) {
// Needs to be sync.
const windowId = ++this._lastWindowId;
page.on(Page.Events.Close, () => {
if (page.browserWindow)
page.browserWindow.dispose();
this._windows.delete(page);
});
page._browserWindowId = windowId;
this._windows.add(page);

// Below is async.
const handle = await (await this._nodeElectronHandlePromised).evaluateHandle(({ BrowserWindow }, windowId) => BrowserWindow.fromId(windowId), windowId).catch(e => {});
if (!handle)
return;
page.browserWindow = handle;
const controller = new ProgressController(internalCallMetadata(), this);
await controller.run(progress => page.mainFrame()._waitForLoadState(progress, 'domcontentloaded'), page._timeoutSettings.navigationTimeout({})).catch(e => {}); // can happen after detach
this.emit(ElectronApplication.Events.Window, page);
(page as any)._browserWindowId = windowId;
}

context(): BrowserContext {
Expand All @@ -105,19 +90,15 @@ export class ElectronApplication extends SdkObject {
async close() {
const progressController = new ProgressController(internalCallMetadata(), this);
const closed = progressController.run(progress => helper.waitForEvent(progress, this, ElectronApplication.Events.Close).promise, this._timeoutSettings.timeout({}));
await (await this._nodeElectronHandlePromised).evaluate(({ app }) => app.quit());
const electronHandle = await this._nodeElectronHandlePromise;
await electronHandle.evaluate(({ app }) => app.quit());
this._nodeConnection.close();
await closed;
}

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

Expand Down Expand Up @@ -188,7 +169,6 @@ export class Electron extends SdkObject {
};
const browser = await CRBrowser.connect(chromeTransport, browserOptions);
app = new ElectronApplication(this, browser, nodeConnection);
await app._init();
return app;
}, TimeoutSettings.timeout(options));
}
Expand Down
2 changes: 1 addition & 1 deletion tests/config/electron-app.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const { app, BrowserWindow } = require('electron');
const { app } = require('electron');

app.on('window-all-closed', e => e.preventDefault());
11 changes: 11 additions & 0 deletions tests/config/electron-window-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { app, BrowserWindow } = require('electron');

app.whenReady().then(() => {
const win = new BrowserWindow({
width: 800,
height: 600,
});
win.loadURL('about:blank');
})

app.on('window-all-closed', e => e.preventDefault());
3 changes: 2 additions & 1 deletion tests/config/electron.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import * as folio from 'folio';
import * as path from 'path';
import { ElectronEnv, electronTest } from './electronTest';
import { baseElectronTest, ElectronEnv, electronTest } from './electronTest';
import { test as pageTest } from './pageTest';

const config: folio.Config = {
Expand Down Expand Up @@ -63,5 +63,6 @@ const envConfig = {
}
};

baseElectronTest.runWith(envConfig);
electronTest.runWith(envConfig);
pageTest.runWith(envConfig, new ElectronPageEnv());
1 change: 1 addition & 0 deletions tests/config/electronTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@ export class ElectronEnv {
}
}

export const baseElectronTest = baseTest.extend({});
export const electronTest = baseTest.extend(new ElectronEnv());
22 changes: 20 additions & 2 deletions tests/electron/electron-app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
* limitations under the License.
*/

import type { BrowserWindow } from 'electron';
import path from 'path';
import { electronTest as test, expect } from '../config/electronTest';
import { electronTest as test, baseElectronTest as baseTest, expect } from '../config/electronTest';

test('should fire close event', async ({ playwright }) => {
baseTest('should fire close event', async ({ playwright }) => {
const electronApp = await playwright._electron.launch({
args: [path.join(__dirname, '..', 'config', 'electron-app.js')],
});
Expand Down Expand Up @@ -88,3 +89,20 @@ test('should have a clipboard instance', async ({ electronApp }) => {
const clipboardContentRead = await electronApp.evaluate(async ({clipboard}) => clipboard.readText());
expect(clipboardContentRead).toEqual(clipboardContentToWrite);
});

test('should test app that opens window fast', async ({ playwright }) => {
const electronApp = await playwright._electron.launch({
args: [path.join(__dirname, '..', 'config', 'electron-window-app.js')],
});
await electronApp.close();
});

test('should return browser window', async ({ playwright }) => {
const electronApp = await playwright._electron.launch({
args: [path.join(__dirname, '..', 'config', 'electron-window-app.js')],
});
const page = await electronApp.waitForEvent('window');
const bwHandle = await electronApp.browserWindow(page);
expect(await bwHandle.evaluate((bw: BrowserWindow) => bw.title)).toBe('Electron');
await electronApp.close();
});
6 changes: 6 additions & 0 deletions types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7511,6 +7511,12 @@ export interface ElectronApplication {
*/
off(event: 'window', listener: (page: Page) => void): this;

/**
* Returns the BrowserWindow object that corresponds to the given Playwright page.
* @param page Page to retrieve the window for.
*/
browserWindow(page: Page): Promise<JSHandle>;

/**
* Closes Electron application.
*/
Expand Down