chore: put remoteObject directly on JSHandle (#113)

This commit is contained in:
Dmitry Gozman 2019-12-02 13:12:28 -08:00 committed by GitHub
parent 113ffd6808
commit ffaf7326ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 90 additions and 97 deletions

View file

@ -20,7 +20,6 @@ import { helper } from '../helper';
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper';
import { Protocol } from './protocol';
import * as js from '../javascript';
import * as dom from '../dom';
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
@ -50,7 +49,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
}).catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return returnByValue ? valueFromRemoteObject(remoteObject) : toHandle(context, remoteObject);
return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject);
}
if (typeof pageFunction !== 'function')
@ -91,7 +90,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return returnByValue ? valueFromRemoteObject(remoteObject) : toHandle(context, remoteObject);
return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject);
function convertArgument(arg: any): any {
if (typeof arg === 'bigint') // eslint-disable-line valid-typeof
@ -141,7 +140,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
for (const property of response.result) {
if (!property.enumerable)
continue;
result.set(property.name, toHandle(handle.executionContext(), property.value));
result.set(property.name, handle.executionContext()._createHandle(property.value));
}
return result;
}
@ -174,19 +173,6 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
}
}
const remoteObjectSymbol = Symbol('RemoteObject');
export function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
return (handle as any)[remoteObjectSymbol];
}
export function toHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
if (remoteObject.subtype === 'node' && context.frame()) {
const handle = new dom.ElementHandle(context);
(handle as any)[remoteObjectSymbol] = remoteObject;
return handle;
}
const handle = new js.JSHandle(context);
(handle as any)[remoteObjectSymbol] = remoteObject;
return handle;
function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

View file

@ -23,8 +23,8 @@ import * as frames from '../frames';
import { CDPSession } from './Connection';
import { FrameManager } from './FrameManager';
import { Protocol } from './protocol';
import { toRemoteObject, toHandle, ExecutionContextDelegate } from './ExecutionContext';
import { ScreenshotOptions } from './Screenshotter';
import { ExecutionContextDelegate } from './ExecutionContext';
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
readonly keyboard: input.Keyboard;
@ -54,6 +54,10 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
return this._frameManager.page()._javascriptEnabled;
}
isElement(remoteObject: any): boolean {
return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node';
}
private _getBoxModel(handle: dom.ElementHandle): Promise<void | Protocol.DOM.getBoxModelReturnValue> {
return this._client.send('DOM.getBoxModel', {
objectId: toRemoteObject(handle).objectId
@ -202,6 +206,10 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
backendNodeId,
executionContextId: (to.context._delegate as ExecutionContextDelegate)._contextId,
});
return toHandle(to.context, object).asElement()!;
return to.context._createHandle(object).asElement()!;
}
}
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

View file

@ -32,16 +32,15 @@ import { PDF } from './features/pdf';
import { Workers } from './features/workers';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { toHandle } from './ExecutionContext';
import { NetworkManagerEvents } from './NetworkManager';
import { Protocol } from './protocol';
import { getExceptionMessage, releaseObject } from './protocolHelper';
import { Target } from './Target';
import * as input from '../input';
import * as types from '../types';
import * as dom from '../dom';
import * as frames from '../frames';
import * as js from '../javascript';
import * as dom from '../dom';
import * as network from '../network';
import * as dialog from '../dialog';
import * as console from '../console';
@ -317,7 +316,7 @@ export class Page extends EventEmitter {
return;
}
const context = this._frameManager.executionContextById(event.executionContextId);
const values = event.args.map(arg => toHandle(context, arg));
const values = event.args.map(arg => context._createHandle(arg));
this._addConsoleMessage(event.type, values, event.stackTrace);
}

View file

