chore(rpc): scope client-side handles (#2796)

This commit is contained in:
Pavel Feldman 2020-07-01 13:55:29 -07:00 committed by GitHub
parent c4e3ed85c0
commit c25fc4956d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 384 additions and 197 deletions

View File

@ -19,7 +19,7 @@ import { BrowserChannel, BrowserInitializer } from '../channels';
import { BrowserContext } from './browserContext';
import { Page } from './page';
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { Events } from '../../events';
export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
@ -36,12 +36,13 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
return browser ? Browser.from(browser) : null;
}
constructor(connection: Connection, channel: BrowserChannel, initializer: BrowserInitializer) {
super(connection, channel, initializer);
channel.on('close', () => {
constructor(scope: ConnectionScope, guid: string, initializer: BrowserInitializer) {
super(scope, guid, initializer, true);
this._channel.on('close', () => {
this._isConnected = false;
this.emit(Events.Browser.Disconnected);
this._isClosedOrClosing = true;
this._scope.dispose();
});
}

View File

@ -23,7 +23,7 @@ import { BrowserContextChannel, BrowserContextInitializer } from '../channels';
import { ChannelOwner } from './channelOwner';
import { helper } from '../../helper';
import { Browser } from './browser';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { Events } from '../../events';
import { TimeoutSettings } from '../../timeoutSettings';
@ -44,8 +44,8 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
return context ? BrowserContext.from(context) : null;
}
constructor(connection: Connection, channel: BrowserContextChannel, initializer: BrowserContextInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: BrowserContextInitializer) {
super(scope, guid, initializer, true);
initializer.pages.map(p => {
const page = Page.from(p);
this._pages.add(page);
@ -193,6 +193,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
}
this._pendingWaitForEvents.clear();
this.emit(Events.BrowserContext.Close);
this._scope.dispose();
}
async close(): Promise<void> {

View File

@ -16,7 +16,7 @@
import { ChildProcess } from 'child_process';
import { BrowserServerChannel, BrowserServerInitializer } from '../channels';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { ChannelOwner } from './channelOwner';
import { Events } from '../../events';
@ -25,9 +25,9 @@ export class BrowserServer extends ChannelOwner<BrowserServerChannel, BrowserSer
return request._object;
}
constructor(connection: Connection, channel: BrowserServerChannel, initializer: BrowserServerInitializer) {
super(connection, channel, initializer);
channel.on('close', () => this.emit(Events.BrowserServer.Close));
constructor(scope: ConnectionScope, guid: string, initializer: BrowserServerInitializer) {
super(scope, guid, initializer);
this._channel.on('close', () => this.emit(Events.BrowserServer.Close));
}
process(): ChildProcess {

View File

@ -19,12 +19,12 @@ import { BrowserTypeChannel, BrowserTypeInitializer } from '../channels';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { BrowserServer } from './browserServer';
export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeInitializer> {
constructor(connection: Connection, channel: BrowserTypeChannel, initializer: BrowserTypeInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: BrowserTypeInitializer) {
super(scope, guid, initializer);
}
executablePath(): string {

View File

@ -16,19 +16,38 @@
import { EventEmitter } from 'events';
import { Channel } from '../channels';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
export abstract class ChannelOwner<T extends Channel, Initializer> extends EventEmitter {
readonly _channel: T;
readonly _initializer: Initializer;
readonly _connection: Connection;
static clientSymbol = Symbol('client');
readonly _scope: ConnectionScope;
constructor(connection: Connection, channel: T, initializer: Initializer) {
constructor(scope: ConnectionScope, guid: string, initializer: Initializer, isScope?: boolean) {
super();
this._connection = connection;
this._channel = channel;
this._scope = isScope ? scope.createChild(guid) : scope;
const base = new EventEmitter();
this._channel = new Proxy(base, {
get: (obj: any, prop) => {
if (String(prop).startsWith('_'))
return obj[prop];
if (prop === 'then')
return obj.then;
if (prop === 'emit')
return obj.emit;
if (prop === 'on')
return obj.on;
if (prop === 'once')
return obj.once;
if (prop === 'addEventListener')
return obj.addListener;
if (prop === 'removeEventListener')
return obj.removeListener;
return (params: any) => scope.sendMessageToServer({ guid, method: String(prop), params });
},
});
this._channel._object = this;
this._channel._guid = guid;
this._initializer = initializer;
(channel as any)[ChannelOwner.clientSymbol] = this;
}
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { EventEmitter } from 'ws';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { BrowserType } from './browserType';
@ -26,7 +25,6 @@ import { Request, Response, Route } from './network';
import { Page, BindingCall } from './page';
import { Worker } from './worker';
import debug = require('debug');
import { Channel } from '../channels';
import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
import { Download } from './download';
@ -34,83 +32,21 @@ import { parseError } from '../serializers';
import { BrowserServer } from './browserServer';
export class Connection {
private _channels = new Map<string, Channel>();
private _waitingForObject = new Map<string, any>();
readonly _objects = new Map<string, ChannelOwner<any, any>>();
readonly _waitingForObject = new Map<string, any>();
onmessage = (message: string): void => {};
private _lastId = 0;
private _callbacks = new Map<number, { resolve: (a: any) => void, reject: (a: Error) => void }>();
readonly _scopes = new Map<string, ConnectionScope>();
private _rootScript: ConnectionScope;
constructor() {}
private _createRemoteObject(type: string, guid: string, initializer: any): any {
const channel = this._createChannel(guid) as any;
this._channels.set(guid, channel);
let result: ChannelOwner<any, any>;
initializer = this._replaceGuidsWithChannels(initializer);
switch (type) {
case 'bindingCall':
result = new BindingCall(this, channel, initializer);
break;
case 'browser':
result = new Browser(this, channel, initializer);
break;
case 'browserServer':
result = new BrowserServer(this, channel, initializer);
break;
case 'browserType':
result = new BrowserType(this, channel, initializer);
break;
case 'context':
result = new BrowserContext(this, channel, initializer);
break;
case 'consoleMessage':
result = new ConsoleMessage(this, channel, initializer);
break;
case 'dialog':
result = new Dialog(this, channel, initializer);
break;
case 'download':
result = new Download(this, channel, initializer);
break;
case 'elementHandle':
result = new ElementHandle(this, channel, initializer);
break;
case 'frame':
result = new Frame(this, channel, initializer);
break;
case 'jsHandle':
result = new JSHandle(this, channel, initializer);
break;
case 'page':
result = new Page(this, channel, initializer);
break;
case 'request':
result = new Request(this, channel, initializer);
break;
case 'response':
result = new Response(this, channel, initializer);
break;
case 'route':
result = new Route(this, channel, initializer);
break;
case 'worker':
result = new Worker(this, channel, initializer);
break;
default:
throw new Error('Missing type ' + type);
}
channel._object = result;
const callback = this._waitingForObject.get(guid);
if (callback) {
callback(result);
this._waitingForObject.delete(guid);
}
return result;
constructor() {
this._rootScript = this.createScope('');
}
waitForObjectWithKnownName(guid: string): Promise<any> {
if (this._channels.has(guid))
return this._channels.get(guid)!._object;
async waitForObjectWithKnownName(guid: string): Promise<any> {
if (this._objects.has(guid))
return this._objects.get(guid)!;
return new Promise(f => this._waitingForObject.set(guid, f));
}
@ -122,6 +58,16 @@ export class Connection {
return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject }));
}
_debugScopeState(): any {
const scopeState: any = {};
scopeState.objects = [...this._objects.keys()];
scopeState.scopes = [...this._scopes.values()].map(scope => ({
_guid: scope._guid,
objects: [...scope._objects.keys()]
}));
return scopeState;
}
dispatch(message: string) {
const parsedMessage = JSON.parse(message);
const { id, guid, method, params, result, error } = parsedMessage;
@ -138,36 +84,15 @@ export class Connection {
debug('pw:channel:event')(parsedMessage);
if (method === '__create__') {
this._createRemoteObject(params.type, guid, params.initializer);
const scopeObject = this._objects.get(guid);
const scope = scopeObject ? scopeObject._scope : this._rootScript;
scope.createRemoteObject(params.type, params.guid, params.initializer);
return;
}
const channel = this._channels.get(guid)!;
channel.emit(method, this._replaceGuidsWithChannels(params));
const object = this._objects.get(guid)!;
object._channel.emit(method, this._replaceGuidsWithChannels(params));
}
private _createChannel(guid: string): Channel {
const base = new EventEmitter();
(base as any)._guid = guid;
return new Proxy(base, {
get: (obj: any, prop) => {
if (String(prop).startsWith('_'))
return obj[prop];
if (prop === 'then')
return obj.then;
if (prop === 'emit')
return obj.emit;
if (prop === 'on')
return obj.on;
if (prop === 'once')
return obj.once;
if (prop === 'addEventListener')
return obj.addListener;
if (prop === 'removeEventListener')
return obj.removeListener;
return (params: any) => this.sendMessageToServer({ guid, method: String(prop), params });
},
});
}
private _replaceChannelsWithGuids(payload: any): any {
if (!payload)
@ -188,13 +113,13 @@ export class Connection {
return payload;
}
private _replaceGuidsWithChannels(payload: any): any {
_replaceGuidsWithChannels(payload: any): any {
if (!payload)
return payload;
if (Array.isArray(payload))
return payload.map(p => this._replaceGuidsWithChannels(p));
if (payload.guid && this._channels.has(payload.guid))
return this._channels.get(payload.guid);
if (payload.guid && this._objects.has(payload.guid))
return this._objects.get(payload.guid)!._channel;
// TODO: send base64
if (payload instanceof Buffer)
return payload;
@ -206,4 +131,120 @@ export class Connection {
}
return payload;
}
createScope(guid: string): ConnectionScope {
const scope = new ConnectionScope(this, guid);
this._scopes.set(guid, scope);
return scope;
}
}
export class ConnectionScope {
private _connection: Connection;
readonly _objects = new Map<string, ChannelOwner<any, any>>();
private _children = new Set<ConnectionScope>();
private _parent: ConnectionScope | undefined;
readonly _guid: string;
constructor(connection: Connection, guid: string) {
this._connection = connection;
this._guid = guid;
}
createChild(guid: string): ConnectionScope {
const scope = this._connection.createScope(guid);
this._children.add(scope);
scope._parent = this;
return scope;
}
dispose() {
// Take care of hierarchy.
for (const child of [...this._children])
child.dispose();
this._children.clear();
// Delete self from scopes and objects.
this._connection._scopes.delete(this._guid);
this._connection._objects.delete(this._guid);
// Delete all of the objects from connection.
for (const guid of this._objects.keys())
this._connection._objects.delete(guid);
// Clean up from parent.
if (this._parent) {
this._parent._objects.delete(this._guid);
this._parent._children.delete(this);
}
}
async sendMessageToServer(message: { guid: string, method: string, params: any }): Promise<any> {
return this._connection.sendMessageToServer(message);
}
createRemoteObject(type: string, guid: string, initializer: any): any {
let result: ChannelOwner<any, any>;
initializer = this._connection._replaceGuidsWithChannels(initializer);
switch (type) {
case 'bindingCall':
result = new BindingCall(this, guid, initializer);
break;
case 'browser':
result = new Browser(this, guid, initializer);
break;
case 'browserServer':
result = new BrowserServer(this, guid, initializer);
break;
case 'browserType':
result = new BrowserType(this, guid, initializer);
break;
case 'context':
result = new BrowserContext(this, guid, initializer);
break;
case 'consoleMessage':
result = new ConsoleMessage(this, guid, initializer);
break;
case 'dialog':
result = new Dialog(this, guid, initializer);
break;
case 'download':
result = new Download(this, guid, initializer);
break;
case 'elementHandle':
result = new ElementHandle(this, guid, initializer);
break;
case 'frame':
result = new Frame(this, guid, initializer);
break;
case 'jsHandle':
result = new JSHandle(this, guid, initializer);
break;
case 'page':
result = new Page(this, guid, initializer);
break;
case 'request':
result = new Request(this, guid, initializer);
break;
case 'response':
result = new Response(this, guid, initializer);
break;
case 'route':
result = new Route(this, guid, initializer);
break;
case 'worker':
result = new Worker(this, guid, initializer);
break;
default:
throw new Error('Missing type ' + type);
}
this._connection._objects.set(guid, result);
this._objects.set(guid, result);
const callback = this._connection._waitingForObject.get(guid);
if (callback) {
callback(result);
this._connection._waitingForObject.delete(guid);
}
return result;
}
}

View File

@ -19,15 +19,15 @@ import { ConsoleMessageLocation } from '../../types';
import { JSHandle } from './jsHandle';
import { ConsoleMessageChannel, ConsoleMessageInitializer } from '../channels';
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
export class ConsoleMessage extends ChannelOwner<ConsoleMessageChannel, ConsoleMessageInitializer> {
static from(request: ConsoleMessageChannel): ConsoleMessage {
return request._object;
}
constructor(connection: Connection, channel: ConsoleMessageChannel, initializer: ConsoleMessageInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: ConsoleMessageInitializer) {
super(scope, guid, initializer);
}
type(): string {

View File

@ -15,7 +15,7 @@
*/
import { DialogChannel, DialogInitializer } from '../channels';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { ChannelOwner } from './channelOwner';
export class Dialog extends ChannelOwner<DialogChannel, DialogInitializer> {
@ -23,8 +23,8 @@ export class Dialog extends ChannelOwner<DialogChannel, DialogInitializer> {
return request._object;
}
constructor(connection: Connection, channel: DialogChannel, initializer: DialogInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: DialogInitializer) {
super(scope, guid, initializer);
}
type(): string {

View File

@ -16,7 +16,7 @@
import * as fs from 'fs';
import { DownloadChannel, DownloadInitializer } from '../channels';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { ChannelOwner } from './channelOwner';
import { Readable } from 'stream';
@ -25,8 +25,8 @@ export class Download extends ChannelOwner<DownloadChannel, DownloadInitializer>
return request._object;
}
constructor(connection: Connection, channel: DownloadChannel, initializer: DownloadInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: DownloadInitializer) {
super(scope, guid, initializer);
}
url(): string {

View File

@ -18,7 +18,7 @@ import * as types from '../../types';
import { ElementHandleChannel, JSHandleInitializer } from '../channels';
import { Frame } from './frame';
import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
readonly _elementChannel: ElementHandleChannel;
@ -31,9 +31,9 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
return handle ? ElementHandle.from(handle) : null;
}
constructor(connection: Connection, channel: ElementHandleChannel, initializer: JSHandleInitializer) {
super(connection, channel, initializer);
this._elementChannel = channel;
constructor(scope: ConnectionScope, guid: string, initializer: JSHandleInitializer) {
super(scope, guid, initializer);
this._elementChannel = this._channel as ElementHandleChannel;
}
asElement(): ElementHandle<T> | null {

View File

@ -25,7 +25,7 @@ import { JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult }
import * as network from './network';
import { Response } from './network';
import { Page } from './page';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { normalizeFilePayloads } from '../serializers';
export type GotoOptions = types.NavigateOptions & {
@ -50,8 +50,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
return frame ? Frame.from(frame) : null;
}
constructor(connection: Connection, channel: FrameChannel, initializer: FrameInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: FrameInitializer) {
super(scope, guid, initializer);
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
if (this._parentFrame)
this._parentFrame._childFrames.add(this);

View File

@ -17,7 +17,7 @@
import { JSHandleChannel, JSHandleInitializer } from '../channels';
import { ElementHandle } from './elementHandle';
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers';
type NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg);
@ -47,10 +47,10 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
return handle ? JSHandle.from(handle) : null;
}
constructor(conection: Connection, channel: JSHandleChannel, initializer: JSHandleInitializer) {
super(conection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: JSHandleInitializer) {
super(scope, guid, initializer);
this._preview = this._initializer.preview;
channel.on('previewUpdated', preview => this._preview = preview);
this._channel.on('previewUpdated', preview => this._preview = preview);
}
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<R>;

View File

@ -19,7 +19,7 @@ import * as types from '../../types';
import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../channels';
import { ChannelOwner } from './channelOwner';
import { Frame } from './frame';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { normalizeFulfillParameters } from '../serializers';
export type NetworkCookie = {
@ -58,8 +58,8 @@ export class Request extends ChannelOwner<RequestChannel, RequestInitializer> {
return request ? Request.from(request) : null;
}
constructor(connection: Connection, channel: RequestChannel, initializer: RequestInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: RequestInitializer) {
super(scope, guid, initializer);
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
if (this._redirectedFrom)
this._redirectedFrom._redirectedTo = this;
@ -138,8 +138,8 @@ export class Route extends ChannelOwner<RouteChannel, RouteInitializer> {
return route._object;
}
constructor(connection: Connection, channel: RouteChannel, initializer: RouteInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: RouteInitializer) {
super(scope, guid, initializer);
}
request(): Request {
@ -176,8 +176,8 @@ export class Response extends ChannelOwner<ResponseChannel, ResponseInitializer>
return response ? Response.from(response) : null;
}
constructor(connection: Connection, channel: ResponseChannel, initializer: ResponseInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: ResponseInitializer) {
super(scope, guid, initializer);
}
url(): string {

View File

@ -22,7 +22,7 @@ import { assert, assertMaxArguments, helper, Listener } from '../../helper';
import { TimeoutSettings } from '../../timeoutSettings';
import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, Channel, PageChannel, PageInitializer } from '../channels';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { parseError, serializeError } from '../serializers';
import { Accessibility } from './accessibility';
import { BrowserContext } from './browserContext';
@ -67,11 +67,11 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
return page ? Page.from(page) : null;
}
constructor(connection: Connection, channel: PageChannel, initializer: PageInitializer) {
super(connection, channel, initializer);
this.accessibility = new Accessibility(channel);
this.keyboard = new Keyboard(channel);
this.mouse = new Mouse(channel);
constructor(scope: ConnectionScope, guid: string, initializer: PageInitializer) {
super(scope, guid, initializer);
this.accessibility = new Accessibility(this._channel);
this.keyboard = new Keyboard(this._channel);
this.mouse = new Mouse(this._channel);
this._mainFrame = Frame.from(initializer.mainFrame);
this._mainFrame._page = this;
@ -505,8 +505,8 @@ export class BindingCall extends ChannelOwner<BindingCallChannel, BindingCallIni
return channel._object;
}
constructor(connection: Connection, channel: BindingCallChannel, initializer: BindingCallInitializer) {
super(connection, channel, initializer);
constructor(scope: ConnectionScope, guid: string, initializer: BindingCallInitializer) {
super(scope, guid, initializer);
}
async call(func: FunctionWithSource) {

View File

@ -17,7 +17,7 @@
import { Events } from '../../events';
import { assertMaxArguments } from '../../helper';
import { WorkerChannel, WorkerInitializer } from '../channels';
import { Connection } from './connection';
import { ConnectionScope } from './connection';
import { ChannelOwner } from './channelOwner';
import { Func1, JSHandle, parseResult, serializeArgument, SmartHandle } from './jsHandle';
import { Page } from './page';
@ -29,9 +29,9 @@ export class Worker extends ChannelOwner<WorkerChannel, WorkerInitializer> {
return worker._object;
}
constructor(connection: Connection, channel: WorkerChannel, initializer: WorkerInitializer) {
super(connection, channel, initializer);
channel.on('close', () => {
constructor(scope: ConnectionScope, guid: string, initializer: WorkerInitializer) {
super(scope, guid, initializer);
this._channel.on('close', () => {
this._page!._workers.delete(this);
this.emit(Events.Worker.Close, this);
});

View File

@ -24,8 +24,7 @@ const transport = new Transport(process.stdout, process.stdin);
transport.onmessage = message => dispatcherConnection.dispatch(message);
dispatcherConnection.onmessage = message => transport.send(message);
const dispatcherScope = dispatcherConnection.createScope();
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']);
new BrowserTypeDispatcher(dispatcherScope, playwright.chromium!);
new BrowserTypeDispatcher(dispatcherScope, playwright.firefox!);
new BrowserTypeDispatcher(dispatcherScope, playwright.webkit!);
new BrowserTypeDispatcher(dispatcherConnection.rootScope(), playwright.chromium!);
new BrowserTypeDispatcher(dispatcherConnection.rootScope(), playwright.firefox!);
new BrowserTypeDispatcher(dispatcherConnection.rootScope(), playwright.webkit!);

View File

@ -29,12 +29,12 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
constructor(scope: DispatcherScope, context: BrowserContextBase) {
super(scope, context, 'context', {
pages: context.pages().map(p => PageDispatcher.from(scope, p))
});
}, true);
this._context = context;
context.on(Events.BrowserContext.Page, page => this._dispatchEvent('page', PageDispatcher.from(this._scope, page)));
context.on(Events.BrowserContext.Close, () => {
this._dispatchEvent('close');
scope.dispose();
this._scope.dispose();
});
}

View File

@ -24,15 +24,15 @@ import { Dispatcher, DispatcherScope } from './dispatcher';
export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel {
constructor(scope: DispatcherScope, browser: BrowserBase) {
super(scope, browser, 'browser', {});
super(scope, browser, 'browser', {}, true);
browser.on(Events.Browser.Disconnected, () => {
this._dispatchEvent('close');
scope.dispose();
this._scope.dispose();
});
}
async newContext(params: { options?: types.BrowserContextOptions }): Promise<BrowserContextChannel> {
return new BrowserContextDispatcher(this._scope.createChild(), await this._object.newContext(params.options) as BrowserContextBase);
return new BrowserContextDispatcher(this._scope, await this._object.newContext(params.options) as BrowserContextBase);
}
async close(): Promise<void> {

View File

@ -29,17 +29,17 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
super(scope, browserType, 'browserType', {
executablePath: browserType.executablePath(),
name: browserType.name()
}, browserType.name());
}, false, browserType.name());
}
async launch(params: { options?: types.LaunchOptions }): Promise<BrowserChannel> {
const browser = await this._object.launch(params.options || undefined);
return new BrowserDispatcher(this._scope.createChild(), browser as BrowserBase);
return new BrowserDispatcher(this._scope, browser as BrowserBase);
}
async launchPersistentContext(params: { userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions }): Promise<BrowserContextChannel> {
const browserContext = await this._object.launchPersistentContext(params.userDataDir, params.options);
return new BrowserContextDispatcher(this._scope.createChild(), browserContext as BrowserContextBase);
return new BrowserContextDispatcher(this._scope, browserContext as BrowserContextBase);
}
async launchServer(params: { options?: types.LaunchServerOptions }): Promise<BrowserServerChannel> {
@ -48,6 +48,6 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
async connect(params: { options: types.ConnectOptions }): Promise<BrowserChannel> {
const browser = await this._object.connect(params.options);
return new BrowserDispatcher(this._scope.createChild(), browser as BrowserBase);
return new BrowserDispatcher(this._scope, browser as BrowserBase);
}
}

View File

@ -41,15 +41,15 @@ export class Dispatcher<Type, Initializer> extends EventEmitter implements Chann
protected _scope: DispatcherScope;
_object: Type;
constructor(scope: DispatcherScope, object: Type, type: string, initializer: Initializer, guid = type + '@' + helper.guid()) {
constructor(scope: DispatcherScope, object: Type, type: string, initializer: Initializer, isScope?: boolean, guid = type + '@' + helper.guid()) {
super();
this._type = type;
this._guid = guid;
this._object = object;
this._scope = scope;
this._scope = isScope ? scope.createChild(guid) : scope;
scope.bind(this._guid, this);
(object as any)[dispatcherSymbol] = this;
this._scope.sendMessageToClient(this._guid, '__create__', { type, initializer });
this._scope.sendMessageToClient(scope.guid, '__create__', { type, initializer, guid });
}
_dispatchEvent(method: string, params: Dispatcher<any, any> | any = {}) {
@ -61,17 +61,19 @@ export class DispatcherScope {
private _connection: DispatcherConnection;
private _dispatchers = new Map<string, Dispatcher<any, any>>();
private _parent: DispatcherScope | undefined;
private _childScopes = new Set<DispatcherScope>();
readonly _children = new Set<DispatcherScope>();
readonly guid: string;
constructor(connection: DispatcherConnection, parent?: DispatcherScope) {
constructor(connection: DispatcherConnection, guid: string, parent?: DispatcherScope) {
this._connection = connection;
this._parent = parent;
this.guid = guid;
if (parent)
parent._childScopes.add(this);
parent._children.add(this);
}
createChild(): DispatcherScope {
return new DispatcherScope(this._connection, this);
createChild(guid: string): DispatcherScope {
return new DispatcherScope(this._connection, guid, this);
}
bind(guid: string, arg: Dispatcher<any, any>) {
@ -80,30 +82,52 @@ export class DispatcherScope {
}
dispose() {
for (const child of [...this._childScopes])
// Take care of hierarchy.
for (const child of [...this._children])
child.dispose();
this._childScopes.clear();
this._children.clear();
// Delete self from scopes and objects.
this._connection._dispatchers.delete(this.guid);
// Delete all of the objects from connection.
for (const guid of this._dispatchers.keys())
this._connection._dispatchers.delete(guid);
if (this._parent)
this._parent._childScopes.delete(this);
if (this._parent) {
this._parent._children.delete(this);
this._parent._dispatchers.delete(this.guid);
}
}
async sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
this._connection._sendMessageToClient(guid, method, params);
}
_dumpScopeState(scopes: any[]): any {
const scopeState: any = { _guid: this.guid };
scopeState.objects = [...this._dispatchers.keys()];
scopes.push(scopeState);
[...this._children].map(c => c._dumpScopeState(scopes));
return scopeState;
}
}
export class DispatcherConnection {
readonly _dispatchers = new Map<string, Dispatcher<any, any>>();
private _rootScope: DispatcherScope;
onmessage = (message: string) => {};
async _sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
this.onmessage(JSON.stringify({ guid, method, params: this._replaceDispatchersWithGuids(params) }));
}
createScope(): DispatcherScope {
return new DispatcherScope(this);
constructor() {
this._rootScope = new DispatcherScope(this, '');
}
rootScope(): DispatcherScope {
return this._rootScope;
}
async dispatch(message: string) {
@ -114,6 +138,14 @@ export class DispatcherConnection {
this.onmessage(JSON.stringify({ id, error: serializeError(new Error('Target browser or context has been closed')) }));
return;
}
if (method === 'debugScopeState') {
const dispatcherState: any = {};
dispatcherState.objects = [...this._dispatchers.keys()];
dispatcherState.scopes = [];
this._rootScope._dumpScopeState(dispatcherState.scopes);
this.onmessage(JSON.stringify({ id, result: dispatcherState }));
return;
}
try {
const result = await (dispatcher as any)[method](this._replaceGuidsWithDispatchers(params));
this.onmessage(JSON.stringify({ id, result: this._replaceDispatchersWithGuids(result) }));
@ -122,7 +154,7 @@ export class DispatcherConnection {
}
}
private _replaceDispatchersWithGuids(payload: any): any {
_replaceDispatchersWithGuids(payload: any): any {
if (!payload)
return payload;
if (payload instanceof Dispatcher)

99
test/channels.spec.js Normal file
View File

@ -0,0 +1,99 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
const util = require('util');
const vm = require('vm');
const { FFOX, CHROMIUM, WEBKIT, WIN, CHANNEL } = require('./utils').testOptions(browserType);
describe.skip(!CHANNEL)('Channels', function() {
it('should work', async({browser}) => {
expect(!!browser._channel).toBeTruthy();
});
it('should scope context handles', async({browser, server}) => {
const GOLDEN_PRECONDITION = {
objects: [ 'chromium', 'browser' ],
scopes: [
{ _guid: '', objects: [ 'chromium', 'browser' ] },
{ _guid: 'browser', objects: [] }
]
};
await expectScopeState(browser, GOLDEN_PRECONDITION);
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await expectScopeState(browser, {
objects: [ 'chromium', 'browser', 'context', 'frame', 'page', 'request', 'response' ],
scopes: [
{ _guid: '', objects: [ 'chromium', 'browser' ] },
{ _guid: 'browser', objects: ['context'] },
{ _guid: 'context', objects: ['frame', 'page', 'request', 'response'] }
]
});
await context.close();
await expectScopeState(browser, GOLDEN_PRECONDITION);
});
it('should browser handles', async({browserType, defaultBrowserOptions}) => {
const GOLDEN_PRECONDITION = {
objects: [ 'chromium', 'browser' ],
scopes: [
{ _guid: '', objects: [ 'chromium', 'browser' ] },
{ _guid: 'browser', objects: [] }
]
};
await expectScopeState(browserType, GOLDEN_PRECONDITION);
const browser = await browserType.launch(defaultBrowserOptions);
await browser.newContext();
await expectScopeState(browserType, {
objects: [ 'chromium', 'browser', 'browser', 'context' ],
scopes: [
{ _guid: '', objects: [ 'chromium', 'browser', 'browser' ] },
{ _guid: 'browser', objects: [] },
{ _guid: 'browser', objects: ['context'] },
{ _guid: 'context', objects: [] },
]
});
await browser.close();
await expectScopeState(browserType, GOLDEN_PRECONDITION);
});
});
async function expectScopeState(object, golden) {
const remoteState = trimGuids(await object._channel.debugScopeState());
const localState = trimGuids(object._scope._connection._debugScopeState());
expect(localState).toEqual(golden);
expect(remoteState).toEqual(golden);
}
function trimGuids(object) {
if (Array.isArray(object))
return object.map(trimGuids);
if (typeof object === 'object') {
const result = {};
for (const key in object)
result[key] = trimGuids(object[key]);
return result;
}
if (typeof object === 'string')
return object ? object.match(/[^@]+/)[0] : '';
return object;
}

View File

@ -1305,9 +1305,3 @@ describe('Page api coverage', function() {
expect(await frame.evaluate(() => document.querySelector('textarea').value)).toBe('a');
});
});
describe.skip(!CHANNEL)('Page channel', function() {
it('page should be client stub', async({page, server}) => {
expect(!!page._channel).toBeTruthy();
});
});

View File

@ -214,6 +214,7 @@ module.exports = {
files: [
'./browser.spec.js',
'./browsercontext.spec.js',
'./channels.spec.js',
'./ignorehttpserrors.spec.js',
'./popup.spec.js',
'./recorder.spec.js',

View File

@ -114,7 +114,7 @@ function collect(browserNames) {
await new Promise(f => setImmediate(f));
return result;
};
new BrowserTypeDispatcher(dispatcherConnection.createScope(), browserType);
new BrowserTypeDispatcher(dispatcherConnection.rootScope(), browserType);
overridenBrowserType = await connection.waitForObjectWithKnownName(browserType.name());
}
state.browserType = overridenBrowserType;