fix(electron): return a ChromiumBrowserContext for electron (#4913)

This commit is contained in:
Joel Einbinder 2021-01-13 12:08:14 -08:00 committed by GitHub
parent df53cb2f83
commit decf373c81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 42 additions and 21 deletions

View File

@ -27,6 +27,7 @@ import { Page } from './page';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { ChromiumBrowserContext } from './chromiumBrowserContext';
type Direction = 'down' | 'up' | 'left' | 'right'; type Direction = 'down' | 'up' | 'left' | 'right';
type SpeedOptions = { speed?: number }; type SpeedOptions = { speed?: number };
@ -233,11 +234,11 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
}); });
} }
async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise<BrowserContext> { async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise<ChromiumBrowserContext> {
return this._wrapApiCall('androidDevice.launchBrowser', async () => { return this._wrapApiCall('androidDevice.launchBrowser', async () => {
const contextOptions = await prepareBrowserContextOptions(options); const contextOptions = await prepareBrowserContextOptions(options);
const { context } = await this._channel.launchBrowser(contextOptions); const { context } = await this._channel.launchBrowser(contextOptions);
return BrowserContext.from(context); return BrowserContext.from(context) as ChromiumBrowserContext;
}); });
} }

View File

@ -40,7 +40,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
_pages = new Set<Page>(); _pages = new Set<Page>();
private _routes: { url: URLMatch, handler: network.RouteHandler }[] = []; private _routes: { url: URLMatch, handler: network.RouteHandler }[] = [];
readonly _browser: Browser | null = null; readonly _browser: Browser | null = null;
readonly _browserName: string;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>(); readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
_timeoutSettings = new TimeoutSettings(); _timeoutSettings = new TimeoutSettings();
_ownerPage: Page | undefined; _ownerPage: Page | undefined;
@ -55,11 +54,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
return context ? BrowserContext.from(context) : null; return context ? BrowserContext.from(context) : null;
} }
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer, browserName: string) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
if (parent instanceof Browser) if (parent instanceof Browser)
this._browser = parent; this._browser = parent;
this._browserName = browserName;
this._channel.on('bindingCall', ({binding}) => this._onBinding(BindingCall.from(binding))); this._channel.on('bindingCall', ({binding}) => this._onBinding(BindingCall.from(binding)));
this._channel.on('close', () => this._onClose()); this._channel.on('close', () => this._onClose());

View File