@ -17,7 +17,6 @@
import { CDPSession } from '../Connection';
import { Protocol } from '../protocol';
import { toRemoteObject } from '../ExecutionContext';
import * as dom from '../../dom';
type SerializedAXNode = {
@ -73,7 +72,8 @@ export class Accessibility {
const {nodes} = await this._client.send('Accessibility.getFullAXTree');
let backendNodeId = null;
if (root) {
const {node} = await this._client.send('DOM.describeNode', {objectId: toRemoteObject(root).objectId});
const remoteObject = root._remoteObject as Protocol.Runtime.RemoteObject;
const {node} = await this._client.send('DOM.describeNode', {objectId: remoteObject.objectId});
backendNodeId = node.backendNodeId;
}
const defaultRoot = AXNode.createTree(nodes);

View file

@ -21,7 +21,7 @@ import { Protocol } from '../protocol';
import { Events } from '../events';
import * as types from '../../types';
import * as js from '../../javascript';
import { toHandle, ExecutionContextDelegate } from '../ExecutionContext';
import { ExecutionContextDelegate } from '../ExecutionContext';
type AddToConsoleCallback = (type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void;
type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void;
@ -67,7 +67,7 @@ export class Worker extends EventEmitter {
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
let jsHandleFactory: (o: Protocol.Runtime.RemoteObject) => js.JSHandle;
this._client.once('Runtime.executionContextCreated', async event => {
jsHandleFactory = remoteObject => toHandle(executionContext, remoteObject);
jsHandleFactory = remoteObject => executionContext._createHandle(remoteObject);
const executionContext = new js.ExecutionContext(new ExecutionContextDelegate(client, event.context));
this._executionContextCallback(executionContext);
});

View file

@ -2,22 +2,21 @@
// Licensed under the MIT license.
import * as frames from './frames';
import { assert, helper } from './helper';
import Injected from './injected/injected';
import * as input from './input';
import * as js from './javascript';
import * as types from './types';
import * as injectedSource from './generated/injectedSource';
import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
type SelectorRoot = Element | ShadowRoot | Document;
import { assert, helper } from './helper';
import Injected from './injected/injected';
export interface DOMWorldDelegate {
keyboard: input.Keyboard;
mouse: input.Mouse;
frame: frames.Frame;
isJavascriptEnabled(): boolean;
isElement(remoteObject: any): boolean;
contentFrame(handle: ElementHandle): Promise<frames.Frame | null>;
boundingBox(handle: ElementHandle): Promise<types.Rect | null>;
screenshot(handle: ElementHandle, options?: any): Promise<string | Buffer>;
@ -38,6 +37,12 @@ export class DOMWorld {
this.delegate = delegate;
}
_createHandle(remoteObject: any): ElementHandle | null {
if (this.delegate.isElement(remoteObject))
return new ElementHandle(this.context, remoteObject);
return null;
}
injected(): Promise<js.JSHandle> {
if (!this._injectedPromise) {
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source];
@ -67,11 +72,13 @@ export class DOMWorld {
}
}
type SelectorRoot = Element | ShadowRoot | Document;
export class ElementHandle extends js.JSHandle {
private readonly _world: DOMWorld;
constructor(context: js.ExecutionContext) {
super(context);
constructor(context: js.ExecutionContext, remoteObject: any) {
super(context, remoteObject);
assert(context._domWorld, 'Element handle should have a dom world');
this._world = context._domWorld;
}

View file

@ -17,7 +17,6 @@
import {helper, debugError} from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import { JugglerSession } from './Connection';
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
@ -48,7 +47,8 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
expression: pageFunction.trim(),
executionContextId: this._executionContextId,
}).catch(rewriteError);
return toHandle(context, payload.result, payload.exceptionDetails);
checkException(payload.exceptionDetails);
return context._createHandle(payload.result);
}
if (typeof pageFunction !== 'function')
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
@ -76,7 +76,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
throw new Error('JSHandles can be evaluated only in the context they were created!');
if (arg._disposed)
throw new Error('JSHandle is disposed!');
return this._toProtocolValue(toPayload(arg));
return this._toCallArgument(arg._remoteObject);
}
if (Object.is(arg, Infinity))
return {unserializableValue: 'Infinity'};
@ -101,7 +101,8 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
throw err;
}
const payload = await callFunctionPromise.catch(rewriteError);
return toHandle(context, payload.result, payload.exceptionDetails);
checkException(payload.exceptionDetails);
return context._createHandle(payload.result);
function rewriteError(error) {
if (error.message.includes('Failed to find execution context with id'))
@ -113,18 +114,18 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const response = await this._session.send('Runtime.getObjectProperties', {
executionContextId: this._executionContextId,
objectId: toPayload(handle).objectId,
objectId: handle._remoteObject.objectId,
});
const result = new Map();
for (const property of response.properties)
result.set(property.name, toHandle(handle.executionContext(), property.value, null));
result.set(property.name, handle.executionContext()._createHandle(property.value));
return result;
}
async releaseHandle(handle: js.JSHandle): Promise<void> {
await this._session.send('Runtime.disposeObject', {
executionContextId: this._executionContextId,
objectId: toPayload(handle).objectId,
objectId: handle._remoteObject.objectId,
}).catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
@ -133,51 +134,37 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
}
async handleJSONValue(handle: js.JSHandle): Promise<any> {
const payload = toPayload(handle);
const payload = handle._remoteObject;
if (!payload.objectId)
return deserializeValue(payload);
const simpleValue = await this._session.send('Runtime.callFunction', {
executionContextId: this._executionContextId,
returnByValue: true,
functionDeclaration: (e => e).toString(),
args: [this._toProtocolValue(payload)],
args: [this._toCallArgument(payload)],
});
return deserializeValue(simpleValue.result);
}
handleToString(handle: js.JSHandle, includeType: boolean): string {
const payload = toPayload(handle);
const payload = handle._remoteObject;
if (payload.objectId)
return 'JSHandle@' + (payload.subtype || payload.type);
return (includeType ? 'JSHandle:' : '') + deserializeValue(payload);
}
private _toProtocolValue(payload: any): any {
private _toCallArgument(payload: any): any {
return { value: payload.value, unserializableValue: payload.unserializableValue, objectId: payload.objectId };
}
}
const payloadSymbol = Symbol('payload');
export function toPayload(handle: js.JSHandle): any {
return (handle as any)[payloadSymbol];
}
export function toHandle(context: js.ExecutionContext, result: any, exceptionDetails?: any) {
function checkException(exceptionDetails?: any) {
if (exceptionDetails) {
if (exceptionDetails.value)
throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value));
else
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack);
}
if (result.subtype === 'node') {
const handle = new dom.ElementHandle(context);
(handle as any)[payloadSymbol] = result;
return handle;
}
const handle = new js.JSHandle(context);
(handle as any)[payloadSymbol] = result;
return handle;
}
export function deserializeValue({unserializableValue, value}) {

View file

@ -22,7 +22,6 @@ import * as types from '../types';
import * as frames from '../frames';
import { JugglerSession } from './Connection';
import { FrameManager } from './FrameManager';
import { toPayload } from './ExecutionContext';
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
readonly keyboard: input.Keyboard;
@ -44,7 +43,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
async contentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const {frameId} = await this._session.send('Page.contentFrame', {
frameId: this._frameId,
objectId: toPayload(handle).objectId,
objectId: toRemoteObject(handle).objectId,
});
if (!frameId)
return null;
@ -56,17 +55,21 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
return this._frameManager._page._javascriptEnabled;
}
isElement(remoteObject: any): boolean {
return remoteObject.subtype === 'node';
}
async boundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
return await this._session.send('Page.getBoundingBox', {
frameId: this._frameId,
objectId: toPayload(handle).objectId,
objectId: toRemoteObject(handle).objectId,
});
}
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
const clip = await this._session.send('Page.getBoundingBox', {
frameId: this._frameId,
objectId: toPayload(handle).objectId,
objectId: toRemoteObject(handle).objectId,
});
if (!clip)
throw new Error('Node is either not visible or not an HTMLElement');
@ -119,7 +122,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
const result = await this._session.send('Page.getContentQuads', {
frameId: this._frameId,
objectId: toPayload(handle).objectId,
objectId: toRemoteObject(handle).objectId,
}).catch(debugError);
if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
@ -140,3 +143,8 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
return handle;
}
}
function toRemoteObject(handle: dom.ElementHandle): any {
return handle._remoteObject;
}

