mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(rpc): scope client-side handles (#2796)
This commit is contained in:
parent
c4e3ed85c0
commit
c25fc4956d
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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!);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
99
test/channels.spec.js
Normal 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;
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user