@ -27,9 +27,10 @@ import * as api from '../../types/types';
export class ChromiumBrowserContext extends BrowserContext implements api.ChromiumBrowserContext { export class ChromiumBrowserContext extends BrowserContext implements api.ChromiumBrowserContext {
_backgroundPages = new Set<Page>(); _backgroundPages = new Set<Page>();
_serviceWorkers = new Set<Worker>(); _serviceWorkers = new Set<Worker>();
_isChromium = true;
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
super(parent, type, guid, initializer, 'chromium'); super(parent, type, guid, initializer);
this._channel.on('crBackgroundPage', ({ page }) => { this._channel.on('crBackgroundPage', ({ page }) => {
const backgroundPage = Page.from(page); const backgroundPage = Page.from(page);
this._backgroundPages.add(backgroundPage); this._backgroundPages.add(backgroundPage);

View File

@ -173,11 +173,11 @@ export class Connection {
break; break;
} }
case 'BrowserContext': { case 'BrowserContext': {
const browserName = (initializer as channels.BrowserContextInitializer).browserName; const {isChromium} = (initializer as channels.BrowserContextInitializer);
if (browserName === 'chromium') if (isChromium)
result = new ChromiumBrowserContext(parent, type, guid, initializer); result = new ChromiumBrowserContext(parent, type, guid, initializer);
else else
result = new BrowserContext(parent, type, guid, initializer, browserName); result = new BrowserContext(parent, type, guid, initializer);
break; break;
} }
case 'BrowserType': case 'BrowserType':

View File

@ -46,6 +46,7 @@ import { evaluationScript, urlMatches } from './clientHelper';
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils'; import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
import { isSafeCloseError } from '../utils/errors'; import { isSafeCloseError } from '../utils/errors';
import { Video } from './video'; import { Video } from './video';
import type { ChromiumBrowserContext } from './chromiumBrowserContext';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const mkdirAsync = util.promisify(fs.mkdir); const mkdirAsync = util.promisify(fs.mkdir);
@ -133,7 +134,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.on('webSocket', ({ webSocket }) => this.emit(Events.Page.WebSocket, WebSocket.from(webSocket))); this._channel.on('webSocket', ({ webSocket }) => this.emit(Events.Page.WebSocket, WebSocket.from(webSocket)));
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker))); this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
if (this._browserContext._browserName === 'chromium') { if ((this._browserContext as ChromiumBrowserContext)._isChromium) {
this.coverage = new ChromiumCoverage(this._channel); this.coverage = new ChromiumCoverage(this._channel);
this.pdf = options => this._pdf(options); this.pdf = options => this._pdf(options);
} else { } else {

View File

@ -27,7 +27,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
private _context: BrowserContext; private _context: BrowserContext;
constructor(scope: DispatcherScope, context: BrowserContext) { constructor(scope: DispatcherScope, context: BrowserContext) {
super(scope, context, 'BrowserContext', { browserName: context._browser._options.name }, true); super(scope, context, 'BrowserContext', { isChromium: context._browser._options.isChromium }, true);
this._context = context; this._context = context;
for (const page of context.pages()) for (const page of context.pages())
@ -131,7 +131,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> { async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
if (this._object._browser._options.name !== 'chromium') if (!this._object._browser._options.isChromium)
throw new Error(`CDP session is only available in Chromium`); throw new Error(`CDP session is only available in Chromium`);
const crBrowserContext = this._object as CRBrowserContext; const crBrowserContext = this._object as CRBrowserContext;
return { session: new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession((params.page as PageDispatcher)._object)) }; return { session: new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession((params.page as PageDispatcher)._object)) };

View File

@ -45,21 +45,21 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserIniti
} }
async crNewBrowserCDPSession(): Promise<channels.BrowserCrNewBrowserCDPSessionResult> { async crNewBrowserCDPSession(): Promise<channels.BrowserCrNewBrowserCDPSessionResult> {
if (this._object._options.name !== 'chromium') if (!this._object._options.isChromium)
throw new Error(`CDP session is only available in Chromium`); throw new Error(`CDP session is only available in Chromium`);
const crBrowser = this._object as CRBrowser; const crBrowser = this._object as CRBrowser;
return { session: new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession()) }; return { session: new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession()) };
} }
async crStartTracing(params: channels.BrowserCrStartTracingParams): Promise<void> { async crStartTracing(params: channels.BrowserCrStartTracingParams): Promise<void> {
if (this._object._options.name !== 'chromium') if (!this._object._options.isChromium)
throw new Error(`Tracing is only available in Chromium`); throw new Error(`Tracing is only available in Chromium`);
const crBrowser = this._object as CRBrowser; const crBrowser = this._object as CRBrowser;
await crBrowser.startTracing(params.page ? (params.page as PageDispatcher)._object : undefined, params); await crBrowser.startTracing(params.page ? (params.page as PageDispatcher)._object : undefined, params);
} }
async crStopTracing(): Promise<channels.BrowserCrStopTracingResult> { async crStopTracing(): Promise<channels.BrowserCrStopTracingResult> {
if (this._object._options.name !== 'chromium') if (!this._object._options.isChromium)
throw new Error(`Tracing is only available in Chromium`); throw new Error(`Tracing is only available in Chromium`);
const crBrowser = this._object as CRBrowser; const crBrowser = this._object as CRBrowser;
const buffer = await crBrowser.stopTracing(); const buffer = await crBrowser.stopTracing();

View File

@ -530,7 +530,7 @@ export type BrowserCrStopTracingResult = {
// ----------- BrowserContext ----------- // ----------- BrowserContext -----------
export type BrowserContextInitializer = { export type BrowserContextInitializer = {
browserName: string, isChromium: boolean,
}; };
export interface BrowserContextChannel extends Channel { export interface BrowserContextChannel extends Channel {
on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this; on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this;

View File

@ -507,7 +507,7 @@ BrowserContext:
type: interface type: interface
initializer: initializer:
browserName: string isChromium: boolean
commands: commands:

View File

@ -256,6 +256,7 @@ export class AndroidDevice extends EventEmitter {
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
name: 'clank', name: 'clank',
isChromium: true,
slowMo: 0, slowMo: 0,
persistent: { ...options, noDefaultViewport: true }, persistent: { ...options, noDefaultViewport: true },
downloadsPath: undefined, downloadsPath: undefined,

View File

@ -32,6 +32,7 @@ export interface BrowserProcess {
export type BrowserOptions = types.UIOptions & { export type BrowserOptions = types.UIOptions & {
name: string, name: string,
isChromium: boolean,
downloadsPath?: string, downloadsPath?: string,
headful?: boolean, headful?: boolean,
persistent?: types.BrowserContextOptions, // Undefined means no persistent context. persistent?: types.BrowserContextOptions, // Undefined means no persistent context.

View File

@ -38,7 +38,7 @@ const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => f
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-'); const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
export abstract class BrowserType { export abstract class BrowserType {
private _name: string; private _name: browserPaths.BrowserName;
private _executablePath: string; private _executablePath: string;
private _browserDescriptor: browserPaths.BrowserDescriptor; private _browserDescriptor: browserPaths.BrowserDescriptor;
readonly _browserPath: string; readonly _browserPath: string;
@ -88,6 +88,7 @@ export abstract class BrowserType {
await (options as any).__testHookBeforeCreateBrowser(); await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
name: this._name, name: this._name,
isChromium: this._name === 'chromium',
slowMo: options.slowMo, slowMo: options.slowMo,
persistent, persistent,
headful: !options.headless, headful: !options.headless,

View File

@ -191,6 +191,7 @@ export class Electron {
}; };
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
name: 'electron', name: 'electron',
isChromium: true,
headful: true, headful: true,
persistent: { noDefaultViewport: true }, persistent: { noDefaultViewport: true },
browserProcess, browserProcess,

View File

@ -21,7 +21,7 @@ import * as path from 'path';
import { getUbuntuVersionSync } from './ubuntuVersion'; import { getUbuntuVersionSync } from './ubuntuVersion';
import { getFromENV } from './utils'; import { getFromENV } from './utils';
export type BrowserName = 'chromium'|'webkit'|'firefox'|'clank'; export type BrowserName = 'chromium'|'webkit'|'firefox';
export type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'mac11.0'|'mac11.0-arm64'|'mac11.1'|'mac11.1-arm64'|'ubuntu18.04'|'ubuntu20.04'; export type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'mac11.0'|'mac11.0-arm64'|'mac11.1'|'mac11.1-arm64'|'ubuntu18.04'|'ubuntu20.04';
export type BrowserDescriptor = { export type BrowserDescriptor = {
name: BrowserName, name: BrowserName,

View File

@ -48,4 +48,12 @@ if (process.env.PW_ANDROID_TESTS) {
await page.close(); await page.close();
await context.close(); await context.close();
}); });
it('should be able to send CDP messages', async ({ device }) => {
const context = await device.launchBrowser();
const [page] = context.pages();
const client = await context.newCDPSession(page);
await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
expect(evalResponse.result.value).toBe(3);
});
} }

View File

@ -124,4 +124,12 @@ describe('electron app', (suite, { browserName }) => {
const clipboardContentRead = await application.evaluate(async ({clipboard}) => clipboard.readText()); const clipboardContentRead = await application.evaluate(async ({clipboard}) => clipboard.readText());
await expect(clipboardContentRead).toEqual(clipboardContentToWrite); await expect(clipboardContentRead).toEqual(clipboardContentToWrite);
}); });
it('should be able to send CDP messages', async ({application, window}) => {
const context = await application.context();
const client = await context.newCDPSession(window);
await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
expect(evalResponse.result.value).toBe(3);
});
}); });

4
types/android.d.ts vendored
View File

@ -15,7 +15,7 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { BrowserContextOptions, BrowserContext, Page } from './types'; import { BrowserContextOptions, Page, ChromiumBrowserContext } from './types';
export interface Android extends EventEmitter { export interface Android extends EventEmitter {
setDefaultTimeout(timeout: number): void; setDefaultTimeout(timeout: number): void;
@ -37,7 +37,7 @@ export interface AndroidDevice extends EventEmitter {
open(command: string): Promise<AndroidSocket>; open(command: string): Promise<AndroidSocket>;
installApk(file: string | Buffer, options?: { args?: string[] }): Promise<void>; installApk(file: string | Buffer, options?: { args?: string[] }): Promise<void>;
push(file: string | Buffer, path: string, options?: { mode?: number }): Promise<void>; push(file: string | Buffer, path: string, options?: { mode?: number }): Promise<void>;
launchBrowser(options?: BrowserContextOptions & { pkg?: string }): Promise<BrowserContext>; launchBrowser(options?: BrowserContextOptions & { pkg?: string }): Promise<ChromiumBrowserContext>;
close(): Promise<void>; close(): Promise<void>;
wait(selector: AndroidSelector, options?: { state?: 'gone' } & { timeout?: number }): Promise<void>; wait(selector: AndroidSelector, options?: { state?: 'gone' } & { timeout?: number }): Promise<void>;