chore: centralize playwright creation, bind context listeners to instance (#5217)

This commit is contained in:
Pavel Feldman 2021-01-29 16:00:56 -08:00 committed by GitHub
parent 7fe7d0ef32
commit 975519150e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 104 additions and 112 deletions

View file

@ -70,7 +70,7 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
this._browser = browser; this._browser = browser;
this._wsEndpoint = ''; this._wsEndpoint = '';
this._process = browser._options.browserProcess.process!; this._process = browser.options.browserProcess.process!;
let readyCallback = () => {}; let readyCallback = () => {};
this._ready = new Promise<void>(f => readyCallback = f); this._ready = new Promise<void>(f => readyCallback = f);
@ -86,7 +86,7 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
this._clientAttached(socket); this._clientAttached(socket);
}); });
browser._options.browserProcess.onclose = (exitCode, signal) => { browser.options.browserProcess.onclose = (exitCode, signal) => {
this._server.close(); this._server.close();
this.emit('close', exitCode, signal); this.emit('close', exitCode, signal);
}; };
@ -101,11 +101,11 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
} }
async close(): Promise<void> { async close(): Promise<void> {
await this._browser._options.browserProcess.close(); await this._browser.options.browserProcess.close();
} }
async kill(): Promise<void> { async kill(): Promise<void> {
await this._browser._options.browserProcess.kill(); await this._browser.options.browserProcess.kill();
} }
private _clientAttached(socket: ws) { private _clientAttached(socket: ws) {
@ -158,7 +158,7 @@ class ConnectedBrowser extends BrowserDispatcher {
async newContext(params: channels.BrowserNewContextParams): Promise<{ context: channels.BrowserContextChannel }> { async newContext(params: channels.BrowserNewContextParams): Promise<{ context: channels.BrowserContextChannel }> {
if (params.recordVideo) { if (params.recordVideo) {
// TODO: we should create a separate temp directory or accept a launchServer parameter. // TODO: we should create a separate temp directory or accept a launchServer parameter.
params.recordVideo.dir = this._object._options.downloadsPath!; params.recordVideo.dir = this._object.options.downloadsPath!;
} }
const result = await super.newContext(params); const result = await super.newContext(params);
const dispatcher = result.context as BrowserContextDispatcher; const dispatcher = result.context as BrowserContextDispatcher;

View file

@ -18,15 +18,12 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { installInspectorController } from '../server/supplements/inspectorController';
import { DispatcherConnection } from '../dispatchers/dispatcher'; import { DispatcherConnection } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { installBrowsersWithProgressBar } from '../install/installer'; import { installBrowsersWithProgressBar } from '../install/installer';
import { Transport } from '../protocol/transport'; import { Transport } from '../protocol/transport';
import { Playwright } from '../server/playwright'; import { createPlaywright } from '../server/playwright';
import { gracefullyCloseAll } from '../server/processLauncher'; import { gracefullyCloseAll } from '../server/processLauncher';
import { installHarTracer } from '../trace/harTracer';
import { installTracer } from '../trace/tracer';
import { BrowserName } from '../utils/browserPaths'; import { BrowserName } from '../utils/browserPaths';
export function printApiJson() { export function printApiJson() {
@ -38,10 +35,6 @@ export function printProtocol() {
} }
export function runServer() { export function runServer() {
installInspectorController();
installTracer();
installHarTracer();
const dispatcherConnection = new DispatcherConnection(); const dispatcherConnection = new DispatcherConnection();
const transport = new Transport(process.stdout, process.stdin); const transport = new Transport(process.stdout, process.stdin);
transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message)); transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message));
@ -56,7 +49,7 @@ export function runServer() {
process.exit(0); process.exit(0);
}; };
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']); const playwright = createPlaywright();
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright); new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
} }

View file

