chore: introduce sdk object base class (#5370)

This commit is contained in:
Pavel Feldman 2021-02-09 09:00:00 -08:00 коммит произвёл GitHub
Родитель 909544907c
Коммит 0652f3251f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
31 изменённых файлов: 215 добавлений и 125 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -21,6 +21,7 @@ import { createScheme, Validator, ValidationError } from '../protocol/validator'
import { assert, createGuid, debugAssert, isUnderTest } from '../utils/utils';
import { tOptional } from '../protocol/validatorPrimitives';
import { kBrowserOrContextClosedError } from '../utils/errors';
import { SdkObject } from '../server/sdkObject';
export const dispatcherSymbol = Symbol('dispatcher');
@ -38,7 +39,14 @@ export function lookupNullableDispatcher<DispatcherType>(object: any | null): Di
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 _isScope: boolean;
// Parent is always "isScope".
@ -112,10 +120,9 @@ export class Dispatcher<Type, Initializer> extends EventEmitter implements chann
}
export type DispatcherScope = Dispatcher<any, any>;
class Root extends Dispatcher<{}, {}> {
class Root extends Dispatcher<SdkObject, {}> {
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);
if (typeof (dispatcher as any)[method] !== 'function')
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) });
} catch (e) {
this.onmessage({ id, error: serializeError(e) });

Просмотреть файл

@ -17,7 +17,7 @@
import { Download } from '../server/download';
import * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher';
import { StreamDispatcher, StreamWrapper } from './streamDispatcher';
import * as fs from 'fs';
import * as util from 'util';
import { mkdirIfNeeded } from '../utils/utils';
@ -65,7 +65,7 @@ export class DownloadDispatcher extends Dispatcher<Download, channels.DownloadIn
try {
const readable = fs.createReadStream(localPath);
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({ stream });
// Block the download until the stream is consumed.
@ -87,7 +87,7 @@ export class DownloadDispatcher extends Dispatcher<Download, channels.DownloadIn
return {};
const readable = fs.createReadStream(fileName);
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> {

Просмотреть файл

@ -31,6 +31,7 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche
import { FileChooser } from '../server/fileChooser';
import { CRCoverage } from '../server/chromium/crCoverage';
import { JSHandle } from '../server/javascript';
import { SdkObject } from '../server/sdkObject';
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
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 _reject: ((error: any) => void) | undefined;
private _promise: Promise<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),
name,
args: needsHandle ? undefined : args.map(serializeResult),

Просмотреть файл

@ -17,18 +17,27 @@
import * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope } from './dispatcher';
import * as stream from 'stream';
import { SdkObject } from '../server/sdkObject';
export class StreamDispatcher extends Dispatcher<stream.Readable, channels.StreamInitializer> implements channels.StreamChannel {
constructor(scope: DispatcherScope, stream: stream.Readable) {
export class StreamWrapper extends SdkObject {
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', {});
}
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') : '' };
}
async close() {
this._object.destroy();
this._object.stream.destroy();
}
}

Просмотреть файл

@ -32,11 +32,12 @@ import { RecentLogsCollector } from '../../utils/debugLogger';
import { TimeoutSettings } from '../../utils/timeoutSettings';
import { AndroidWebView } from '../../protocol/channels';
import { CRPage } from '../chromium/crPage';
import { SdkObject } from '../sdkObject';
const readFileAsync = util.promisify(fs.readFile);
export interface Backend {
devices(): Promise<DeviceBackend[]>;
devices(owner: SdkObject): Promise<DeviceBackend[]>;
}
export interface DeviceBackend {
@ -44,22 +45,23 @@ export interface DeviceBackend {
status: string;
close(): Promise<void>;
init(): Promise<void>;
runCommand(command: string): Promise<Buffer>;
open(command: string): Promise<SocketBackend>;
runCommand(owner: SdkObject, command: string): Promise<Buffer>;
open(owner: SdkObject, command: string): Promise<SocketBackend>;
}
export interface SocketBackend extends EventEmitter {
export interface SocketBackend extends SdkObject {
write(data: Buffer): Promise<void>;
close(): Promise<void>;
}
export class Android {
export class Android extends SdkObject {
private _backend: Backend;
private _devices = new Map<string, AndroidDevice>();
readonly _timeoutSettings: TimeoutSettings;
readonly _playwrightOptions: PlaywrightOptions;
constructor(backend: Backend, playwrightOptions: PlaywrightOptions) {
super(playwrightOptions.rootSdkObject);
this._backend = backend;
this._playwrightOptions = playwrightOptions;
this._timeoutSettings = new TimeoutSettings();
@ -70,7 +72,7 @@ export class Android {
}
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>();
for (const d of devices) {
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 model: string;
readonly serial: string;
@ -113,8 +115,7 @@ export class AndroidDevice extends EventEmitter {
private _isClosed = false;
constructor(android: Android, backend: DeviceBackend, model: string) {
super();
this.setMaxListeners(0);
super(android);
this._android = android;
this._backend = backend;
this.model = model;
@ -124,7 +125,7 @@ export class AndroidDevice extends EventEmitter {
static async create(android: Android, backend: DeviceBackend): Promise<AndroidDevice> {
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());
await device._init();
return device;
@ -143,17 +144,17 @@ export class AndroidDevice extends EventEmitter {
}
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();
return result;
}
async open(command: string): Promise<SocketBackend> {
return await this._backend.open(`${command}`);
return await this._backend.open(this, `${command}`);
}
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> {
@ -198,7 +199,7 @@ export class AndroidDevice extends EventEmitter {
debug('pw:android')(`Polling the socket localabstract:${socketName}`);
while (!socket) {
try {
socket = await this._backend.open(`localabstract:${socketName}`);
socket = await this._backend.open(this, `localabstract:${socketName}`);
} catch (e) {
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> {
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 commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
debug('pw:android')('Starting', pkg, commandLine);
await this._backend.runCommand(`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:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
await this._backend.runCommand(this, `shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`);
return await this._connectToBrowser(socketName, options);
}
@ -295,7 +296,7 @@ export class AndroidDevice extends EventEmitter {
async installApk(content: Buffer, options?: { args?: string[] }): Promise<void> {
const args = options && options.args ? options.args : ['-r', '-t', '-S'];
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);
await installSocket.write(content);
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> {
const socket = await this._backend.open(`sync:`);
const socket = await this._backend.open(this, `sync:`);
const sendHeader = async (command: string, length: number) => {
const buffer = Buffer.alloc(command.length + 4);
buffer.write(command, 0);
@ -328,7 +329,7 @@ export class AndroidDevice extends EventEmitter {
}
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)
return;
@ -344,7 +345,7 @@ export class AndroidDevice extends EventEmitter {
if (this._webViews.has(pid))
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)
return;
let pkg = '';

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -23,6 +23,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types';
import { helper } from '../helper';
import { SdkObject } from '../sdkObject';
export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -123,7 +124,7 @@ export const CRSessionEvents = {
Disconnected: Symbol('Events.CDPSession.Disconnected')
};
export class CRSession extends EventEmitter {
export class CRSession extends SdkObject {
_connection: CRConnection | null;
_eventListener?: (method: string, params?: Object) => void;
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;
constructor(connection: CRConnection, rootSessionId: string, targetType: string, sessionId: string) {
super();
this.setMaxListeners(0);
super(null);
this._connection = connection;
this._rootSessionId = rootSessionId;
this._targetType = targetType;

Просмотреть файл

@ -639,7 +639,7 @@ class FrameSession {
}
const url = event.targetInfo.url;
const worker = new Worker(url);
const worker = new Worker(this._page, url);
this._page._addWorker(event.sessionId, worker);
session.once('Runtime.executionContextCreated', async event => {
worker._createExecutionContext(new CRExecutionContext(session, event.context));
@ -759,7 +759,7 @@ class FrameSession {
lineNumber: lineNumber || 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));
}
}

Просмотреть файл

@ -15,15 +15,17 @@
*/
import * as js from './javascript';
import { SdkObject } from './sdkObject';
import { ConsoleMessageLocation } from './types';
export class ConsoleMessage {
export class ConsoleMessage extends SdkObject {
private _type: string;
private _text?: string;
private _args: js.JSHandle[];
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._text = text;
this._args = args;

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -244,7 +244,7 @@ export class FFPage implements PageDelegate {
async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) {
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) => {
this._session.send('Page.sendMessageToWorker', {
frameId: event.frameId,

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

39
src/server/sdkObject.ts Normal 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 };
}
}

Просмотреть файл

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

Просмотреть файл

@ -189,7 +189,7 @@ function isSharedLib(basename: string) {
async function executablesOrSharedLibraries(directoryPath: string): Promise<string[]> {
const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file));
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 basename = path.basename(filePath).toLowerCase();

Просмотреть файл

@ -35,7 +35,7 @@ export class WKWorkers {
this.clear();
this._sessionListeners = [
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) => {
session.send('Worker.sendMessageToWorker', {
workerId: event.workerId,

Просмотреть файл

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