diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index a7ef1d6c30..1aa152ec22 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -29,6 +29,7 @@ export type PlaywrightInitializer = { chromium: BrowserTypeChannel, firefox: BrowserTypeChannel, webkit: BrowserTypeChannel, + electron?: ElectronChannel, deviceDescriptors: types.Devices, selectors: SelectorsChannel, }; @@ -384,4 +385,33 @@ export type PDFOptions = { preferCSSPageSize?: boolean, margin?: {top?: string, bottom?: string, left?: string, right?: string}, path?: string, +}; + + +export type ElectronLaunchOptions = { + args?: string[], + cwd?: string, + env?: {[key: string]: string|number|boolean}, + handleSIGINT?: boolean, + handleSIGTERM?: boolean, + handleSIGHUP?: boolean, + timeout?: number, +}; +export interface ElectronChannel extends Channel { + launch(params: { executablePath: string } & ElectronLaunchOptions): Promise; } +export type ElectronInitializer = {}; + + +export interface ElectronApplicationChannel extends Channel { + on(event: 'close', callback: () => void): this; + on(event: 'window', callback: (params: PageChannel) => void): this; + + newBrowserWindow(params: { arg: any }): Promise; + evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise; + evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any }): Promise; + close(): Promise; +} +export type ElectronApplicationInitializer = { + context: BrowserContextChannel, +}; diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index a82978f587..7e6a7443b6 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -25,7 +25,6 @@ import { helper } from '../../helper'; import { Browser } from './browser'; import { Events } from '../../events'; import { TimeoutSettings } from '../../timeoutSettings'; -import { BrowserType } from './browserType'; import { Waiter } from './waiter'; import { TimeoutError } from '../../errors'; @@ -33,7 +32,7 @@ export class BrowserContext extends ChannelOwner(); private _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = []; readonly _browser: Browser | undefined; - readonly _browserType: BrowserType; + readonly _browserName: string; readonly _bindings = new Map(); _timeoutSettings = new TimeoutSettings(); _ownerPage: Page | undefined; @@ -48,15 +47,11 @@ export class BrowserContext extends ChannelOwner this._onBinding(BindingCall.from(bindingCall))); this._channel.on('close', () => this._onClose()); diff --git a/src/rpc/client/chromiumBrowserContext.ts b/src/rpc/client/chromiumBrowserContext.ts index 0819505278..55c16a3d5d 100644 --- a/src/rpc/client/chromiumBrowserContext.ts +++ b/src/rpc/client/chromiumBrowserContext.ts @@ -28,7 +28,7 @@ export class ChromiumBrowserContext extends BrowserContext { _serviceWorkers = new Set(); constructor(parent: ChannelOwner, type: string, guid: string, initializer: BrowserContextInitializer) { - super(parent, type, guid, initializer); + super(parent, type, guid, initializer, 'chromium'); this._channel.on('crBackgroundPage', pageChannel => { const page = Page.from(pageChannel); this._backgroundPages.add(page); diff --git a/src/rpc/client/connection.ts b/src/rpc/client/connection.ts index 8b38528a20..ff52e34eb4 100644 --- a/src/rpc/client/connection.ts +++ b/src/rpc/client/connection.ts @@ -32,6 +32,7 @@ import { parseError } from '../serializers'; import { BrowserServer } from './browserServer'; import { CDPSession } from './cdpSession'; import { Playwright } from './playwright'; +import { Electron, ElectronApplication } from './electron'; import { Channel } from '../channels'; import { ChromiumBrowser } from './chromiumBrowser'; import { ChromiumBrowserContext } from './chromiumBrowserContext'; @@ -151,12 +152,21 @@ export class Connection { result = new CDPSession(parent, type, guid, initializer); break; case 'context': - // Launching persistent context produces BrowserType parent directly for BrowserContext. - const browserType = parent instanceof Browser ? parent._browserType : parent as BrowserType; - if (browserType.name() === 'chromium') + let browserName = ''; + if (parent instanceof Electron) { + // Launching electron produces Electron parent for BrowserContext. + browserName = 'electron'; + } else if (parent instanceof Browser) { + // Launching a browser produces Browser parent for BrowserContext. + browserName = parent._browserType.name(); + } else { + // Launching persistent context produces BrowserType parent for BrowserContext. + browserName = (parent as BrowserType).name(); + } + if (browserName === 'chromium') result = new ChromiumBrowserContext(parent, type, guid, initializer); else - result = new BrowserContext(parent, type, guid, initializer); + result = new BrowserContext(parent, type, guid, initializer, browserName); break; case 'consoleMessage': result = new ConsoleMessage(parent, type, guid, initializer); @@ -167,6 +177,12 @@ export class Connection { case 'download': result = new Download(parent, type, guid, initializer); break; + case 'electron': + result = new Electron(parent, type, guid, initializer); + break; + case 'electronApplication': + result = new ElectronApplication(parent, type, guid, initializer); + break; case 'elementHandle': result = new ElementHandle(parent, type, guid, initializer); break; diff --git a/src/rpc/client/electron.ts b/src/rpc/client/electron.ts new file mode 100644 index 0000000000..577d889ca8 --- /dev/null +++ b/src/rpc/client/electron.ts @@ -0,0 +1,112 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as types from '../../types'; +import { ElectronChannel, ElectronInitializer, ElectronLaunchOptions, ElectronApplicationChannel, ElectronApplicationInitializer } from '../channels'; +import { BrowserContext } from './browserContext'; +import { ChannelOwner } from './channelOwner'; +import { Page } from './page'; +import { serializeArgument, FuncOn, parseResult, SmartHandle, JSHandle } from './jsHandle'; +import { ElectronEvents } from '../../server/electron'; +import { TimeoutSettings } from '../../timeoutSettings'; +import { Waiter } from './waiter'; +import { TimeoutError } from '../../errors'; + +export class Electron extends ChannelOwner { + static from(electron: ElectronChannel): Electron { + return (electron as any)._object; + } + + constructor(parent: ChannelOwner, type: string, guid: string, initializer: ElectronInitializer) { + super(parent, type, guid, initializer, true); + } + + async launch(executablePath: string, options: ElectronLaunchOptions = {}): Promise { + options = { ...options }; + delete (options as any).logger; + return ElectronApplication.from(await this._channel.launch({ executablePath, ...options })); + } +} + +export class ElectronApplication extends ChannelOwner { + private _context: BrowserContext; + private _windows = new Set(); + private _timeoutSettings = new TimeoutSettings(); + + static from(electronApplication: ElectronApplicationChannel): ElectronApplication { + return (electronApplication as any)._object; + } + + constructor(parent: ChannelOwner, type: string, guid: string, initializer: ElectronApplicationInitializer) { + super(parent, type, guid, initializer); + this._context = BrowserContext.from(initializer.context); + this._channel.on('window', pageChannel => { + const page = Page.from(pageChannel); + this._windows.add(page); + this.emit(ElectronEvents.ElectronApplication.Window, page); + }); + this._channel.on('close', () => { + this.emit(ElectronEvents.ElectronApplication.Close); + }); + } + + windows(): Page[] { + return [...this._windows]; + } + + async firstWindow(): Promise { + if (this._windows.size) + return this._windows.values().next().value; + return this.waitForEvent('window'); + } + + async newBrowserWindow(options: any): Promise { + return Page.from(await this._channel.newBrowserWindow({ arg: serializeArgument(options) })); + } + + context(): BrowserContext { + return this._context; + } + + async close() { + await this._channel.close(); + } + + async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { + const timeout = this._timeoutSettings.timeout(optionsOrPredicate instanceof Function ? {} : optionsOrPredicate); + const predicate = optionsOrPredicate instanceof Function ? optionsOrPredicate : optionsOrPredicate.predicate; + const waiter = new Waiter(); + waiter.rejectOnTimeout(timeout, new TimeoutError(`Timeout while waiting for event "${event}"`)); + if (event !== ElectronEvents.ElectronApplication.Close) + waiter.rejectOnEvent(this, ElectronEvents.ElectronApplication.Close, new Error('Electron application closed')); + const result = await waiter.waitForEvent(this, event, predicate as any); + waiter.dispose(); + return result; + } + + async evaluate(pageFunction: FuncOn, arg: Arg): Promise; + async evaluate(pageFunction: FuncOn, arg?: any): Promise; + async evaluate(pageFunction: FuncOn, arg: Arg): Promise { + return parseResult(await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) })); + } + + async evaluateHandle(pageFunction: FuncOn, arg: Arg): Promise>; + async evaluateHandle(pageFunction: FuncOn, arg?: any): Promise>; + async evaluateHandle(pageFunction: FuncOn, arg: Arg): Promise> { + const handleChannel = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); + return JSHandle.from(handleChannel) as SmartHandle; + } +} diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index 8ff502e117..3a22c74e35 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -104,7 +104,7 @@ export class Page extends ChannelOwner { this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request))); this._channel.on('worker', worker => this._onWorker(Worker.from(worker))); - if (this._browserContext._browserType.name() === 'chromium') { + if (this._browserContext._browserName === 'chromium') { this.coverage = new Coverage(this._channel); this.pdf = options => this._pdf(options); } diff --git a/src/rpc/client/playwright.ts b/src/rpc/client/playwright.ts index edfab0a08d..246af0424d 100644 --- a/src/rpc/client/playwright.ts +++ b/src/rpc/client/playwright.ts @@ -19,6 +19,7 @@ import * as types from '../../types'; import { BrowserType } from './browserType'; import { ChannelOwner } from './channelOwner'; import { Selectors } from './selectors'; +import { Electron } from './electron'; export class Playwright extends ChannelOwner { chromium: BrowserType; @@ -32,6 +33,8 @@ export class Playwright extends ChannelOwner dispatcherConnection.dispatch(JSON.parse(messag dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message)); const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']); +(playwright as any).electron = new Electron(); new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright); diff --git a/src/rpc/server/electronDispatcher.ts b/src/rpc/server/electronDispatcher.ts new file mode 100644 index 0000000000..b176f21260 --- /dev/null +++ b/src/rpc/server/electronDispatcher.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher'; +import { Electron, ElectronApplication, ElectronEvents } from '../../server/electron'; +import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, ElectronLaunchOptions } from '../channels'; +import { BrowserContextDispatcher } from './browserContextDispatcher'; +import { BrowserContextBase } from '../../browserContext'; +import { Page } from '../../page'; +import { PageDispatcher } from './pageDispatcher'; +import { parseArgument } from './jsHandleDispatcher'; +import { createHandle } from './elementHandlerDispatcher'; + +export class ElectronDispatcher extends Dispatcher implements ElectronChannel { + constructor(scope: DispatcherScope, electron: Electron) { + super(scope, electron, 'electron', {}, true); + } + + async launch(params: { executablePath: string } & ElectronLaunchOptions): Promise { + const electronApplication = await this._object.launch(params.executablePath, params); + return new ElectronApplicationDispatcher(this._scope, electronApplication); + } +} + +export class ElectronApplicationDispatcher extends Dispatcher implements ElectronApplicationChannel { + constructor(scope: DispatcherScope, electronApplication: ElectronApplication) { + super(scope, electronApplication, 'electronApplication', { + context: new BrowserContextDispatcher(scope, electronApplication.context() as BrowserContextBase), + }); + + electronApplication.on(ElectronEvents.ElectronApplication.Close, () => this._dispatchEvent('close')); + electronApplication.on(ElectronEvents.ElectronApplication.Window, (page: Page) => this._dispatchEvent('window', lookupDispatcher(page))); + } + + async newBrowserWindow(params: { arg: any }): Promise { + return lookupDispatcher(await this._object.newBrowserWindow(parseArgument(params.arg))); + } + + async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise { + const handle = this._object._nodeElectronHandle!; + return handle._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg)); + } + + async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise { + const handle = this._object._nodeElectronHandle!; + const result = await handle._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); + return createHandle(this._scope, result); + } + + async close(): Promise { + await this._object.close(); + } +} diff --git a/src/rpc/server/playwrightDispatcher.ts b/src/rpc/server/playwrightDispatcher.ts index 4176a68f3d..a208b0b4da 100644 --- a/src/rpc/server/playwrightDispatcher.ts +++ b/src/rpc/server/playwrightDispatcher.ts @@ -19,13 +19,17 @@ import { PlaywrightChannel, PlaywrightInitializer } from '../channels'; import { BrowserTypeDispatcher } from './browserTypeDispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher'; import { SelectorsDispatcher } from './selectorsDispatcher'; +import { Electron } from '../../server/electron'; +import { ElectronDispatcher } from './electronDispatcher'; export class PlaywrightDispatcher extends Dispatcher implements PlaywrightChannel { constructor(scope: DispatcherScope, playwright: Playwright) { + const electron = (playwright as any).electron as (Electron | undefined); super(scope, playwright, 'playwright', { chromium: new BrowserTypeDispatcher(scope, playwright.chromium!), firefox: new BrowserTypeDispatcher(scope, playwright.firefox!), webkit: new BrowserTypeDispatcher(scope, playwright.webkit!), + electron: electron ? new ElectronDispatcher(scope, electron) : undefined, deviceDescriptors: playwright.devices, selectors: new SelectorsDispatcher(scope, playwright.selectors), }, false, 'playwright'); diff --git a/src/server/electron.ts b/src/server/electron.ts index c325cbed5d..4a15480a8e 100644 --- a/src/server/electron.ts +++ b/src/server/electron.ts @@ -63,7 +63,7 @@ export class ElectronApplication extends EventEmitter { private _nodeConnection: CRConnection; private _nodeSession: CRSession; private _nodeExecutionContext: js.ExecutionContext | undefined; - private _nodeElectronHandle: js.JSHandle | undefined; + _nodeElectronHandle: js.JSHandle | undefined; private _windows = new Set(); private _lastWindowId = 0; readonly _timeoutSettings = new TimeoutSettings(); diff --git a/test/channels.spec.js b/test/channels.spec.js index 5b0c797a5b..950266a84b 100644 --- a/test/channels.spec.js +++ b/test/channels.spec.js @@ -33,6 +33,7 @@ describe.skip(!CHANNEL)('Channels', function() { { _guid: 'browserType', objects: [] }, { _guid: 'playwright' }, { _guid: 'selectors' }, + { _guid: 'electron', objects: [] }, ] }; await expectScopeState(browser, GOLDEN_PRECONDITION); @@ -57,6 +58,7 @@ describe.skip(!CHANNEL)('Channels', function() { ] }, { _guid: 'playwright' }, { _guid: 'selectors' }, + { _guid: 'electron', objects: [] }, ] }); @@ -75,6 +77,7 @@ describe.skip(!CHANNEL)('Channels', function() { { _guid: 'browserType', objects: [] }, { _guid: 'playwright' }, { _guid: 'selectors' }, + { _guid: 'electron', objects: [] }, ] }; await expectScopeState(browserType, GOLDEN_PRECONDITION); @@ -92,6 +95,7 @@ describe.skip(!CHANNEL)('Channels', function() { { _guid: 'browserType', objects: [] }, { _guid: 'playwright' }, { _guid: 'selectors' }, + { _guid: 'electron', objects: [] }, ] }); @@ -110,6 +114,7 @@ describe.skip(!CHANNEL)('Channels', function() { { _guid: 'browserType', objects: [] }, { _guid: 'playwright' }, { _guid: 'selectors' }, + { _guid: 'electron', objects: [] }, ] }; await expectScopeState(browserType, GOLDEN_PRECONDITION); @@ -129,6 +134,7 @@ describe.skip(!CHANNEL)('Channels', function() { { _guid: 'browserType', objects: [] }, { _guid: 'playwright' }, { _guid: 'selectors' }, + { _guid: 'electron', objects: [] }, ] }); diff --git a/test/electron/electron.spec.js b/test/electron/electron.spec.js index 20988d1b12..08fef8672e 100644 --- a/test/electron/electron.spec.js +++ b/test/electron/electron.spec.js @@ -21,7 +21,7 @@ const {CHANNEL} = utils.testOptions(browserType); const electronName = process.platform === 'win32' ? 'electron.cmd' : 'electron'; -describe.skip(CHANNEL)('Electron', function() { +describe('Electron', function() { beforeEach(async (state, testRun) => { const electronPath = path.join(__dirname, '..', '..', 'node_modules', '.bin', electronName); state.logger = utils.createTestLogger(config.dumpLogOnFailure, testRun); @@ -55,7 +55,7 @@ describe.skip(CHANNEL)('Electron', function() { await page.goto('data:text/html,Hello World 2'); expect(await page.title()).toBe('Hello World 2'); }); - it('should create multiple windows', async ({ application }) => { + it.skip(CHANNEL)('should create multiple windows', async ({ application }) => { const createPage = async ordinal => { const page = await application.newBrowserWindow({ width: 800, height: 600 }); await Promise.all([ @@ -117,7 +117,7 @@ describe.skip(CHANNEL)('Electron', function() { }); }); -describe.skip(CHANNEL)('Electron per window', function() { +describe('Electron per window', function() { beforeAll(async state => { const electronPath = path.join(__dirname, '..', '..', 'node_modules', '.bin', electronName); state.application = await state.playwright.electron.launch(electronPath, {