@ -28,7 +28,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', { isChromium: context._browser._options.isChromium }, 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())
@ -41,7 +41,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
context.on(BrowserContext.Events.StdOut, data => this._dispatchEvent('stdout', { data: Buffer.from(data, 'utf8').toString('base64') })); context.on(BrowserContext.Events.StdOut, data => this._dispatchEvent('stdout', { data: Buffer.from(data, 'utf8').toString('base64') }));
context.on(BrowserContext.Events.StdErr, data => this._dispatchEvent('stderr', { data: Buffer.from(data, 'utf8').toString('base64') })); context.on(BrowserContext.Events.StdErr, data => this._dispatchEvent('stderr', { data: Buffer.from(data, 'utf8').toString('base64') }));
if (context._browser._options.name === 'chromium') { if (context._browser.options.name === 'chromium') {
for (const page of (context as CRBrowserContext).backgroundPages()) for (const page of (context as CRBrowserContext).backgroundPages())
this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) }); this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) });
context.on(CRBrowserContext.CREvents.BackgroundPage, page => this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) })); context.on(CRBrowserContext.CREvents.BackgroundPage, page => this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) }));
@ -139,7 +139,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async pause() { async pause() {
if (!this._context._browser._options.headful) if (!this._context._browser.options.headful)
return; return;
const recorder = await RecorderSupplement.getOrCreate(this._context, 'pause', { const recorder = await RecorderSupplement.getOrCreate(this._context, 'pause', {
language: 'javascript', language: 'javascript',
@ -149,7 +149,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.isChromium) 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

@ -24,7 +24,7 @@ import { PageDispatcher } from './pageDispatcher';
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserInitializer> implements channels.BrowserChannel { export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserInitializer> implements channels.BrowserChannel {
constructor(scope: DispatcherScope, browser: Browser) { constructor(scope: DispatcherScope, browser: Browser) {
super(scope, browser, 'Browser', { version: browser.version(), name: browser._options.name }, true); super(scope, browser, 'Browser', { version: browser.version(), name: browser.options.name }, true);
browser.on(Browser.Events.Disconnected, () => this._didClose()); browser.on(Browser.Events.Disconnected, () => this._didClose());
} }
@ -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.isChromium) 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.isChromium) 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.isChromium) 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

@ -15,22 +15,14 @@
*/ */
import { DispatcherConnection } from './dispatchers/dispatcher'; import { DispatcherConnection } from './dispatchers/dispatcher';
import { Playwright as PlaywrightImpl } from './server/playwright'; import { createPlaywright } from './server/playwright';
import type { Playwright as PlaywrightAPI } from './client/playwright'; import type { Playwright as PlaywrightAPI } from './client/playwright';
import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
import { Connection } from './client/connection'; import { Connection } from './client/connection';
import { BrowserServerLauncherImpl } from './browserServerImpl'; import { BrowserServerLauncherImpl } from './browserServerImpl';
import { installInspectorController } from './server/supplements/inspectorController';
import { installTracer } from './trace/tracer';
import { installHarTracer } from './trace/harTracer';
import * as path from 'path';
function setupInProcess(): PlaywrightAPI { function setupInProcess(): PlaywrightAPI {
const playwright = new PlaywrightImpl(path.join(__dirname, '..'), require('../browsers.json')['browsers']); const playwright = createPlaywright();
installInspectorController();
installTracer();
installHarTracer();
const clientConnection = new Connection(); const clientConnection = new Connection();
const dispatcherConnection = new DispatcherConnection(); const dispatcherConnection = new DispatcherConnection();

View file

@ -17,20 +17,13 @@
import * as debug from 'debug'; import * as debug from 'debug';
import * as http from 'http'; import * as http from 'http';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { installInspectorController } from '../server/supplements/inspectorController';
import { DispatcherConnection } from '../dispatchers/dispatcher'; import { DispatcherConnection } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { Playwright } from '../server/playwright'; import { createPlaywright } from '../server/playwright';
import { gracefullyCloseAll } from '../server/processLauncher'; import { gracefullyCloseAll } from '../server/processLauncher';
import { installTracer } from '../trace/tracer';
import { installHarTracer } from '../trace/harTracer';
const debugLog = debug('pw:server'); const debugLog = debug('pw:server');
installInspectorController();
installTracer();
installHarTracer();
export class PlaywrightServer { export class PlaywrightServer {
private _server: http.Server | undefined; private _server: http.Server | undefined;
private _client: WebSocket | undefined; private _client: WebSocket | undefined;
@ -62,8 +55,7 @@ export class PlaywrightServer {
this._onDisconnect(); this._onDisconnect();
}); });
dispatcherConnection.onmessage = message => ws.send(JSON.stringify(message)); dispatcherConnection.onmessage = message => ws.send(JSON.stringify(message));
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']); new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), createPlaywright());
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
}); });
} }

