chore: introduce sdk object base class (#5370)

This commit is contained in:
Pavel Feldman 2021-02-09 09:00:00 -08:00 committed by GitHub
parent 909544907c
commit 0652f3251f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 215 additions and 125 deletions

View file

@ -31,8 +31,9 @@ import { createGuid } from './utils/utils';
import { SelectorsDispatcher } from './dispatchers/selectorsDispatcher'; import { SelectorsDispatcher } from './dispatchers/selectorsDispatcher';
import { Selectors } from './server/selectors'; import { Selectors } from './server/selectors';
import { BrowserContext, Video } from './server/browserContext'; import { BrowserContext, Video } from './server/browserContext';
import { StreamDispatcher } from './dispatchers/streamDispatcher'; import { StreamDispatcher, StreamWrapper } from './dispatchers/streamDispatcher';
import { ProtocolLogger } from './server/types'; import { ProtocolLogger } from './server/types';
import { SdkObject } from './server/sdkObject';
export class BrowserServerLauncherImpl implements BrowserServerLauncher { export class BrowserServerLauncherImpl implements BrowserServerLauncher {
private _browserType: BrowserType; private _browserType: BrowserType;
@ -118,7 +119,7 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
connection.dispatch(JSON.parse(Buffer.from(message).toString())); connection.dispatch(JSON.parse(Buffer.from(message).toString()));
}); });
socket.on('error', () => {}); socket.on('error', () => {});
const selectors = new Selectors(); const selectors = new Selectors(this._browser.options.rootSdkObject);
const scope = connection.rootDispatcher(); const scope = connection.rootDispatcher();
const remoteBrowser = new RemoteBrowserDispatcher(scope, this._browser, selectors); const remoteBrowser = new RemoteBrowserDispatcher(scope, this._browser, selectors);
socket.on('close', () => { socket.on('close', () => {
@ -130,12 +131,12 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer {
} }
} }
class RemoteBrowserDispatcher extends Dispatcher<{}, channels.RemoteBrowserInitializer> implements channels.PlaywrightChannel { class RemoteBrowserDispatcher extends Dispatcher<SdkObject, channels.RemoteBrowserInitializer> implements channels.PlaywrightChannel {
readonly connectedBrowser: ConnectedBrowser; readonly connectedBrowser: ConnectedBrowser;
constructor(scope: DispatcherScope, browser: Browser, selectors: Selectors) { constructor(scope: DispatcherScope, browser: Browser, selectors: Selectors) {
const connectedBrowser = new ConnectedBrowser(scope, browser, selectors); const connectedBrowser = new ConnectedBrowser(scope, browser, selectors);
super(scope, {}, 'RemoteBrowser', { super(scope, browser, 'RemoteBrowser', {
selectors: new SelectorsDispatcher(scope, selectors), selectors: new SelectorsDispatcher(scope, selectors),
browser: connectedBrowser, browser: connectedBrowser,
}, false, 'remoteBrowser'); }, false, 'remoteBrowser');
@ -188,7 +189,7 @@ class ConnectedBrowser extends BrowserDispatcher {
video._waitForCallbackOnFinish(async () => { video._waitForCallbackOnFinish(async () => {
const readable = fs.createReadStream(video._path); const readable = fs.createReadStream(video._path);
await new Promise(f => readable.on('readable', f)); await new Promise(f => readable.on('readable', f));
const stream = new StreamDispatcher(this._remoteBrowser!._scope, readable); const stream = new StreamDispatcher(this._remoteBrowser!._scope, new StreamWrapper(this._object, readable));
this._remoteBrowser!._dispatchEvent('video', { this._remoteBrowser!._dispatchEvent('video', {
stream, stream,
context: contextDispatcher, context: contextDispatcher,

View file

@ -35,6 +35,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: Initializer) { constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: Initializer) {
super(); super();
this.setMaxListeners(0);
this._connection = parent instanceof ChannelOwner ? parent._connection : parent; this._connection = parent instanceof ChannelOwner ? parent._connection : parent;
this._type = type; this._type = type;
this._guid = guid; this._guid = guid;

View file

@ -97,7 +97,6 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PageInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PageInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.setMaxListeners(0);
this._browserContext = parent as BrowserContext; this._browserContext = parent as BrowserContext;
this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings); this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings);

View file

@ -21,6 +21,7 @@ import { createScheme, Validator, ValidationError } from '../protocol/validator'
import { assert, createGuid, debugAssert, isUnderTest } from '../utils/utils'; import { assert, createGuid, debugAssert, isUnderTest } from '../utils/utils';
import { tOptional } from '../protocol/validatorPrimitives'; import { tOptional } from '../protocol/validatorPrimitives';
import { kBrowserOrContextClosedError } from '../utils/errors'; import { kBrowserOrContextClosedError } from '../utils/errors';
import { SdkObject } from '../server/sdkObject';
export const dispatcherSymbol = Symbol('dispatcher'); export const dispatcherSymbol = Symbol('dispatcher');
@ -38,7 +39,14 @@ export function lookupNullableDispatcher<DispatcherType>(object: any | null): Di
return object ? lookupDispatcher(object) : undefined; return object ? lookupDispatcher(object) : undefined;
} }
export class Dispatcher<Type, Initializer> extends EventEmitter implements channels.Channel { export type CallMetadata = channels.Metadata & {
object: SdkObject;
type: string;
method: string;
params: any;
};
export class Dispatcher<Type extends SdkObject, Initializer> extends EventEmitter implements channels.Channel {
private _connection: DispatcherConnection; private _connection: DispatcherConnection;
private _isScope: boolean; private _isScope: boolean;
// Parent is always "isScope". // Parent is always "isScope".
@ -112,10 +120,9 @@ export class Dispatcher<Type, Initializer> extends EventEmitter implements chann
} }
export type DispatcherScope = Dispatcher<any, any>; export type DispatcherScope = Dispatcher<any, any>;
class Root extends Dispatcher<SdkObject, {}> {
class Root extends Dispatcher<{}, {}> {
constructor(connection: DispatcherConnection) { constructor(connection: DispatcherConnection) {
super(connection, {}, '', {}, true, ''); super(connection, new SdkObject(null), '', {}, true, '');
} }
} }
@ -178,7 +185,14 @@ export class DispatcherConnection {
const validated = this._validateParams(dispatcher._type, method, params); const validated = this._validateParams(dispatcher._type, method, params);
if (typeof (dispatcher as any)[method] !== 'function') if (typeof (dispatcher as any)[method] !== 'function')
throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`); throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`);
const result = await (dispatcher as any)[method](validated, this._validateMetadata(metadata)); const callMetadata: CallMetadata = {
...this._validateMetadata(metadata).stack,
object: dispatcher._object,
type: dispatcher._type,
method,
params,
};
const result = await (dispatcher as any)[method](validated, callMetadata);
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) }); this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
} catch (e) { } catch (e) {
this.onmessage({ id, error: serializeError(e) }); this.onmessage({ id, error: serializeError(e) });

View file

@ -17,7 +17,7 @@
import { Download } from '../server/download'; import { Download } from '../server/download';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope } from './dispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher'; import { StreamDispatcher, StreamWrapper } from './streamDispatcher';
import * as fs from 'fs'; import * as fs from 'fs';
import * as util from 'util'; import * as util from 'util';
import { mkdirIfNeeded } from '../utils/utils'; import { mkdirIfNeeded } from '../utils/utils';
@ -65,7 +65,7 @@ export class DownloadDispatcher extends Dispatcher<Download, channels.DownloadIn
try { try {
const readable = fs.createReadStream(localPath); const readable = fs.createReadStream(localPath);
await new Promise(f => readable.on('readable', f)); await new Promise(f => readable.on('readable', f));
const stream = new StreamDispatcher(this._scope, readable); const stream = new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable));
// Resolve with a stream, so that client starts saving the data. // Resolve with a stream, so that client starts saving the data.
resolve({ stream }); resolve({ stream });
// Block the download until the stream is consumed. // Block the download until the stream is consumed.
@ -87,7 +87,7 @@ export class DownloadDispatcher extends Dispatcher<Download, channels.DownloadIn
return {}; return {};
const readable = fs.createReadStream(fileName); const readable = fs.createReadStream(fileName);
await new Promise(f => readable.on('readable', f)); await new Promise(f => readable.on('readable', f));
return { stream: new StreamDispatcher(this._scope, readable) }; return { stream: new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable)) };
} }
async failure(): Promise<channels.DownloadFailureResult> { async failure(): Promise<channels.DownloadFailureResult> {

View file

@ -31,6 +31,7 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche
import { FileChooser } from '../server/fileChooser'; import { FileChooser } from '../server/fileChooser';
import { CRCoverage } from '../server/chromium/crCoverage'; import { CRCoverage } from '../server/chromium/crCoverage';
import { JSHandle } from '../server/javascript'; import { JSHandle } from '../server/javascript';
import { SdkObject } from '../server/sdkObject';
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel { export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page; private _page: Page;
@ -264,13 +265,13 @@ export class WorkerDispatcher extends Dispatcher<Worker, channels.WorkerInitiali
} }
} }
export class BindingCallDispatcher extends Dispatcher<{}, channels.BindingCallInitializer> implements channels.BindingCallChannel { export class BindingCallDispatcher extends Dispatcher<SdkObject, channels.BindingCallInitializer> implements channels.BindingCallChannel {
private _resolve: ((arg: any) => void) | undefined; private _resolve: ((arg: any) => void) | undefined;
private _reject: ((error: any) => void) | undefined; private _reject: ((error: any) => void) | undefined;
private _promise: Promise<any>; private _promise: Promise<any>;
constructor(scope: DispatcherScope, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { constructor(scope: DispatcherScope, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
super(scope, {}, 'BindingCall', { super(scope, new SdkObject(null), 'BindingCall', {
frame: lookupDispatcher<FrameDispatcher>(source.frame), frame: lookupDispatcher<FrameDispatcher>(source.frame),
name, name,
args: needsHandle ? undefined : args.map(serializeResult), args: needsHandle ? undefined : args.map(serializeResult),

View file

@ -17,18 +17,27 @@
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope } from './dispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher';
import * as stream from 'stream'; import * as stream from 'stream';
import { SdkObject } from '../server/sdkObject';
export class StreamDispatcher extends Dispatcher<stream.Readable, channels.StreamInitializer> implements channels.StreamChannel { export class StreamWrapper extends SdkObject {
constructor(scope: DispatcherScope, stream: stream.Readable) { readonly stream: stream.Readable;
constructor(parentObject: SdkObject, stream: stream.Readable) {
super(parentObject);
this.stream = stream;
}
}
export class StreamDispatcher extends Dispatcher<StreamWrapper, channels.StreamInitializer> implements channels.StreamChannel {
constructor(scope: DispatcherScope, stream: StreamWrapper) {
super(scope, stream, 'Stream', {}); super(scope, stream, 'Stream', {});
} }
async read(params: channels.StreamReadParams): Promise<channels.StreamReadResult> { async read(params: channels.StreamReadParams): Promise<channels.StreamReadResult> {
const buffer = this._object.read(Math.min(this._object.readableLength, params.size || this._object.readableLength)); const buffer = this._object.stream.read(Math.min(this._object.stream.readableLength, params.size || this._object.stream.readableLength));
return { binary: buffer ? buffer.toString('base64') : '' }; return { binary: buffer ? buffer.toString('base64') : '' };
} }
async close() { async close() {
this._object.destroy(); this._object.stream.destroy();
} }
} }

View file

@ -32,11 +32,12 @@ import { RecentLogsCollector } from '../../utils/debugLogger';
import { TimeoutSettings } from '../../utils/timeoutSettings'; import { TimeoutSettings } from '../../utils/timeoutSettings';
import { AndroidWebView } from '../../protocol/channels'; import { AndroidWebView } from '../../protocol/channels';
import { CRPage } from '../chromium/crPage'; import { CRPage } from '../chromium/crPage';
import { SdkObject } from '../sdkObject';
const readFileAsync = util.promisify(fs.readFile); const readFileAsync = util.promisify(fs.readFile);
export interface Backend { export interface Backend {
devices(): Promise<DeviceBackend[]>; devices(owner: SdkObject): Promise<DeviceBackend[]>;
} }
export interface DeviceBackend { export interface DeviceBackend {
@ -44,22 +45,23 @@ export interface DeviceBackend {
status: string; status: string;
close(): Promise<void>; close(): Promise<void>;
init(): Promise<void>; init(): Promise<void>;
runCommand(command: string): Promise<Buffer>; runCommand(owner: SdkObject, command: string): Promise<Buffer>;
open(command: string): Promise<SocketBackend>; open(owner: SdkObject, command: string): Promise<SocketBackend>;
} }
export interface SocketBackend extends EventEmitter { export interface SocketBackend extends SdkObject {
write(data: Buffer): Promise<void>; write(data: Buffer): Promise<void>;
close(): Promise<void>; close(): Promise<void>;
} }
export class Android { export class Android extends SdkObject {
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; readonly _playwrightOptions: PlaywrightOptions;
constructor(backend: Backend, playwrightOptions: PlaywrightOptions) { constructor(backend: Backend, playwrightOptions: PlaywrightOptions) {
super(playwrightOptions.rootSdkObject);
this._backend = backend; this._backend = backend;
this._playwrightOptions = playwrightOptions; this._playwrightOptions = playwrightOptions;
this._timeoutSettings = new TimeoutSettings(); this._timeoutSettings = new TimeoutSettings();
@ -70,7 +72,7 @@ export class Android {
} }
async devices(): Promise<AndroidDevice[]> { async devices(): Promise<AndroidDevice[]> {
const devices = (await this._backend.devices()).filter(d => d.status === 'device'); const devices = (await this._backend.devices(this)).filter(d => d.status === 'device');
const newSerials = new Set<string>(); const newSerials = new Set<string>();
for (const d of devices) { for (const d of devices) {
newSerials.add(d.serial); newSerials.add(d.serial);
@ -91,7 +93,7 @@ export class Android {
} }
} }
export class AndroidDevice extends EventEmitter { export class AndroidDevice extends SdkObject {
readonly _backend: DeviceBackend; readonly _backend: DeviceBackend;
readonly model: string; readonly model: string;
readonly serial: string; readonly serial: string;
@ -113,8 +115,7 @@ export class AndroidDevice extends EventEmitter {
private _isClosed = false; private _isClosed = false;
constructor(android: Android, backend: DeviceBackend, model: string) { constructor(android: Android, backend: DeviceBackend, model: string) {
super(); super(android);
this.setMaxListeners(0);
this._android = android; this._android = android;
this._backend = backend; this._backend = backend;
this.model = model; this.model = model;
@ -124,7 +125,7 @@ export class AndroidDevice extends EventEmitter {
static async create(android: Android, backend: DeviceBackend): Promise<AndroidDevice> { static async create(android: Android, backend: DeviceBackend): Promise<AndroidDevice> {
await backend.init(); await backend.init();
const model = await backend.runCommand('shell:getprop ro.product.model'); const model = await backend.runCommand(android, 'shell:getprop ro.product.model');
const device = new AndroidDevice(android, backend, model.toString().trim()); const device = new AndroidDevice(android, backend, model.toString().trim());
await device._init(); await device._init();
return device; return device;
@ -143,17 +144,17 @@ export class AndroidDevice extends EventEmitter {
} }
async shell(command: string): Promise<Buffer> { async shell(command: string): Promise<Buffer> {
const result = await this._backend.runCommand(`shell:${command}`); const result = await this._backend.runCommand(this, `shell:${command}`);
await this._refreshWebViews(); await this._refreshWebViews();
return result; return result;
} }
async open(command: string): Promise<SocketBackend> { async open(command: string): Promise<SocketBackend> {
return await this._backend.open(`${command}`); return await this._backend.open(this, `${command}`);
} }
async screenshot(): Promise<Buffer> { async screenshot(): Promise<Buffer> {
return await this._backend.runCommand(`shell:screencap -p`); return await this._backend.runCommand(this, `shell:screencap -p`);
} }
private async _driver(): Promise<Transport> { private async _driver(): Promise<Transport> {
@ -198,7 +199,7 @@ export class AndroidDevice extends EventEmitter {
debug('pw:android')(`Polling the socket localabstract:${socketName}`); debug('pw:android')(`Polling the socket localabstract:${socketName}`);
while (!socket) { while (!socket) {
try { try {
socket = await this._backend.open(`localabstract:${socketName}`); socket = await this._backend.open(this, `localabstract:${socketName}`);
} catch (e) { } catch (e) {
await new Promise(f => setTimeout(f, 250)); await new Promise(f => setTimeout(f, 250));
} }
@ -234,13 +235,13 @@ export class AndroidDevice extends EventEmitter {
async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise<BrowserContext> { async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
debug('pw:android')('Force-stopping', pkg); debug('pw:android')('Force-stopping', pkg);
await this._backend.runCommand(`shell:am force-stop ${pkg}`); await this._backend.runCommand(this, `shell:am force-stop ${pkg}`);
const socketName = 'playwright-' + createGuid(); const socketName = 'playwright-' + createGuid();
const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`; const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
debug('pw:android')('Starting', pkg, commandLine); debug('pw:android')('Starting', pkg, commandLine);
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`); await this._backend.runCommand(this, `shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
await this._backend.runCommand(`shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`); await this._backend.runCommand(this, `shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`);
return await this._connectToBrowser(socketName, options); return await this._connectToBrowser(socketName, options);
} }
@ -295,7 +296,7 @@ export class AndroidDevice extends EventEmitter {
async installApk(content: Buffer, options?: { args?: string[] }): Promise<void> { async installApk(content: Buffer, options?: { args?: string[] }): Promise<void> {
const args = options && options.args ? options.args : ['-r', '-t', '-S']; const args = options && options.args ? options.args : ['-r', '-t', '-S'];
debug('pw:android')('Opening install socket'); debug('pw:android')('Opening install socket');
const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`); const installSocket = await this._backend.open(this, `shell:cmd package install ${args.join(' ')} ${content.length}`);
debug('pw:android')('Writing driver bytes: ' + content.length); debug('pw:android')('Writing driver bytes: ' + content.length);
await installSocket.write(content); await installSocket.write(content);
const success = await new Promise(f => installSocket.on('data', f)); const success = await new Promise(f => installSocket.on('data', f));
@ -304,7 +305,7 @@ export class AndroidDevice extends EventEmitter {
} }
async push(content: Buffer, path: string, mode = 0o644): Promise<void> { async push(content: Buffer, path: string, mode = 0o644): Promise<void> {
const socket = await this._backend.open(`sync:`); const socket = await this._backend.open(this, `sync:`);
const sendHeader = async (command: string, length: number) => { const sendHeader = async (command: string, length: number) => {
const buffer = Buffer.alloc(command.length + 4); const buffer = Buffer.alloc(command.length + 4);
buffer.write(command, 0); buffer.write(command, 0);
@ -328,7 +329,7 @@ export class AndroidDevice extends EventEmitter {
} }
private async _refreshWebViews() { private async _refreshWebViews() {
const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n'); const sockets = (await this._backend.runCommand(this, `shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
if (this._isClosed) if (this._isClosed)
return; return;
@ -344,7 +345,7 @@ export class AndroidDevice extends EventEmitter {
if (this._webViews.has(pid)) if (this._webViews.has(pid))
continue; continue;
const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n'); const procs = (await this._backend.runCommand(this, `shell:ps -A | grep ${pid}`)).toString().split('\n');
if (this._isClosed) if (this._isClosed)
return; return;
let pkg = ''; let pkg = '';

View file

@ -17,12 +17,12 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as debug from 'debug'; import * as debug from 'debug';
import * as net from 'net'; import * as net from 'net';
import { EventEmitter } from 'ws'; import { SdkObject } from '../sdkObject';
import { Backend, DeviceBackend, SocketBackend } from './android'; import { Backend, DeviceBackend, SocketBackend } from './android';
export class AdbBackend implements Backend { export class AdbBackend implements Backend {
async devices(): Promise<DeviceBackend[]> { async devices(sdkObject: SdkObject): Promise<DeviceBackend[]> {
const result = await runCommand('host:devices'); const result = await runCommand(sdkObject, 'host:devices');
const lines = result.toString().trim().split('\n'); const lines = result.toString().trim().split('\n');
return lines.map(line => { return lines.map(line => {
const [serial, status] = line.trim().split('\t'); const [serial, status] = line.trim().split('\t');
@ -46,20 +46,20 @@ class AdbDevice implements DeviceBackend {
async close() { async close() {
} }
runCommand(command: string): Promise<Buffer> { runCommand(sdkObject: SdkObject, command: string): Promise<Buffer> {
return runCommand(command, this.serial); return runCommand(sdkObject, command, this.serial);
} }
async open(command: string): Promise<SocketBackend> { async open(sdkObject: SdkObject, command: string): Promise<SocketBackend> {
const result = await open(command, this.serial); const result = await open(sdkObject, command, this.serial);
result.becomeSocket(); result.becomeSocket();
return result; return result;
} }
} }
async function runCommand(command: string, serial?: string): Promise<Buffer> { async function runCommand(sdkObject: SdkObject, command: string, serial?: string): Promise<Buffer> {
debug('pw:adb:runCommand')(command, serial); debug('pw:adb:runCommand')(command, serial);
const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 }));
if (serial) { if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`)); await socket.write(encodeMessage(`host:transport:${serial}`));
const status = await socket.read(4); const status = await socket.read(4);
@ -79,8 +79,8 @@ async function runCommand(command: string, serial?: string): Promise<Buffer> {
return commandOutput; return commandOutput;
} }
async function open(command: string, serial?: string): Promise<BufferedSocketWrapper> { async function open(sdkObject: SdkObject, command: string, serial?: string): Promise<BufferedSocketWrapper> {
const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 }));
if (serial) { if (serial) {
await socket.write(encodeMessage(`host:transport:${serial}`)); await socket.write(encodeMessage(`host:transport:${serial}`));
const status = await socket.read(4); const status = await socket.read(4);
@ -98,7 +98,7 @@ function encodeMessage(message: string): Buffer {
return Buffer.from(lenHex + message); return Buffer.from(lenHex + message);
} }
class BufferedSocketWrapper extends EventEmitter implements SocketBackend { class BufferedSocketWrapper extends SdkObject implements SocketBackend {
private _socket: net.Socket; private _socket: net.Socket;
private _buffer = Buffer.from([]); private _buffer = Buffer.from([]);
private _isSocket = false; private _isSocket = false;
@ -107,9 +107,8 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
private _isClosed = false; private _isClosed = false;
private _command: string; private _command: string;
constructor(command: string, socket: net.Socket) { constructor(parent: SdkObject, command: string, socket: net.Socket) {
super(); super(parent);
this.setMaxListeners(0);
this._command = command; this._command = command;
this._socket = socket; this._socket = socket;
this._connectPromise = new Promise(f => this._socket.on('connect', f)); this._connectPromise = new Promise(f => this._socket.on('connect', f));

View file

@ -17,12 +17,13 @@
import * as types from './types'; import * as types from './types';
import { BrowserContext, ContextListener, Video } from './browserContext'; import { BrowserContext, ContextListener, Video } from './browserContext';
import { Page } from './page'; import { Page } from './page';
import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
import { ProxySettings } from './types'; import { ProxySettings } from './types';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { RecentLogsCollector } from '../utils/debugLogger'; import { RecentLogsCollector } from '../utils/debugLogger';
import * as registry from '../utils/registry'; import * as registry from '../utils/registry';
import { SdkObject } from './sdkObject';
import { Selectors } from './selectors';
export interface BrowserProcess { export interface BrowserProcess {
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined; onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
@ -34,7 +35,10 @@ export interface BrowserProcess {
export type PlaywrightOptions = { export type PlaywrightOptions = {
contextListeners: ContextListener[], contextListeners: ContextListener[],
registry: registry.Registry, registry: registry.Registry,
isInternal: boolean isInternal: boolean,
rootSdkObject: SdkObject,
// FIXME, this is suspicious
selectors: Selectors
}; };
export type BrowserOptions = PlaywrightOptions & { export type BrowserOptions = PlaywrightOptions & {
@ -50,7 +54,7 @@ export type BrowserOptions = PlaywrightOptions & {
slowMo?: number, slowMo?: number,
}; };
export abstract class Browser extends EventEmitter { export abstract class Browser extends SdkObject {
static Events = { static Events = {
Disconnected: 'disconnected', Disconnected: 'disconnected',
}; };
@ -62,8 +66,8 @@ export abstract class Browser extends EventEmitter {
readonly _idToVideo = new Map<string, Video>(); readonly _idToVideo = new Map<string, Video>();
constructor(options: BrowserOptions) { constructor(options: BrowserOptions) {
super(); super(options.rootSdkObject);
this.setMaxListeners(0); this.attribution.browser = this;
this.options = options; this.options = options;
} }

View file

@ -15,7 +15,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { EventEmitter } from 'events';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { mkdirIfNeeded } from '../utils/utils'; import { mkdirIfNeeded } from '../utils/utils';
import { Browser, BrowserOptions } from './browser'; import { Browser, BrowserOptions } from './browser';
@ -26,9 +25,10 @@ import { helper } from './helper';
import * as network from './network'; import * as network from './network';
import { Page, PageBinding, PageDelegate } from './page'; import { Page, PageBinding, PageDelegate } from './page';
import { Progress, ProgressController, ProgressResult } from './progress'; import { Progress, ProgressController, ProgressResult } from './progress';
import { Selectors, serverSelectors } from './selectors'; import { Selectors } from './selectors';
import * as types from './types'; import * as types from './types';
import * as path from 'path'; import * as path from 'path';
import { SdkObject } from './sdkObject';
export class Video { export class Video {
readonly _videoId: string; readonly _videoId: string;
@ -94,7 +94,7 @@ export interface ContextListener {
onContextDidDestroy(context: BrowserContext): Promise<void>; onContextDidDestroy(context: BrowserContext): Promise<void>;
} }
export abstract class BrowserContext extends EventEmitter { export abstract class BrowserContext extends SdkObject {
static Events = { static Events = {
Close: 'close', Close: 'close',
Page: 'page', Page: 'page',
@ -122,8 +122,8 @@ export abstract class BrowserContext extends EventEmitter {
terminalSize: { rows?: number, columns?: number } = {}; terminalSize: { rows?: number, columns?: number } = {};
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
super(); super(browser);
this.setMaxListeners(0); this.attribution.context = this;
this._browser = browser; this._browser = browser;
this._options = options; this._options = options;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
@ -135,8 +135,8 @@ export abstract class BrowserContext extends EventEmitter {
this._selectors = selectors; this._selectors = selectors;
} }
selectors() { selectors(): Selectors {
return this._selectors || serverSelectors; return this._selectors || this._browser.options.selectors;
} }
async _initialize() { async _initialize() {

View file

@ -31,18 +31,21 @@ import { validateHostRequirements } from './validateDependencies';
import { isDebugMode } from '../utils/utils'; import { isDebugMode } from '../utils/utils';
import { helper } from './helper'; import { helper } from './helper';
import { RecentLogsCollector } from '../utils/debugLogger'; import { RecentLogsCollector } from '../utils/debugLogger';
import { SdkObject } from './sdkObject';
const mkdirAsync = util.promisify(fs.mkdir); const mkdirAsync = util.promisify(fs.mkdir);
const mkdtempAsync = util.promisify(fs.mkdtemp); const mkdtempAsync = util.promisify(fs.mkdtemp);
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err))); const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
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 extends SdkObject {
private _name: registry.BrowserName; private _name: registry.BrowserName;
readonly _registry: registry.Registry; readonly _registry: registry.Registry;
readonly _playwrightOptions: PlaywrightOptions; readonly _playwrightOptions: PlaywrightOptions;
constructor(browserName: registry.BrowserName, playwrightOptions: PlaywrightOptions) { constructor(browserName: registry.BrowserName, playwrightOptions: PlaywrightOptions) {
super(playwrightOptions.rootSdkObject);
this.attribution.browserType = this;
this._playwrightOptions = playwrightOptions; this._playwrightOptions = playwrightOptions;
this._name = browserName; this._name = browserName;
this._registry = playwrightOptions.registry; this._registry = playwrightOptions.registry;

View file

@ -263,7 +263,7 @@ class CRServiceWorker extends Worker {
readonly _browserContext: CRBrowserContext; readonly _browserContext: CRBrowserContext;
constructor(browserContext: CRBrowserContext, session: CRSession, url: string) { constructor(browserContext: CRBrowserContext, session: CRSession, url: string) {
super(url); super(browserContext, url);
this._browserContext = browserContext; this._browserContext = browserContext;
session.once('Runtime.executionContextCreated', event => { session.once('Runtime.executionContextCreated', event => {
this._createExecutionContext(new CRExecutionContext(session, event.context)); this._createExecutionContext(new CRExecutionContext(session, event.context));

View file

@ -23,6 +23,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types'; import { ProtocolLogger } from '../types';
import { helper } from '../helper'; import { helper } from '../helper';
import { SdkObject } from '../sdkObject';
export const ConnectionEvents = { export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected') Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -123,7 +124,7 @@ export const CRSessionEvents = {
Disconnected: Symbol('Events.CDPSession.Disconnected') Disconnected: Symbol('Events.CDPSession.Disconnected')
}; };
export class CRSession extends EventEmitter { export class CRSession extends SdkObject {
_connection: CRConnection | null; _connection: CRConnection | null;
_eventListener?: (method: string, params?: Object) => void; _eventListener?: (method: string, params?: Object) => void;
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>(); private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
@ -139,8 +140,7 @@ export class CRSession extends EventEmitter {
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
constructor(connection: CRConnection, rootSessionId: string, targetType: string, sessionId: string) { constructor(connection: CRConnection, rootSessionId: string, targetType: string, sessionId: string) {
super(); super(null);
this.setMaxListeners(0);
this._connection = connection; this._connection = connection;
this._rootSessionId = rootSessionId; this._rootSessionId = rootSessionId;
this._targetType = targetType; this._targetType = targetType;

View file

@ -639,7 +639,7 @@ class FrameSession {
} }
const url = event.targetInfo.url; const url = event.targetInfo.url;
const worker = new Worker(url); const worker = new Worker(this._page, url);
this._page._addWorker(event.sessionId, worker); this._page._addWorker(event.sessionId, worker);
session.once('Runtime.executionContextCreated', async event => { session.once('Runtime.executionContextCreated', async event => {
worker._createExecutionContext(new CRExecutionContext(session, event.context)); worker._createExecutionContext(new CRExecutionContext(session, event.context));
@ -759,7 +759,7 @@ class FrameSession {
lineNumber: lineNumber || 0, lineNumber: lineNumber || 0,
columnNumber: 0, columnNumber: 0,
}; };
this._page.emit(Page.Events.Console, new ConsoleMessage(level, text, [], location)); this._page.emit(Page.Events.Console, new ConsoleMessage(this._page, level, text, [], location));
} }
} }

View file

@ -15,15 +15,17 @@
*/ */
import * as js from './javascript'; import * as js from './javascript';
import { SdkObject } from './sdkObject';
import { ConsoleMessageLocation } from './types'; import { ConsoleMessageLocation } from './types';
export class ConsoleMessage { export class ConsoleMessage extends SdkObject {
private _type: string; private _type: string;
private _text?: string; private _text?: string;
private _args: js.JSHandle[]; private _args: js.JSHandle[];
private _location: ConsoleMessageLocation; private _location: ConsoleMessageLocation;
constructor(type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { constructor(parent: SdkObject, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) {
super(parent);
this._type = type; this._type = type;
this._text = text; this._text = text;
this._args = args; this._args = args;

View file

@ -17,12 +17,13 @@
import { assert } from '../utils/utils'; import { assert } from '../utils/utils';
import { Page } from './page'; import { Page } from './page';
import { SdkObject } from './sdkObject';
type OnHandle = (accept: boolean, promptText?: string) => Promise<void>; type OnHandle = (accept: boolean, promptText?: string) => Promise<void>;
export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt'; export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt';
export class Dialog { export class Dialog extends SdkObject {
private _page: Page; private _page: Page;
private _type: string; private _type: string;
private _message: string; private _message: string;
@ -31,6 +32,7 @@ export class Dialog {
private _defaultValue: string; private _defaultValue: string;
constructor(page: Page, type: string, message: string, onHandle: OnHandle, defaultValue?: string) { constructor(page: Page, type: string, message: string, onHandle: OnHandle, defaultValue?: string) {
super(page);
this._page = page; this._page = page;
this._type = type; this._type = type;
this._message = message; this._message = message;

View file

@ -31,7 +31,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
readonly world: types.World | null; readonly world: types.World | null;
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) { constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) {
super(delegate); super(frame, delegate);
this.frame = frame; this.frame = frame;
this.world = world; this.world = world;
} }

View file

@ -19,10 +19,11 @@ import * as fs from 'fs';
import * as util from 'util'; import * as util from 'util';
import { Page } from './page'; import { Page } from './page';
import { assert } from '../utils/utils'; import { assert } from '../utils/utils';
import { SdkObject } from './sdkObject';
type SaveCallback = (localPath: string, error?: string) => Promise<void>; type SaveCallback = (localPath: string, error?: string) => Promise<void>;
export class Download { export class Download extends SdkObject {
private _downloadsPath: string; private _downloadsPath: string;
private _uuid: string; private _uuid: string;
private _finishedCallback: () => void; private _finishedCallback: () => void;
@ -37,6 +38,7 @@ export class Download {
private _suggestedFilename: string | undefined; private _suggestedFilename: string | undefined;
constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) { constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) {
super(page);
this._page = page; this._page = page;
this._downloadsPath = downloadsPath; this._downloadsPath = downloadsPath;
this._uuid = uuid; this._uuid = uuid;

View file

@ -27,12 +27,12 @@ import { launchProcess, envArrayToObject } from '../processLauncher';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import type {BrowserWindow} from 'electron'; import type {BrowserWindow} from 'electron';
import { Progress, ProgressController, runAbortableTask } from '../progress'; import { Progress, ProgressController, runAbortableTask } from '../progress';
import { EventEmitter } from 'events';
import { helper } from '../helper'; import { helper } from '../helper';
import { BrowserOptions, BrowserProcess, PlaywrightOptions } 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';
import { SdkObject } from '../sdkObject';
export type ElectronLaunchOptionsBase = { export type ElectronLaunchOptionsBase = {
executablePath?: string, executablePath?: string,
@ -47,7 +47,7 @@ export interface ElectronPage extends Page {
_browserWindowId: number; _browserWindowId: number;
} }
export class ElectronApplication extends EventEmitter { export class ElectronApplication extends SdkObject {
static Events = { static Events = {
Close: 'close', Close: 'close',
Window: 'window', Window: 'window',
@ -62,9 +62,8 @@ export class ElectronApplication extends EventEmitter {
private _lastWindowId = 0; private _lastWindowId = 0;
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
constructor(browser: CRBrowser, nodeConnection: CRConnection) { constructor(parent: SdkObject, browser: CRBrowser, nodeConnection: CRConnection) {
super(); super(parent);
this.setMaxListeners(0);
this._browserContext = browser._defaultContext as CRBrowserContext; this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(BrowserContext.Events.Close, () => { this._browserContext.on(BrowserContext.Events.Close, () => {
// Emit application closed after context closed. // Emit application closed after context closed.
@ -115,17 +114,18 @@ export class ElectronApplication extends EventEmitter {
async _init() { async _init() {
this._nodeSession.on('Runtime.executionContextCreated', (event: any) => { this._nodeSession.on('Runtime.executionContextCreated', (event: any) => {
if (event.context.auxData && event.context.auxData.isDefault) if (event.context.auxData && event.context.auxData.isDefault)
this._nodeExecutionContext = new js.ExecutionContext(new CRExecutionContext(this._nodeSession, event.context)); this._nodeExecutionContext = new js.ExecutionContext(this, new CRExecutionContext(this._nodeSession, event.context));
}); });
await this._nodeSession.send('Runtime.enable', {}).catch(e => {}); await this._nodeSession.send('Runtime.enable', {}).catch(e => {});
this._nodeElectronHandle = await js.evaluate(this._nodeExecutionContext!, false /* returnByValue */, `process.mainModule.require('electron')`); this._nodeElectronHandle = await js.evaluate(this._nodeExecutionContext!, false /* returnByValue */, `process.mainModule.require('electron')`);
} }
} }
export class Electron { export class Electron extends SdkObject {
private _playwrightOptions: PlaywrightOptions; private _playwrightOptions: PlaywrightOptions;
constructor(playwrightOptions: PlaywrightOptions) { constructor(playwrightOptions: PlaywrightOptions) {
super(playwrightOptions.rootSdkObject);
this._playwrightOptions = playwrightOptions; this._playwrightOptions = playwrightOptions;
} }
@ -187,7 +187,7 @@ export class Electron {
browserLogsCollector, browserLogsCollector,
}; };
const browser = await CRBrowser.connect(chromeTransport, browserOptions); const browser = await CRBrowser.connect(chromeTransport, browserOptions);
app = new ElectronApplication(browser, nodeConnection); app = new ElectronApplication(this, browser, nodeConnection);
await app._init(); await app._init();
return app; return app;
}, TimeoutSettings.timeout(options)); }, TimeoutSettings.timeout(options));

View file

@ -244,7 +244,7 @@ export class FFPage implements PageDelegate {
async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) { async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) {
const workerId = event.workerId; const workerId = event.workerId;
const worker = new Worker(event.url); const worker = new Worker(this._page, event.url);
const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => { const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => {
this._session.send('Page.sendMessageToWorker', { this._session.send('Page.sendMessageToWorker', {
frameId: event.frameId, frameId: event.frameId,

View file

@ -24,9 +24,9 @@ import { Page } from './page';
import * as types from './types'; import * as types from './types';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
import { Progress, ProgressController, runAbortableTask } from './progress'; import { Progress, ProgressController, runAbortableTask } from './progress';
import { EventEmitter } from 'events';
import { assert, makeWaitForNextTask } from '../utils/utils'; import { assert, makeWaitForNextTask } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
import { SdkObject } from './sdkObject';
type ContextData = { type ContextData = {
contextPromise: Promise<dom.FrameExecutionContext>; contextPromise: Promise<dom.FrameExecutionContext>;
@ -342,7 +342,7 @@ export class FrameManager {
} }
onWebSocketCreated(requestId: string, url: string) { onWebSocketCreated(requestId: string, url: string) {
const ws = new network.WebSocket(url); const ws = new network.WebSocket(this._page, url);
this._webSockets.set(requestId, ws); this._webSockets.set(requestId, ws);
} }
@ -386,7 +386,7 @@ export class FrameManager {
} }
} }
export class Frame extends EventEmitter { export class Frame extends SdkObject {
static Events = { static Events = {
Navigation: 'navigation', Navigation: 'navigation',
AddLifecycle: 'addlifecycle', AddLifecycle: 'addlifecycle',
@ -412,8 +412,8 @@ export class Frame extends EventEmitter {
private _detachedCallback = () => {}; private _detachedCallback = () => {};
constructor(page: Page, id: string, parentFrame: Frame | null) { constructor(page: Page, id: string, parentFrame: Frame | null) {
super(); super(page);
this.setMaxListeners(0); this.attribution.frame = this;
this._id = id; this._id = id;
this._page = page; this._page = page;
this._parentFrame = parentFrame; this._parentFrame = parentFrame;

View file

@ -18,6 +18,7 @@ import * as dom from './dom';
import * as utilityScriptSource from '../generated/utilityScriptSource'; import * as utilityScriptSource from '../generated/utilityScriptSource';
import { serializeAsCallArgument } from './common/utilityScriptSerializers'; import { serializeAsCallArgument } from './common/utilityScriptSerializers';
import type UtilityScript from './injected/utilityScript'; import type UtilityScript from './injected/utilityScript';
import { SdkObject } from './sdkObject';
type ObjectId = string; type ObjectId = string;
export type RemoteObject = { export type RemoteObject = {
@ -49,11 +50,12 @@ export interface ExecutionContextDelegate {
releaseHandle(handle: JSHandle): Promise<void>; releaseHandle(handle: JSHandle): Promise<void>;
} }
export class ExecutionContext { export class ExecutionContext extends SdkObject {
readonly _delegate: ExecutionContextDelegate; readonly _delegate: ExecutionContextDelegate;
private _utilityScriptPromise: Promise<JSHandle> | undefined; private _utilityScriptPromise: Promise<JSHandle> | undefined;
constructor(delegate: ExecutionContextDelegate) { constructor(parent: SdkObject, delegate: ExecutionContextDelegate) {
super(parent);
this._delegate = delegate; this._delegate = delegate;
} }
@ -82,7 +84,7 @@ export class ExecutionContext {
} }
} }
export class JSHandle<T = any> { export class JSHandle<T = any> extends SdkObject {
readonly _context: ExecutionContext; readonly _context: ExecutionContext;
_disposed = false; _disposed = false;
readonly _objectId: ObjectId | undefined; readonly _objectId: ObjectId | undefined;
@ -92,6 +94,7 @@ export class JSHandle<T = any> {
private _previewCallback: ((preview: string) => void) | undefined; private _previewCallback: ((preview: string) => void) | undefined;
constructor(context: ExecutionContext, type: string, objectId?: ObjectId, value?: any) { constructor(context: ExecutionContext, type: string, objectId?: ObjectId, value?: any) {
super(context);
this._context = context; this._context = context;
this._objectId = objectId; this._objectId = objectId;
this._value = value; this._value = value;

View file

@ -17,7 +17,7 @@
import * as frames from './frames'; import * as frames from './frames';
import * as types from './types'; import * as types from './types';
import { assert } from '../utils/utils'; import { assert } from '../utils/utils';
import { EventEmitter } from 'events'; import { SdkObject } from './sdkObject';
export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] {
const parsedURLs = urls.map(s => new URL(s)); const parsedURLs = urls.map(s => new URL(s));
@ -78,7 +78,7 @@ export function stripFragmentFromUrl(url: string): string {
return url.substring(0, url.indexOf('#')); return url.substring(0, url.indexOf('#'));
} }
export class Request { export class Request extends SdkObject {
readonly _routeDelegate: RouteDelegate | null; readonly _routeDelegate: RouteDelegate | null;
private _response: Response | null = null; private _response: Response | null = null;
private _redirectedFrom: Request | null; private _redirectedFrom: Request | null;
@ -99,6 +99,7 @@ export class Request {
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined, constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) { url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) {
super(frame);
assert(!url.startsWith('data:'), 'Data urls should not fire requests'); assert(!url.startsWith('data:'), 'Data urls should not fire requests');
assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects'); assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects');
this._routeDelegate = routeDelegate; this._routeDelegate = routeDelegate;
@ -203,12 +204,13 @@ export class Request {
} }
} }
export class Route { export class Route extends SdkObject {
private readonly _request: Request; private readonly _request: Request;
private readonly _delegate: RouteDelegate; private readonly _delegate: RouteDelegate;
private _handled = false; private _handled = false;
constructor(request: Request, delegate: RouteDelegate) { constructor(request: Request, delegate: RouteDelegate) {
super(request.frame());
this._request = request; this._request = request;
this._delegate = delegate; this._delegate = delegate;
} }
@ -261,7 +263,7 @@ export type ResourceTiming = {
responseStart: number; responseStart: number;
}; };
export class Response { export class Response extends SdkObject {
private _request: Request; private _request: Request;
private _contentPromise: Promise<Buffer> | null = null; private _contentPromise: Promise<Buffer> | null = null;
_finishedPromise: Promise<{ error?: string }>; _finishedPromise: Promise<{ error?: string }>;
@ -275,6 +277,7 @@ export class Response {
private _timing: ResourceTiming; private _timing: ResourceTiming;
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback) { constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback) {
super(request.frame());
this._request = request; this._request = request;
this._timing = timing; this._timing = timing;
this._status = status; this._status = status;
@ -343,7 +346,7 @@ export class Response {
} }
} }
export class WebSocket extends EventEmitter { export class WebSocket extends SdkObject {
private _url: string; private _url: string;
static Events = { static Events = {
@ -353,9 +356,8 @@ export class WebSocket extends EventEmitter {
FrameSent: 'framesent', FrameSent: 'framesent',
}; };
constructor(url: string) { constructor(parent: SdkObject, url: string) {
super(); super(parent);
this.setMaxListeners(0);
this._url = url; this._url = url;
} }

View file

@ -26,12 +26,12 @@ import * as types from './types';
import { BrowserContext, Video } from './browserContext'; import { BrowserContext, Video } from './browserContext';
import { ConsoleMessage } from './console'; import { ConsoleMessage } from './console';
import * as accessibility from './accessibility'; import * as accessibility from './accessibility';
import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import { ProgressController, runAbortableTask } from './progress'; import { ProgressController, runAbortableTask } from './progress';
import { assert, isError } from '../utils/utils'; import { assert, isError } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
import { Selectors } from './selectors'; import { Selectors } from './selectors';
import { SdkObject } from './sdkObject';
export interface PageDelegate { export interface PageDelegate {
readonly rawMouse: input.RawMouse; readonly rawMouse: input.RawMouse;
@ -92,7 +92,7 @@ type PageState = {
extraHTTPHeaders: types.HeadersArray | null; extraHTTPHeaders: types.HeadersArray | null;
}; };
export class Page extends EventEmitter { export class Page extends SdkObject {
static Events = { static Events = {
Close: 'close', Close: 'close',
Crash: 'crash', Crash: 'crash',
@ -149,8 +149,8 @@ export class Page extends EventEmitter {
_video: Video | null = null; _video: Video | null = null;
constructor(delegate: PageDelegate, browserContext: BrowserContext) { constructor(delegate: PageDelegate, browserContext: BrowserContext) {
super(); super(browserContext);
this.setMaxListeners(0); this.attribution.page = this;
this._delegate = delegate; this._delegate = delegate;
this._closedCallback = () => {}; this._closedCallback = () => {};
this._closedPromise = new Promise(f => this._closedCallback = f); this._closedPromise = new Promise(f => this._closedCallback = f);
@ -288,7 +288,7 @@ export class Page extends EventEmitter {
} }
_addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { _addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) {
const message = new ConsoleMessage(type, text, args, location); const message = new ConsoleMessage(this, type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message); const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Page.Events.Console)) if (intercepted || !this.listenerCount(Page.Events.Console))
args.forEach(arg => arg.dispose()); args.forEach(arg => arg.dispose());
@ -502,7 +502,7 @@ export class Page extends EventEmitter {
} }
} }
export class Worker extends EventEmitter { export class Worker extends SdkObject {
static Events = { static Events = {
Close: 'close', Close: 'close',
}; };
@ -512,16 +512,15 @@ export class Worker extends EventEmitter {
private _executionContextCallback: (value: js.ExecutionContext) => void; private _executionContextCallback: (value: js.ExecutionContext) => void;
_existingExecutionContext: js.ExecutionContext | null = null; _existingExecutionContext: js.ExecutionContext | null = null;
constructor(url: string) { constructor(parent: SdkObject, url: string) {
super(); super(parent);
this.setMaxListeners(0);
this._url = url; this._url = url;
this._executionContextCallback = () => {}; this._executionContextCallback = () => {};
this._executionContextPromise = new Promise(x => this._executionContextCallback = x); this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
} }
_createExecutionContext(delegate: js.ExecutionContextDelegate) { _createExecutionContext(delegate: js.ExecutionContextDelegate) {
this._existingExecutionContext = new js.ExecutionContext(delegate); this._existingExecutionContext = new js.ExecutionContext(this, delegate);
this._executionContextCallback(this._existingExecutionContext); this._executionContextCallback(this._existingExecutionContext);
} }

View file

@ -22,14 +22,15 @@ import { PlaywrightOptions } from './browser';
import { Chromium } from './chromium/chromium'; 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 { Selectors } from './selectors';
import { HarTracer } from './supplements/har/harTracer'; import { HarTracer } from './supplements/har/harTracer';
import { InspectorController } from './supplements/inspectorController'; import { InspectorController } from './supplements/inspectorController';
import { WebKit } from './webkit/webkit'; import { WebKit } from './webkit/webkit';
import { Registry } from '../utils/registry'; import { Registry } from '../utils/registry';
import { SdkObject } from './sdkObject';
export class Playwright { export class Playwright extends SdkObject {
readonly selectors = serverSelectors; readonly selectors: Selectors;
readonly chromium: Chromium; readonly chromium: Chromium;
readonly android: Android; readonly android: Android;
readonly electron: Electron; readonly electron: Electron;
@ -38,6 +39,8 @@ export class Playwright {
readonly options: PlaywrightOptions; readonly options: PlaywrightOptions;
constructor(isInternal: boolean) { constructor(isInternal: boolean) {
super(null);
this.selectors = new Selectors(this);
this.options = { this.options = {
isInternal, isInternal,
registry: new Registry(path.join(__dirname, '..', '..')), registry: new Registry(path.join(__dirname, '..', '..')),
@ -46,7 +49,9 @@ export class Playwright {
new InspectorController(), new InspectorController(),
new Tracer(), new Tracer(),
new HarTracer() new HarTracer()
] ],
rootSdkObject: this,
selectors: this.selectors
}; };
this.chromium = new Chromium(this.options); this.chromium = new Chromium(this.options);
this.firefox = new Firefox(this.options); this.firefox = new Firefox(this.options);

39
src/server/sdkObject.ts Normal file
View file

@ -0,0 +1,39 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* 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 { EventEmitter } from 'events';
import type { Browser } from './browser';
import type { BrowserContext } from './browserContext';
import type { BrowserType } from './browserType';
import type { Frame } from './frames';
import type { Page } from './page';
export type Attribution = {
browserType?: BrowserType;
browser?: Browser;
context?: BrowserContext;
page?: Page;
frame?: Frame;
};
export class SdkObject extends EventEmitter {
attribution: Attribution;
constructor(parent: SdkObject | null) {
super();
this.setMaxListeners(0);
this.attribution = { ...parent?.attribution };
}
}

View file

@ -19,6 +19,7 @@ import * as frames from './frames';
import * as js from './javascript'; import * as js from './javascript';
import * as types from './types'; import * as types from './types';
import { ParsedSelector, parseSelector } from './common/selectorParser'; import { ParsedSelector, parseSelector } from './common/selectorParser';
import { SdkObject } from './sdkObject';
export type SelectorInfo = { export type SelectorInfo = {
parsed: ParsedSelector, parsed: ParsedSelector,
@ -26,11 +27,12 @@ export type SelectorInfo = {
selector: string, selector: string,
}; };
export class Selectors { export class Selectors extends SdkObject {
readonly _builtinEngines: Set<string>; readonly _builtinEngines: Set<string>;
readonly _engines: Map<string, { source: string, contentScript: boolean }>; readonly _engines: Map<string, { source: string, contentScript: boolean }>;
constructor() { constructor(parent: SdkObject) {
super(parent);
// Note: keep in sync with InjectedScript class. // Note: keep in sync with InjectedScript class.
this._builtinEngines = new Set([ this._builtinEngines = new Set([
'css', 'css:light', 'css', 'css:light',
@ -134,4 +136,6 @@ export class Selectors {
} }
} }
export const serverSelectors = new Selectors(); export function serverSelectors(parent: SdkObject) {
return new Selectors(parent);
}

View file

@ -189,7 +189,7 @@ function isSharedLib(basename: string) {
async function executablesOrSharedLibraries(directoryPath: string): Promise<string[]> { async function executablesOrSharedLibraries(directoryPath: string): Promise<string[]> {
const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file)); const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file));
const allStats = await Promise.all(allPaths.map(aPath => statAsync(aPath))); const allStats = await Promise.all(allPaths.map(aPath => statAsync(aPath)));
const filePaths = allPaths.filter((aPath, index) => allStats[index].isFile()); const filePaths = allPaths.filter((aPath, index) => (allStats[index] as any).isFile());
const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => { const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => {
const basename = path.basename(filePath).toLowerCase(); const basename = path.basename(filePath).toLowerCase();

View file

@ -35,7 +35,7 @@ export class WKWorkers {
this.clear(); this.clear();
this._sessionListeners = [ this._sessionListeners = [
helper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => { helper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => {
const worker = new Worker(event.url); const worker = new Worker(this._page, event.url);
const workerSession = new WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message: any) => { const workerSession = new WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message: any) => {
session.send('Worker.sendMessageToWorker', { session.send('Worker.sendMessageToWorker', {
workerId: event.workerId, workerId: event.workerId,

View file

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "es2018",
"module": "commonjs", "module": "commonjs",
"lib": ["esnext", "dom", "DOM.Iterable"], "lib": ["esnext", "dom", "DOM.Iterable"],
"sourceMap": true, "sourceMap": true,