View file

@ -32,12 +32,11 @@ import { NavigationWatchdog } from './NavigationWatchdog';
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
import * as input from '../input';
import * as types from '../types';
import * as dom from '../dom';
import * as js from '../javascript';
import * as dom from '../dom';
import * as network from '../network';
import * as frames from '../frames';
import * as dialog from '../dialog';
import { toHandle } from './ExecutionContext';
import * as console from '../console';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -553,7 +552,7 @@ export class Page extends EventEmitter {
_onConsole({type, args, executionContextId, location}) {
const context = this._frameManager.executionContextById(executionContextId);
this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args.map(arg => toHandle(context, arg)), location));
this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args.map(arg => context._createHandle(arg)), location));
}
isClosed(): boolean {
@ -577,7 +576,7 @@ export class Page extends EventEmitter {
if (!this._fileChooserInterceptors.size)
return;
const context = this._frameManager.executionContextById(executionContextId);
const handle = toHandle(context, element) as dom.ElementHandle;
const handle = context._createHandle(element).asElement()!;
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);

View file

@ -7,11 +7,11 @@ export { Browser, BrowserContext, Target } from './Browser';
export { BrowserFetcher } from './BrowserFetcher';
export { Dialog } from '../dialog';
export { ExecutionContext, JSHandle } from '../javascript';
export { ElementHandle } from '../dom';
export { Accessibility } from './features/accessibility';
export { Interception } from './features/interception';
export { Permissions } from './features/permissions';
export { Frame } from '../frames';
export { ElementHandle } from '../dom';
export { Request, Response } from '../network';
export { Page } from './Page';
export { Playwright } from './Playwright';

View file

@ -32,14 +32,20 @@ export class ExecutionContext {
evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => {
return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args);
}
_createHandle(remoteObject: any): JSHandle {
return (this._domWorld && this._domWorld._createHandle(remoteObject)) || new JSHandle(this, remoteObject);
}
}
export class JSHandle {
readonly _context: ExecutionContext;
readonly _remoteObject: any;
_disposed = false;
constructor(context: ExecutionContext) {
constructor(context: ExecutionContext, remoteObject: any) {
this._context = context;
this._remoteObject = remoteObject;
}
executionContext(): ExecutionContext {

View file

@ -20,7 +20,6 @@ import { helper } from '../helper';
import { valueFromRemoteObject, releaseObject } from './protocolHelper';
import { Protocol } from './protocol';
import * as js from '../javascript';
import * as dom from '../dom';
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
@ -78,7 +77,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
if (response.wasThrown)
throw new Error('Evaluation failed: ' + response.result.description);
if (!returnByValue)
return toHandle(context, response.result);
return context._createHandle(response.result);
if (response.result.objectId) {
const serializeFunction = function() {
try {
@ -184,7 +183,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
if (response.wasThrown)
throw new Error('Evaluation failed: ' + response.result.description);
if (!returnByValue)
return toHandle(context, response.result);
return context._createHandle(response.result);
if (response.result.objectId) {
const serializeFunction = function() {
try {
@ -281,7 +280,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
for (const property of response.properties) {
if (!property.enumerable)
continue;
result.set(property.name, toHandle(handle.executionContext(), property.value));
result.set(property.name, handle.executionContext()._createHandle(property.value));
}
return result;
}
@ -332,19 +331,6 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
}
const remoteObjectSymbol = Symbol('RemoteObject');
export function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
return (handle as any)[remoteObjectSymbol];
}
export function toHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
if (remoteObject.subtype === 'node' && context.frame()) {
const handle = new dom.ElementHandle(context);
(handle as any)[remoteObjectSymbol] = remoteObject;
return handle;
}
const handle = new js.JSHandle(context);
(handle as any)[remoteObjectSymbol] = remoteObject;
return handle;
function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

View file

@ -22,8 +22,8 @@ import * as dom from '../dom';
import * as frames from '../frames';
import * as types from '../types';
import { TargetSession } from './Connection';
import { toRemoteObject } from './ExecutionContext';
import { FrameManager } from './FrameManager';
import { Protocol } from './protocol';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -50,6 +50,10 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
return this._frameManager.page()._javascriptEnabled;
}
isElement(remoteObject: any): boolean {
return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node';
}
async boundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
throw new Error('boundingBox() is not implemented');
}
@ -142,3 +146,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
return handle;
}
}
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

View file

@ -26,16 +26,15 @@ import { TargetSession, TargetSessionEvents } from './Connection';
import { Events } from './events';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawKeyboardImpl, RawMouseImpl } from './Input';
import { toHandle } from './ExecutionContext';
import { NetworkManagerEvents } from './NetworkManager';
import { Protocol } from './protocol';
import { Target } from './Target';
import { TaskQueue } from './TaskQueue';
import * as input from '../input';
import * as types from '../types';
import * as dom from '../dom';
import * as frames from '../frames';
import * as js from '../javascript';
import * as dom from '../dom';
import * as network from '../network';
import * as dialog from '../dialog';
import * as console from '../console';
@ -170,14 +169,14 @@ export class Page extends EventEmitter {
derivedType = 'timeEnd';
const mainFrameContext = await this.mainFrame().executionContext();
const handles = (parameters || []).map(p => {
let context = null;
let context: js.ExecutionContext | null = null;
if (p.objectId) {
const objectId = JSON.parse(p.objectId);
context = this._frameManager._contextIdToContext.get(objectId.injectedScriptId);
} else {
context = mainFrameContext;
}
return toHandle(context, p);
return context._createHandle(p);
});
this.emit(Events.Page.Console, new console.ConsoleMessage(derivedType, handles.length ? undefined : text, handles, { url, lineNumber, columnNumber }));
}
@ -446,7 +445,7 @@ export class Page extends EventEmitter {
if (!this._fileChooserInterceptors.size)
return;
const context = await this._frameManager.frame(event.frameId)._utilityContext();
const handle = toHandle(context, event.element) as dom.ElementHandle;
const handle = context._createHandle(event.element).asElement()!;
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);

View file

@ -5,9 +5,9 @@ export { TimeoutError } from '../Errors';
export { Browser, BrowserContext } from './Browser';
export { BrowserFetcher } from './BrowserFetcher';
export { ExecutionContext, JSHandle } from '../javascript';
export { ElementHandle } from '../dom';
export { Frame } from '../frames';
export { Mouse, Keyboard } from '../input';
export { ElementHandle } from '../dom';
export { Request, Response } from '../network';
export { Page } from './Page';
export { Playwright } from './Playwright';