View file

@ -22,7 +22,7 @@ import * as stream from 'stream';
import * as util from 'util'; import * as util from 'util';
import * as ws from 'ws'; import * as ws from 'ws';
import { createGuid, makeWaitForNextTask } from '../../utils/utils'; import { createGuid, makeWaitForNextTask } from '../../utils/utils';
import { BrowserOptions, BrowserProcess } from '../browser'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
import { BrowserContext, validateBrowserContextOptions } from '../browserContext'; import { BrowserContext, validateBrowserContextOptions } from '../browserContext';
import { ProgressController } from '../progress'; import { ProgressController } from '../progress';
import { CRBrowser } from '../chromium/crBrowser'; import { CRBrowser } from '../chromium/crBrowser';
@ -57,9 +57,11 @@ export class Android {
private _backend: Backend; private _backend: Backend;
private _devices = new Map<string, AndroidDevice>(); private _devices = new Map<string, AndroidDevice>();
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
readonly _playwrightOptions: PlaywrightOptions;
constructor(backend: Backend) { constructor(backend: Backend, playwrightOptions: PlaywrightOptions) {
this._backend = backend; this._backend = backend;
this._playwrightOptions = playwrightOptions;
this._timeoutSettings = new TimeoutSettings(); this._timeoutSettings = new TimeoutSettings();
} }
@ -255,6 +257,7 @@ export class AndroidDevice extends EventEmitter {
this._browserConnections.add(androidBrowser); this._browserConnections.add(androidBrowser);
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
...this._android._playwrightOptions,
name: 'clank', name: 'clank',
isChromium: true, isChromium: true,
slowMo: 0, slowMo: 0,

View file

@ -15,7 +15,7 @@
*/ */
import * as types from './types'; import * as types from './types';
import { BrowserContext, Video } from './browserContext'; import { BrowserContext, ContextListener, Video } from './browserContext';
import { Page } from './page'; import { Page } from './page';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
@ -30,7 +30,11 @@ export interface BrowserProcess {
close(): Promise<void>; close(): Promise<void>;
} }
export type BrowserOptions = types.UIOptions & { export type PlaywrightOptions = {
contextListeners: ContextListener[]
};
export type BrowserOptions = PlaywrightOptions & {
name: string, name: string,
isChromium: boolean, isChromium: boolean,
downloadsPath?: string, downloadsPath?: string,
@ -40,6 +44,7 @@ export type BrowserOptions = types.UIOptions & {
proxy?: ProxySettings, proxy?: ProxySettings,
protocolLogger: types.ProtocolLogger, protocolLogger: types.ProtocolLogger,
browserLogsCollector: RecentLogsCollector, browserLogsCollector: RecentLogsCollector,
slowMo?: number,
}; };
export abstract class Browser extends EventEmitter { export abstract class Browser extends EventEmitter {
@ -47,7 +52,7 @@ export abstract class Browser extends EventEmitter {
Disconnected: 'disconnected', Disconnected: 'disconnected',
}; };
readonly _options: BrowserOptions; readonly options: BrowserOptions;
private _downloads = new Map<string, Download>(); private _downloads = new Map<string, Download>();
_defaultContext: BrowserContext | null = null; _defaultContext: BrowserContext | null = null;
private _startedClosing = false; private _startedClosing = false;
@ -55,7 +60,7 @@ export abstract class Browser extends EventEmitter {
constructor(options: BrowserOptions) { constructor(options: BrowserOptions) {
super(); super();
this._options = options; this.options = options;
} }
abstract newContext(options?: types.BrowserContextOptions): Promise<BrowserContext>; abstract newContext(options?: types.BrowserContextOptions): Promise<BrowserContext>;
@ -71,7 +76,7 @@ export abstract class Browser extends EventEmitter {
} }
_downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) { _downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) {
const download = new Download(page, this._options.downloadsPath || '', uuid, url, suggestedFilename); const download = new Download(page, this.options.downloadsPath || '', uuid, url, suggestedFilename);
this._downloads.set(uuid, download); this._downloads.set(uuid, download);
} }
@ -117,7 +122,7 @@ export abstract class Browser extends EventEmitter {
async close() { async close() {
if (!this._startedClosing) { if (!this._startedClosing) {
this._startedClosing = true; this._startedClosing = true;
await this._options.browserProcess.close(); await this.options.browserProcess.close();
} }
if (this.isConnected()) if (this.isConnected())
await new Promise(x => this.once(Browser.Events.Disconnected, x)); await new Promise(x => this.once(Browser.Events.Disconnected, x));

View file

@ -94,8 +94,6 @@ export interface ContextListener {
onContextDidDestroy(context: BrowserContext): Promise<void>; onContextDidDestroy(context: BrowserContext): Promise<void>;
} }
export const contextListeners = new Set<ContextListener>();
export abstract class BrowserContext extends EventEmitter { export abstract class BrowserContext extends EventEmitter {
static Events = { static Events = {
Close: 'close', Close: 'close',
@ -140,7 +138,7 @@ export abstract class BrowserContext extends EventEmitter {
} }
async _initialize() { async _initialize() {
for (const listener of contextListeners) for (const listener of this._browser.options.contextListeners)
await listener.onContextCreated(this); await listener.onContextCreated(this);
} }
@ -259,7 +257,7 @@ export abstract class BrowserContext extends EventEmitter {
} }
protected _authenticateProxyViaHeader() { protected _authenticateProxyViaHeader() {
const proxy = this._options.proxy || this._browser._options.proxy || { username: undefined, password: undefined }; const proxy = this._options.proxy || this._browser.options.proxy || { username: undefined, password: undefined };
const { username, password } = proxy; const { username, password } = proxy;
if (username) { if (username) {
this._options.httpCredentials = { username, password: password! }; this._options.httpCredentials = { username, password: password! };
@ -272,7 +270,7 @@ export abstract class BrowserContext extends EventEmitter {
} }
protected _authenticateProxyViaCredentials() { protected _authenticateProxyViaCredentials() {
const proxy = this._options.proxy || this._browser._options.proxy; const proxy = this._options.proxy || this._browser.options.proxy;
if (!proxy) if (!proxy)
return; return;
const { username, password } = proxy; const { username, password } = proxy;
@ -294,7 +292,7 @@ export abstract class BrowserContext extends EventEmitter {
this.emit(BrowserContext.Events.BeforeClose); this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing'; this._closedStatus = 'closing';
for (const listener of contextListeners) for (const listener of this._browser.options.contextListeners)
await listener.onContextWillDestroy(this); await listener.onContextWillDestroy(this);
// Collect videos/downloads that we will await. // Collect videos/downloads that we will await.
@ -323,7 +321,7 @@ export abstract class BrowserContext extends EventEmitter {
await this._browser.close(); await this._browser.close();
// Bookkeeping. // Bookkeeping.
for (const listener of contextListeners) for (const listener of this._browser.options.contextListeners)
await listener.onContextDidDestroy(this); await listener.onContextDidDestroy(this);
this._didCloseInternal(); this._didCloseInternal();
} }

View file

@ -21,7 +21,7 @@ import * as util from 'util';
import { BrowserContext, normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; import { BrowserContext, normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
import * as browserPaths from '../utils/browserPaths'; import * as browserPaths from '../utils/browserPaths';
import { ConnectionTransport } from './transport'; import { ConnectionTransport } from './transport';
import { BrowserOptions, Browser, BrowserProcess } from './browser'; import { BrowserOptions, Browser, BrowserProcess, PlaywrightOptions } from './browser';
import { launchProcess, Env, envArrayToObject } from './processLauncher'; import { launchProcess, Env, envArrayToObject } from './processLauncher';
import { PipeTransport } from './pipeTransport'; import { PipeTransport } from './pipeTransport';
import { Progress, ProgressController } from './progress'; import { Progress, ProgressController } from './progress';
@ -42,8 +42,10 @@ export abstract class BrowserType {
private _executablePath: string; private _executablePath: string;
private _browserDescriptor: browserPaths.BrowserDescriptor; private _browserDescriptor: browserPaths.BrowserDescriptor;
readonly _browserPath: string; readonly _browserPath: string;
readonly _playwrightOptions: PlaywrightOptions;
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor) { constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, playwrightOptions: PlaywrightOptions) {
this._playwrightOptions = playwrightOptions;
this._name = browser.name; this._name = browser.name;
const browsersPath = browserPaths.browsersPath(packagePath); const browsersPath = browserPaths.browsersPath(packagePath);
this._browserDescriptor = browser; this._browserDescriptor = browser;
@ -87,6 +89,7 @@ export abstract class BrowserType {
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser(); await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
...this._playwrightOptions,
name: this._name, name: this._name,
isChromium: this._name === 'chromium', isChromium: this._name === 'chromium',
slowMo: options.slowMo, slowMo: options.slowMo,

View file

@ -24,15 +24,15 @@ import { BrowserType } from '../browserType';
import { ConnectionTransport, ProtocolRequest } from '../transport'; import { ConnectionTransport, ProtocolRequest } from '../transport';
import type { BrowserDescriptor } from '../../utils/browserPaths'; import type { BrowserDescriptor } from '../../utils/browserPaths';
import { CRDevTools } from './crDevTools'; import { CRDevTools } from './crDevTools';
import { BrowserOptions } from '../browser'; import { BrowserOptions, PlaywrightOptions } from '../browser';
import * as types from '../types'; import * as types from '../types';
import { isDebugMode } from '../../utils/utils'; import { isDebugMode } from '../../utils/utils';
export class Chromium extends BrowserType { export class Chromium extends BrowserType {
private _devtools: CRDevTools | undefined; private _devtools: CRDevTools | undefined;
constructor(packagePath: string, browser: BrowserDescriptor) { constructor(packagePath: string, browser: BrowserDescriptor, playwrightOptions: PlaywrightOptions) {
super(packagePath, browser); super(packagePath, browser, playwrightOptions);
if (isDebugMode()) if (isDebugMode())
this._devtools = this._createDevTools(); this._devtools = this._createDevTools();
} }

View file

@ -98,7 +98,7 @@ export class CRBrowser extends Browser {
} }
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> { async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options, this._options); validateBrowserContextOptions(options, this.options);
const { browserContextId } = await this._session.send('Target.createBrowserContext', { const { browserContextId } = await this._session.send('Target.createBrowserContext', {
disposeOnDetach: true, disposeOnDetach: true,
proxyServer: options.proxy ? options.proxy.server : undefined, proxyServer: options.proxy ? options.proxy.server : undefined,
@ -119,7 +119,7 @@ export class CRBrowser extends Browser {
} }
isClank(): boolean { isClank(): boolean {
return this._options.name === 'clank'; return this.options.name === 'clank';
} }
_onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) { _onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
@ -293,11 +293,11 @@ export class CRBrowserContext extends BrowserContext {
async _initialize() { async _initialize() {
assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this)); assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this));
const promises: Promise<any>[] = [ super._initialize() ]; const promises: Promise<any>[] = [ super._initialize() ];
if (this._browser._options.downloadsPath) { if (this._browser.options.downloadsPath) {
promises.push(this._browser._session.send('Browser.setDownloadBehavior', { promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny', behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny',
browserContextId: this._browserContextId, browserContextId: this._browserContextId,
downloadPath: this._browser._options.downloadsPath downloadPath: this._browser.options.downloadsPath
})); }));
} }
if (this._options.permissions) if (this._options.permissions)

View file

@ -845,7 +845,7 @@ class FrameSession {
]; ];
if (this._windowId) { if (this._windowId) {
let insets = { width: 0, height: 0 }; let insets = { width: 0, height: 0 };
if (this._crPage._browserContext._browser._options.headful) { if (this._crPage._browserContext._browser.options.headful) {
// TODO: popup windows have their own insets. // TODO: popup windows have their own insets.
insets = { width: 24, height: 88 }; insets = { width: 24, height: 88 };
if (process.platform === 'win32') if (process.platform === 'win32')

View file

@ -29,7 +29,7 @@ import type {BrowserWindow} from 'electron';
import { Progress, ProgressController, runAbortableTask } from '../progress'; import { Progress, ProgressController, runAbortableTask } from '../progress';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { helper } from '../helper'; import { helper } from '../helper';
import { BrowserOptions, BrowserProcess } from '../browser'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import * as readline from 'readline'; import * as readline from 'readline';
import { RecentLogsCollector } from '../../utils/debugLogger'; import { RecentLogsCollector } from '../../utils/debugLogger';
@ -139,6 +139,12 @@ export class ElectronApplication extends EventEmitter {
} }
export class Electron { export class Electron {
private _playwrightOptions: PlaywrightOptions;
constructor(playwrightOptions: PlaywrightOptions) {
this._playwrightOptions = playwrightOptions;
}
async launch(executablePath: string, options: ElectronLaunchOptionsBase = {}): Promise<ElectronApplication> { async launch(executablePath: string, options: ElectronLaunchOptionsBase = {}): Promise<ElectronApplication> {
const { const {
args = [], args = [],
@ -190,6 +196,7 @@ export class Electron {
kill kill
}; };
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
...this._playwrightOptions,
name: 'electron', name: 'electron',
isChromium: true, isChromium: true,
headful: true, headful: true,

View file

@ -72,7 +72,7 @@ export class FFBrowser extends Browser {
} }
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> { async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options, this._options); validateBrowserContextOptions(options, this.options);
if (options.isMobile) if (options.isMobile)
throw new Error('options.isMobile is not supported in Firefox'); throw new Error('options.isMobile is not supported in Firefox');
const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true }); const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true });
@ -149,12 +149,12 @@ export class FFBrowserContext extends BrowserContext {
assert(!this._ffPages().length); assert(!this._ffPages().length);
const browserContextId = this._browserContextId; const browserContextId = this._browserContextId;
const promises: Promise<any>[] = [ super._initialize() ]; const promises: Promise<any>[] = [ super._initialize() ];
if (this._browser._options.downloadsPath) { if (this._browser.options.downloadsPath) {
promises.push(this._browser._connection.send('Browser.setDownloadOptions', { promises.push(this._browser._connection.send('Browser.setDownloadOptions', {
browserContextId, browserContextId,
downloadOptions: { downloadOptions: {
behavior: this._options.acceptDownloads ? 'saveToDisk' : 'cancel', behavior: this._options.acceptDownloads ? 'saveToDisk' : 'cancel',
downloadsDir: this._browser._options.downloadsPath, downloadsDir: this._browser.options.downloadsPath,
}, },
})); }));
} }

View file

@ -198,7 +198,7 @@ export class Page extends EventEmitter {
} }
async _doSlowMo() { async _doSlowMo() {
const slowMo = this._browserContext._browser._options.slowMo; const slowMo = this._browserContext._browser.options.slowMo;
if (!slowMo) if (!slowMo)
return; return;
await new Promise(x => setTimeout(x, slowMo)); await new Promise(x => setTimeout(x, slowMo));

View file

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as path from 'path';
import { Tracer } from '../trace/tracer';
import * as browserPaths from '../utils/browserPaths'; import * as browserPaths from '../utils/browserPaths';
import { Android } from './android/android'; import { Android } from './android/android';
import { AdbBackend } from './android/backendAdb'; import { AdbBackend } from './android/backendAdb';
@ -21,6 +23,8 @@ import { Chromium } from './chromium/chromium';
import { Electron } from './electron/electron'; import { Electron } from './electron/electron';
import { Firefox } from './firefox/firefox'; import { Firefox } from './firefox/firefox';
import { serverSelectors } from './selectors'; import { serverSelectors } from './selectors';
import { HarTracer } from './supplements/har/harTracer';
import { InspectorController } from './supplements/inspectorController';
import { WebKit } from './webkit/webkit'; import { WebKit } from './webkit/webkit';
export class Playwright { export class Playwright {
@ -30,18 +34,29 @@ export class Playwright {
readonly electron: Electron; readonly electron: Electron;
readonly firefox: Firefox; readonly firefox: Firefox;
readonly webkit: WebKit; readonly webkit: WebKit;
readonly options = {
contextListeners: [
new InspectorController(),
new Tracer(),
new HarTracer()
]
};
constructor(packagePath: string, browsers: browserPaths.BrowserDescriptor[]) { constructor(packagePath: string, browsers: browserPaths.BrowserDescriptor[]) {
const chromium = browsers.find(browser => browser.name === 'chromium'); const chromium = browsers.find(browser => browser.name === 'chromium');
this.chromium = new Chromium(packagePath, chromium!); this.chromium = new Chromium(packagePath, chromium!, this.options);
const firefox = browsers.find(browser => browser.name === 'firefox'); const firefox = browsers.find(browser => browser.name === 'firefox');
this.firefox = new Firefox(packagePath, firefox!); this.firefox = new Firefox(packagePath, firefox!, this.options);
const webkit = browsers.find(browser => browser.name === 'webkit'); const webkit = browsers.find(browser => browser.name === 'webkit');
this.webkit = new WebKit(packagePath, webkit!); this.webkit = new WebKit(packagePath, webkit!, this.options);
this.electron = new Electron(); this.electron = new Electron(this.options);
this.android = new Android(new AdbBackend()); this.android = new Android(new AdbBackend(), this.options);
} }
} }
export function createPlaywright() {
return new Playwright(path.join(__dirname, '..', '..'), require('../../browsers.json')['browsers']);
}

View file

@ -16,19 +16,15 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as util from 'util'; import * as util from 'util';
import { BrowserContext, ContextListener, contextListeners } from '../server/browserContext'; import { BrowserContext, ContextListener } from '../../browserContext';
import { helper } from '../server/helper'; import { helper } from '../../helper';
import * as network from '../server/network'; import * as network from '../../network';
import { Page } from '../server/page'; import { Page } from '../../page';
import * as har from './har'; import * as har from './har';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
export function installHarTracer() { export class HarTracer implements ContextListener {
contextListeners.add(new HarTracer());
}
class HarTracer implements ContextListener {
private _contextTracers = new Map<BrowserContext, HarContextTracer>(); private _contextTracers = new Map<BrowserContext, HarContextTracer>();
async onContextCreated(context: BrowserContext): Promise<void> { async onContextCreated(context: BrowserContext): Promise<void> {
@ -68,10 +64,10 @@ class HarContextTracer {
version: '1.2', version: '1.2',
creator: { creator: {
name: 'Playwright', name: 'Playwright',
version: require('../../package.json')['version'], version: require('../../../../package.json')['version'],
}, },
browser: { browser: {
name: context._browser._options.name, name: context._browser.options.name,
version: context._browser.version() version: context._browser.version()
}, },
pages: [], pages: [],

View file

@ -14,18 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { BrowserContext, ContextListener, contextListeners } from '../browserContext'; import { BrowserContext, ContextListener } from '../browserContext';
import { isDebugMode } from '../../utils/utils'; import { isDebugMode } from '../../utils/utils';
import { ConsoleApiSupplement } from './consoleApiSupplement'; import { ConsoleApiSupplement } from './consoleApiSupplement';
import { RecorderSupplement } from './recorderSupplement'; import { RecorderSupplement } from './recorderSupplement';
import { Page } from '../page'; import { Page } from '../page';
import { ConsoleMessage } from '../console'; import { ConsoleMessage } from '../console';
export function installInspectorController() { export class InspectorController implements ContextListener {
contextListeners.add(new InspectorController());
}
class InspectorController implements ContextListener {
async onContextCreated(context: BrowserContext): Promise<void> { async onContextCreated(context: BrowserContext): Promise<void> {
if (isDebugMode()) { if (isDebugMode()) {
const consoleApi = new ConsoleApiSupplement(context); const consoleApi = new ConsoleApiSupplement(context);

View file

@ -92,7 +92,7 @@ export class RecorderSupplement {
this._output.setEnabled(app === 'codegen'); this._output.setEnabled(app === 'codegen');
context.on(BrowserContext.Events.BeforeClose, () => this._output.flush()); context.on(BrowserContext.Events.BeforeClose, () => this._output.flush());
const generator = new CodeGenerator(context._browser._options.name, app === 'codegen', params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage); const generator = new CodeGenerator(context._browser.options.name, app === 'codegen', params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage);
this._generator = generator; this._generator = generator;
} }

View file

@ -271,7 +271,7 @@ type LaunchOptionsBase = {
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
}; };
export type LaunchOptions = LaunchOptionsBase & UIOptions & { export type LaunchOptions = LaunchOptionsBase & {
firefoxUserPrefs?: { [key: string]: string | number | boolean }, firefoxUserPrefs?: { [key: string]: string | number | boolean },
}; };
export type LaunchPersistentOptions = LaunchOptionsBase & BrowserContextOptions; export type LaunchPersistentOptions = LaunchOptionsBase & BrowserContextOptions;
@ -326,10 +326,6 @@ export type Error = {
stack?: string, stack?: string,
}; };
export type UIOptions = {
slowMo?: number;
};
export type NameValueList = { export type NameValueList = {
name: string; name: string;
value: string; value: string;

View file

@ -74,7 +74,7 @@ export class WKBrowser extends Browser {
} }
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> { async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options, this._options); validateBrowserContextOptions(options, this.options);
const createOptions = options.proxy ? { const createOptions = options.proxy ? {
proxyServer: options.proxy.server, proxyServer: options.proxy.server,
proxyBypassList: options.proxy.bypass proxyBypassList: options.proxy.bypass
@ -208,10 +208,10 @@ export class WKBrowserContext extends BrowserContext {
assert(!this._wkPages().length); assert(!this._wkPages().length);
const browserContextId = this._browserContextId; const browserContextId = this._browserContextId;
const promises: Promise<any>[] = [ super._initialize() ]; const promises: Promise<any>[] = [ super._initialize() ];
if (this._browser._options.downloadsPath) { if (this._browser.options.downloadsPath) {
promises.push(this._browser._browserSession.send('Playwright.setDownloadBehavior', { promises.push(this._browser._browserSession.send('Playwright.setDownloadBehavior', {
behavior: this._options.acceptDownloads ? 'allow' : 'deny', behavior: this._options.acceptDownloads ? 'allow' : 'deny',
downloadPath: this._browser._options.downloadsPath, downloadPath: this._browser.options.downloadsPath,
browserContextId browserContextId
})); }));
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners, Video } from '../server/browserContext'; import { ActionListener, ActionMetadata, BrowserContext, ContextListener, Video } from '../server/browserContext';
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter'; import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import * as trace from './traceTypes'; import * as trace from './traceTypes';
import * as path from 'path'; import * as path from 'path';
@ -34,11 +34,7 @@ const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
const fsAccessAsync = util.promisify(fs.access.bind(fs)); const fsAccessAsync = util.promisify(fs.access.bind(fs));
const envTrace = getFromENV('PW_TRACE_DIR'); const envTrace = getFromENV('PW_TRACE_DIR');
export function installTracer() { export class Tracer implements ContextListener {
contextListeners.add(new Tracer());
}
class Tracer implements ContextListener {
private _contextTracers = new Map<BrowserContext, ContextTracer>(); private _contextTracers = new Map<BrowserContext, ContextTracer>();
async onContextCreated(context: BrowserContext): Promise<void> { async onContextCreated(context: BrowserContext): Promise<void> {
@ -93,7 +89,7 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
const event: trace.ContextCreatedTraceEvent = { const event: trace.ContextCreatedTraceEvent = {
timestamp: monotonicTime(), timestamp: monotonicTime(),
type: 'context-created', type: 'context-created',
browserName: context._browser._options.name, browserName: context._browser.options.name,
contextId: this._contextId, contextId: this._contextId,
isMobile: !!context._options.isMobile, isMobile: !!context._options.isMobile,
deviceScaleFactor: context._options.deviceScaleFactor || 1, deviceScaleFactor: context._options.deviceScaleFactor || 1,

View file

@ -17,7 +17,7 @@
import { folio as baseFolio } from './fixtures'; import { folio as baseFolio } from './fixtures';
import * as fs from 'fs'; import * as fs from 'fs';
import type * as har from '../src/trace/har'; import type * as har from '../src/server/supplements/har/har';
import type { BrowserContext, Page } from '../index'; import type { BrowserContext, Page } from '../index';
const builder = baseFolio.extend<{ const builder = baseFolio.extend<{

View file

@ -138,7 +138,7 @@ DEPS['src/server/injected/'] = ['src/server/common/'];
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/']; DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/']; DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/']; DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/trace/', 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/'];
DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**']; DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
// Tracing is a client/server plugin, nothing should depend on it. // Tracing is a client/server plugin, nothing should depend on it.