diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index f9a914da38..37d5ee6d3d 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -17,8 +17,8 @@ import { CDPSession } from './Connection'; import { helper } from '../helper'; -import { valueFromRemoteObject, getExceptionMessage } from './protocolHelper'; -import { createJSHandle, JSHandle, ElementHandle } from './JSHandle'; +import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper'; +import { createJSHandle, ElementHandle } from './JSHandle'; import { Protocol } from './protocol'; import { Response } from './NetworkManager'; import * as js from '../javascript'; @@ -26,9 +26,10 @@ import * as js from '../javascript'; export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; -export type ExecutionContext = js.ExecutionContext; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { _client: CDPSession; _contextId: number; @@ -107,17 +108,18 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate> { + const response = await this._client.send('Runtime.getProperties', { + objectId: toRemoteObject(handle).objectId, + ownProperties: true + }); + const result = new Map(); + for (const property of response.result) { + if (!property.enumerable) + continue; + result.set(property.name, createJSHandle(handle.executionContext(), property.value)); + } + return result; + } + + async releaseHandle(handle: JSHandle): Promise { + await releaseObject(this._client, toRemoteObject(handle)); + } + + async handleJSONValue(handle: JSHandle): Promise { + const remoteObject = toRemoteObject(handle); + if (remoteObject.objectId) { + const response = await this._client.send('Runtime.callFunctionOn', { + functionDeclaration: 'function() { return this; }', + objectId: remoteObject.objectId, + returnByValue: true, + awaitPromise: true, + }); + return valueFromRemoteObject(response.result); + } + return valueFromRemoteObject(remoteObject); + } + + handleToString(handle: JSHandle): string { + const object = toRemoteObject(handle); + if (object.objectId) { + const type = object.subtype || object.type; + return 'JSHandle@' + type; + } + return 'JSHandle:' + valueFromRemoteObject(object); + } +} + +const remoteObjectSymbol = Symbol('RemoteObject'); + +export function toRemoteObject(handle: JSHandle): Protocol.Runtime.RemoteObject { + return (handle as any)[remoteObjectSymbol]; +} + +export function markJSHandle(handle: JSHandle, remoteObject: Protocol.Runtime.RemoteObject) { + (handle as any)[remoteObjectSymbol] = remoteObject; } diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index c7d65a6114..906a793c09 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -19,14 +19,14 @@ import { EventEmitter } from 'events'; import { assert, debugError } from '../helper'; import { TimeoutSettings } from '../TimeoutSettings'; import { CDPSession } from './Connection'; -import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate, ExecutionContext } from './ExecutionContext'; +import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate, ExecutionContext, JSHandle, toRemoteObject } from './ExecutionContext'; import * as frames from '../frames'; import * as js from '../javascript'; import { LifecycleWatcher } from './LifecycleWatcher'; import { NetworkManager, Response } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; -import { ElementHandle, JSHandle, createJSHandle } from './JSHandle'; +import { ElementHandle, createJSHandle } from './JSHandle'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -45,9 +45,9 @@ type FrameData = { lifecycleEvents: Set, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _client: CDPSession; private _page: Page; private _networkManager: NetworkManager; @@ -185,7 +185,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { const nodeInfo = await this._client.send('DOM.describeNode', { - objectId: elementHandle._remoteObject.objectId, + objectId: toRemoteObject(elementHandle).objectId, }); return (context._delegate as ExecutionContextDelegate).adoptBackendNodeId(context, nodeInfo.node.backendNodeId); } diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index 0a9d3b3f68..e419e375c8 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -19,13 +19,14 @@ import { assert, debugError, helper } from '../helper'; import Injected from '../injected/injected'; import * as input from '../input'; import * as types from '../types'; +import * as js from '../javascript'; import { CDPSession } from './Connection'; import { Frame } from './FrameManager'; import { FrameManager } from './FrameManager'; import { Page } from './Page'; import { Protocol } from './protocol'; -import { releaseObject, valueFromRemoteObject } from './protocolHelper'; -import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; +import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext'; +import { Response } from './NetworkManager'; type SelectorRoot = Element | ShadowRoot | Document; @@ -41,102 +42,24 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol const frameManager = frame._delegate as FrameManager; return new ElementHandle(context, delegate._client, remoteObject, frameManager.page(), frameManager); } - return new JSHandle(context, delegate._client, remoteObject); + const handle = new js.JSHandle(context); + markJSHandle(handle, remoteObject); + return handle; } -export class JSHandle { - _context: ExecutionContext; - protected _client: CDPSession; - _remoteObject: Protocol.Runtime.RemoteObject; - _disposed = false; - - constructor(context: ExecutionContext, client: CDPSession, remoteObject: Protocol.Runtime.RemoteObject) { - this._context = context; - this._client = client; - this._remoteObject = remoteObject; - } - - executionContext(): ExecutionContext { - return this._context; - } - - evaluate: types.EvaluateOn = (pageFunction, ...args) => { - return this.executionContext().evaluate(pageFunction, this, ...args); - } - - evaluateHandle: types.EvaluateHandleOn = (pageFunction, ...args) => { - return this.executionContext().evaluateHandle(pageFunction, this, ...args); - } - - async getProperty(propertyName: string): Promise { - const objectHandle = await this.evaluateHandle((object, propertyName) => { - const result = {__proto__: null}; - result[propertyName] = object[propertyName]; - return result; - }, propertyName); - const properties = await objectHandle.getProperties(); - const result = properties.get(propertyName) || null; - await objectHandle.dispose(); - return result; - } - - async getProperties(): Promise> { - const response = await this._client.send('Runtime.getProperties', { - objectId: this._remoteObject.objectId, - ownProperties: true - }); - const result = new Map(); - for (const property of response.result) { - if (!property.enumerable) - continue; - result.set(property.name, createJSHandle(this._context, property.value)); - } - return result; - } - - async jsonValue(): Promise { - if (this._remoteObject.objectId) { - const response = await this._client.send('Runtime.callFunctionOn', { - functionDeclaration: 'function() { return this; }', - objectId: this._remoteObject.objectId, - returnByValue: true, - awaitPromise: true, - }); - return valueFromRemoteObject(response.result); - } - return valueFromRemoteObject(this._remoteObject); - } - - asElement(): ElementHandle | null { - return null; - } - - async dispose() { - if (this._disposed) - return; - this._disposed = true; - await releaseObject(this._client, this._remoteObject); - } - - toString(): string { - if (this._remoteObject.objectId) { - const type = this._remoteObject.subtype || this._remoteObject.type; - return 'JSHandle@' + type; - } - return 'JSHandle:' + valueFromRemoteObject(this._remoteObject); - } -} - -export class ElementHandle extends JSHandle { +export class ElementHandle extends js.JSHandle { + private _client: CDPSession; + private _remoteObject: Protocol.Runtime.RemoteObject; private _page: Page; private _frameManager: FrameManager; constructor(context: ExecutionContext, client: CDPSession, remoteObject: Protocol.Runtime.RemoteObject, page: Page, frameManager: FrameManager) { - super(context, client, remoteObject); + super(context); this._client = client; this._remoteObject = remoteObject; this._page = page; this._frameManager = frameManager; + markJSHandle(this, remoteObject); } asElement(): ElementHandle | null { diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index 656d0b8979..e47dd2f5d9 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -36,7 +36,8 @@ import { Workers } from './features/workers'; import { Frame } from './FrameManager'; import { FrameManager, FrameManagerEvents } from './FrameManager'; import { RawMouseImpl, RawKeyboardImpl } from './Input'; -import { createJSHandle, ElementHandle, JSHandle } from './JSHandle'; +import { createJSHandle, ElementHandle } from './JSHandle'; +import { JSHandle, toRemoteObject } from './ExecutionContext'; import { NetworkManagerEvents, Response } from './NetworkManager'; import { Protocol } from './protocol'; import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper'; @@ -359,7 +360,7 @@ export class Page extends EventEmitter { } const textTokens = []; for (const arg of args) { - const remoteObject = arg._remoteObject; + const remoteObject = toRemoteObject(arg); if (remoteObject.objectId) textTokens.push(arg.toString()); else diff --git a/src/chromium/api.ts b/src/chromium/api.ts index feaf7b0946..ba8ceb3d4b 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -8,7 +8,7 @@ export { BrowserFetcher } from './BrowserFetcher'; export { Chromium } from './features/chromium'; export { CDPSession } from './Connection'; export { Dialog } from './Dialog'; -export { ExecutionContext } from '../javascript'; +export { ExecutionContext, JSHandle } from '../javascript'; export { Accessibility } from './features/accessibility'; export { Coverage } from './features/coverage'; export { Overrides } from './features/overrides'; @@ -18,7 +18,7 @@ export { Permissions } from './features/permissions'; export { Worker, Workers } from './features/workers'; export { Frame } from '../frames'; export { Keyboard, Mouse } from '../input'; -export { ElementHandle, JSHandle } from './JSHandle'; +export { ElementHandle } from './JSHandle'; export { Request, Response } from './NetworkManager'; export { ConsoleMessage, FileChooser, Page } from './Page'; export { Playwright } from './Playwright'; diff --git a/src/chromium/features/accessibility.ts b/src/chromium/features/accessibility.ts index dfb15d02c5..34974773fc 100644 --- a/src/chromium/features/accessibility.ts +++ b/src/chromium/features/accessibility.ts @@ -18,6 +18,7 @@ import { CDPSession } from '../Connection'; import { ElementHandle } from '../JSHandle'; import { Protocol } from '../protocol'; +import { toRemoteObject } from '../ExecutionContext'; type SerializedAXNode = { role: string, @@ -72,7 +73,7 @@ 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: root._remoteObject.objectId}); + const {node} = await this._client.send('DOM.describeNode', {objectId: toRemoteObject(root).objectId}); backendNodeId = node.backendNodeId; } const defaultRoot = AXNode.createTree(nodes); diff --git a/src/chromium/features/workers.ts b/src/chromium/features/workers.ts index 3184b002d3..78a0006c1d 100644 --- a/src/chromium/features/workers.ts +++ b/src/chromium/features/workers.ts @@ -17,12 +17,12 @@ import { EventEmitter } from 'events'; import { CDPSession, Connection } from '../Connection'; import { debugError } from '../../helper'; -import { JSHandle } from '../JSHandle'; import { Protocol } from '../protocol'; import { Events } from '../events'; import * as types from '../../types'; import * as js from '../../javascript'; -import { ExecutionContext, ExecutionContextDelegate } from '../ExecutionContext'; +import { JSHandle, ExecutionContext, ExecutionContextDelegate } from '../ExecutionContext'; +import { createJSHandle } from '../JSHandle'; type AddToConsoleCallback = (type: string, args: JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void; type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void; @@ -68,7 +68,7 @@ export class Worker extends EventEmitter { this._executionContextPromise = new Promise(x => this._executionContextCallback = x); let jsHandleFactory: (o: Protocol.Runtime.RemoteObject) => JSHandle; this._client.once('Runtime.executionContextCreated', async event => { - jsHandleFactory = remoteObject => new JSHandle(executionContext, client, remoteObject); + jsHandleFactory = remoteObject => createJSHandle(executionContext, remoteObject); const executionContext = new js.ExecutionContext(new ExecutionContextDelegate(client, event.context), null); this._executionContextCallback(executionContext); }); diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index eda9a382bf..72100e112d 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -15,15 +15,16 @@ * limitations under the License. */ -import {helper} from '../helper'; -import { JSHandle, createHandle, ElementHandle } from './JSHandle'; +import {helper, debugError} from '../helper'; +import { createHandle, ElementHandle } from './JSHandle'; import { Response } from './NetworkManager'; import * as js from '../javascript'; import { JugglerSession } from './Connection'; -export type ExecutionContext = js.ExecutionContext; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { _session: JugglerSession; _executionContextId: string; @@ -74,12 +75,12 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { - if (arg instanceof JSHandle) { + if (arg instanceof js.JSHandle) { if (arg._context !== context) throw new Error('JSHandles can be evaluated only in the context they were created!'); if (arg._disposed) throw new Error('JSHandle is disposed!'); - return arg._protocolValue; + return this._toProtocolValue(toPayload(arg)); } if (Object.is(arg, Infinity)) return {unserializableValue: 'Infinity'}; @@ -112,4 +113,72 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate> { + const response = await this._session.send('Runtime.getObjectProperties', { + executionContextId: this._executionContextId, + objectId: toPayload(handle).objectId, + }); + const result = new Map(); + for (const property of response.properties) + result.set(property.name, createHandle(handle.executionContext(), property.value, null)); + return result; + } + + async releaseHandle(handle: JSHandle): Promise { + await this._session.send('Runtime.disposeObject', { + executionContextId: this._executionContextId, + objectId: toPayload(handle).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. + debugError(error); + }); + } + + async handleJSONValue(handle: JSHandle): Promise { + const payload = toPayload(handle); + 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)], + }); + return deserializeValue(simpleValue.result); + } + + handleToString(handle: JSHandle): string { + const payload = toPayload(handle); + if (payload.objectId) + return 'JSHandle@' + (payload.subtype || payload.type); + return 'JSHandle:' + deserializeValue(payload); + } + + private _toProtocolValue(payload: any): any { + return { value: payload.value, unserializableValue: payload.unserializableValue, objectId: payload.objectId }; + } +} + +const payloadSymbol = Symbol('payload'); + +export function toPayload(handle: JSHandle): any { + return (handle as any)[payloadSymbol]; +} + +export function markJSHandle(handle: JSHandle, payload: any) { + (handle as any)[payloadSymbol] = payload; +} + +export function deserializeValue({unserializableValue, value}) { + if (unserializableValue === 'Infinity') + return Infinity; + if (unserializableValue === '-Infinity') + return -Infinity; + if (unserializableValue === '-0') + return -0; + if (unserializableValue === 'NaN') + return NaN; + return value; } diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 63ac6e3f7d..6e5fedcdcf 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -20,9 +20,9 @@ import { Page } from './Page'; import {RegisteredListener, helper, assert} from '../helper'; import {TimeoutError} from '../Errors'; import {EventEmitter} from 'events'; -import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; +import { JSHandle, ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; import {NavigationWatchdog, NextNavigationWatchdog} from './NavigationWatchdog'; -import { JSHandle, ElementHandle } from './JSHandle'; +import { ElementHandle } from './JSHandle'; import { TimeoutSettings } from '../TimeoutSettings'; import { Response } from './NetworkManager'; import * as frames from '../frames'; @@ -43,9 +43,9 @@ type FrameData = { firedEvents: Set, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _session: JugglerSession; _page: Page; _networkManager: any; diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index c3fd3dfc99..197d0101ed 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -19,134 +19,31 @@ import { assert, debugError, helper } from '../helper'; import Injected from '../injected/injected'; import * as input from '../input'; import * as types from '../types'; +import * as js from '../javascript'; import { JugglerSession } from './Connection'; import { Frame, FrameManager } from './FrameManager'; import { Page } from './Page'; -import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; +import { JSHandle, ExecutionContext, markJSHandle, ExecutionContextDelegate } from './ExecutionContext'; +import { Response } from './NetworkManager'; type SelectorRoot = Element | ShadowRoot | Document; -export class JSHandle { - _context: ExecutionContext; - protected _session: JugglerSession; - private _executionContextId: string; - protected _objectId: string; - private _type: string; - private _subtype: string; - _disposed: boolean; - _protocolValue: { unserializableValue: any; value: any; objectId: any; }; - - constructor(context: ExecutionContext, payload: any) { - this._context = context; - const delegate = context._delegate as ExecutionContextDelegate; - this._session = delegate._session; - this._executionContextId = delegate._executionContextId; - this._objectId = payload.objectId; - this._type = payload.type; - this._subtype = payload.subtype; - this._disposed = false; - this._protocolValue = { - unserializableValue: payload.unserializableValue, - value: payload.value, - objectId: payload.objectId, - }; - } - - executionContext(): ExecutionContext { - return this._context; - } - - evaluate: types.EvaluateOn = (pageFunction, ...args) => { - return this.executionContext().evaluate(pageFunction, this, ...args); - } - - evaluateHandle: types.EvaluateHandleOn = (pageFunction, ...args) => { - return this.executionContext().evaluateHandle(pageFunction, this, ...args); - } - - toString(): string { - if (this._objectId) - return 'JSHandle@' + (this._subtype || this._type); - return 'JSHandle:' + this._deserializeValue(this._protocolValue); - } - - async getProperty(propertyName: string): Promise { - const objectHandle = await this._context.evaluateHandle((object, propertyName) => { - const result = {__proto__: null}; - result[propertyName] = object[propertyName]; - return result; - }, this, propertyName); - const properties = await objectHandle.getProperties(); - const result = properties.get(propertyName) || null; - await objectHandle.dispose(); - return result; - } - - async getProperties(): Promise> { - const response = await this._session.send('Runtime.getObjectProperties', { - executionContextId: this._executionContextId, - objectId: this._objectId, - }); - const result = new Map(); - for (const property of response.properties) - result.set(property.name, createHandle(this._context, property.value, null)); - - return result; - } - - _deserializeValue({unserializableValue, value}) { - if (unserializableValue === 'Infinity') - return Infinity; - if (unserializableValue === '-Infinity') - return -Infinity; - if (unserializableValue === '-0') - return -0; - if (unserializableValue === 'NaN') - return NaN; - return value; - } - - async jsonValue() { - if (!this._objectId) - return this._deserializeValue(this._protocolValue); - const simpleValue = await this._session.send('Runtime.callFunction', { - executionContextId: this._executionContextId, - returnByValue: true, - functionDeclaration: (e => e).toString(), - args: [this._protocolValue], - }); - return this._deserializeValue(simpleValue.result); - } - - asElement(): ElementHandle | null { - return null; - } - - async dispose() { - if (!this._objectId) - return; - this._disposed = true; - await this._session.send('Runtime.disposeObject', { - executionContextId: this._executionContextId, - objectId: this._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. - debugError(error); - }); - } -} - -export class ElementHandle extends JSHandle { +export class ElementHandle extends js.JSHandle { _frame: Frame; _frameId: string; _page: Page; + _context: ExecutionContext; + protected _session: JugglerSession; + protected _objectId: string; - constructor(frame: Frame, frameId: string, page: Page, context: ExecutionContext, payload: any) { - super(context, payload); + constructor(frame: Frame, frameId: string, page: Page, session: JugglerSession, context: ExecutionContext, payload: any) { + super(context); this._frame = frame; this._frameId = frameId; this._page = page; + this._session = session; + this._objectId = payload.objectId; + markJSHandle(this, payload); } async contentFrame(): Promise { @@ -392,9 +289,12 @@ export function createHandle(context: ExecutionContext, result: any, exceptionDe const frameManager = frame._delegate as FrameManager; const frameId = frameManager._frameData(frame).frameId; const page = frameManager._page; - return new ElementHandle(frame, frameId, page, context, result); + const session = (context._delegate as ExecutionContextDelegate)._session; + return new ElementHandle(frame, frameId, page, session, context, result); } - return new JSHandle(context, result); + const handle = new js.JSHandle(context); + markJSHandle(handle, result); + return handle; } function computeQuadArea(quad) { diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index c74b0e1db0..15ec3936c9 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -12,11 +12,12 @@ import { Accessibility } from './features/accessibility'; import { Interception } from './features/interception'; import { FrameManager, FrameManagerEvents, normalizeWaitUntil, Frame } from './FrameManager'; import { RawMouseImpl, RawKeyboardImpl } from './Input'; -import { createHandle, ElementHandle, JSHandle } from './JSHandle'; +import { createHandle, ElementHandle } from './JSHandle'; import { NavigationWatchdog } from './NavigationWatchdog'; import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager'; import * as input from '../input'; import * as types from '../types'; +import { JSHandle, toPayload, deserializeValue } from './ExecutionContext'; const writeFileAsync = helper.promisify(fs.writeFile); @@ -559,8 +560,9 @@ export class Page extends EventEmitter { export class ConsoleMessage { private _type: string; - private _args: any[]; + private _args: JSHandle[]; private _location: any; + constructor(type: string, args: Array, location) { this._type = type; this._args = args; @@ -581,9 +583,10 @@ export class ConsoleMessage { text(): string { return this._args.map(arg => { - if (arg._objectId) + const payload = toPayload(arg); + if (payload.objectId) return arg.toString(); - return arg._deserializeValue(arg._protocolValue); + return deserializeValue(payload); }).join(' '); } } diff --git a/src/firefox/api.ts b/src/firefox/api.ts index 2730ca6368..a553214777 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -6,12 +6,12 @@ export { Keyboard, Mouse } from '../input'; export { Browser, BrowserContext, Target } from './Browser'; export { BrowserFetcher } from './BrowserFetcher'; export { Dialog } from './Dialog'; -export { ExecutionContext } from '../javascript'; +export { ExecutionContext, JSHandle } from '../javascript'; export { Accessibility } from './features/accessibility'; export { Interception } from './features/interception'; export { Permissions } from './features/permissions'; export { Frame } from './FrameManager'; -export { ElementHandle, JSHandle } from './JSHandle'; +export { ElementHandle } from './JSHandle'; export { Request, Response } from './NetworkManager'; export { ConsoleMessage, FileChooser, Page } from './Page'; export { Playwright } from './Playwright'; diff --git a/src/frames.ts b/src/frames.ts index 5a1cda5488..525f89249a 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -26,11 +26,11 @@ import { TimeoutSettings } from './TimeoutSettings'; const readFileAsync = helper.promisify(fs.readFile); type WorldType = 'main' | 'utility'; -type World, ElementHandle extends types.ElementHandle, Response> = { - contextPromise: Promise>; - contextResolveCallback: (c: js.ExecutionContext) => void; - context: js.ExecutionContext | null; - waitTasks: Set>; +type World, Response> = { + contextPromise: Promise>; + contextResolveCallback: (c: js.ExecutionContext) => void; + context: js.ExecutionContext | null; + waitTasks: Set>; }; export type NavigateOptions = { @@ -42,24 +42,24 @@ export type GotoOptions = NavigateOptions & { referer?: string, }; -export interface FrameDelegate, ElementHandle extends types.ElementHandle, Response> { +export interface FrameDelegate, Response> { timeoutSettings(): TimeoutSettings; - navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise; - waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise; - setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; - adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext): Promise; + navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise; + waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise; + setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; + adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext): Promise; } -export class Frame, ElementHandle extends types.ElementHandle, Response> { - _delegate: FrameDelegate; - private _parentFrame: Frame; +export class Frame, Response> { + _delegate: FrameDelegate; + private _parentFrame: Frame; private _url = ''; private _detached = false; - private _worlds = new Map>(); - private _childFrames = new Set>(); + private _worlds = new Map>(); + private _childFrames = new Set>(); private _name: string; - constructor(delegate: FrameDelegate, parentFrame: Frame | null) { + constructor(delegate: FrameDelegate, parentFrame: Frame | null) { this._delegate = delegate; this._parentFrame = parentFrame; @@ -80,28 +80,28 @@ export class Frame> { + _mainContext(): Promise> { if (this._detached) throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); return this._worlds.get('main').contextPromise; } - _utilityContext(): Promise> { + _utilityContext(): Promise> { if (this._detached) throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); return this._worlds.get('utility').contextPromise; } - executionContext(): Promise> { + executionContext(): Promise> { return this._mainContext(); } - evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandle> = async (pageFunction, ...args) => { const context = await this._mainContext(); return context.evaluateHandle(pageFunction, ...args as any); } - evaluate: types.Evaluate = async (pageFunction, ...args) => { + evaluate: types.Evaluate> = async (pageFunction, ...args) => { const context = await this._mainContext(); return context.evaluate(pageFunction, ...args as any); } @@ -118,13 +118,13 @@ export class Frame = async (selector, pageFunction, ...args) => { + $eval: types.$Eval> = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); const document = await context._document(); return document.$eval(selector, pageFunction, ...args as any); } - $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { + $$eval: types.$$Eval> = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); const document = await context._document(); return document.$$eval(selector, pageFunction, ...args as any); @@ -160,11 +160,11 @@ export class Frame | null { + parentFrame(): Frame | null { return this._parentFrame; } - childFrames(): Frame[] { + childFrames(): Frame[] { return Array.from(this._childFrames); } @@ -368,7 +368,7 @@ export class Frame { + waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise | null> { const xPathPattern = '//'; if (helper.isString(selectorOrFunctionOrTimeout)) { @@ -415,7 +415,7 @@ export class Frame { + ...args): Promise> { const { polling = 'raf', timeout = this._delegate.timeoutSettings().timeout(), @@ -451,7 +451,7 @@ export class Frame): Promise { + private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise> { const task = new WaitTask(params, () => world.waitTasks.delete(task)); world.waitTasks.add(task); if (world.context) @@ -459,7 +459,7 @@ export class Frame | null) { + private _setContext(worldType: WorldType, context: js.ExecutionContext | null) { const world = this._worlds.get(worldType); world.context = context; if (context) { @@ -473,7 +473,7 @@ export class Frame) { + _contextCreated(worldType: WorldType, context: js.ExecutionContext) { const world = this._worlds.get(worldType); // In case of multiple sessions to the same target, there's a race between // connections so we might end up creating multiple isolated worlds. @@ -482,14 +482,14 @@ export class Frame) { + _contextDestroyed(context: js.ExecutionContext) { for (const [worldType, world] of this._worlds) { if (world.context === context) this._setContext(worldType, null); } } - private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise { + private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise { if (elementHandle.executionContext() === context) return elementHandle; const handle = this._delegate.adoptElementHandle(elementHandle, context); diff --git a/src/javascript.ts b/src/javascript.ts index e97885e3f5..9b773e71af 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -7,34 +7,38 @@ import * as injectedSource from './generated/injectedSource'; import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource'; import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource'; -export interface ExecutionContextDelegate, ElementHandle extends types.ElementHandle, Response> { - evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise; +export interface ExecutionContextDelegate, Response> { + evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise; + getProperties(handle: JSHandle): Promise>>; + releaseHandle(handle: JSHandle): Promise; + handleToString(handle: JSHandle): string; + handleJSONValue(handle: JSHandle): Promise; } -export class ExecutionContext, ElementHandle extends types.ElementHandle, Response> { - _delegate: ExecutionContextDelegate; - private _frame: frames.Frame; - private _injectedPromise: Promise | null = null; +export class ExecutionContext, Response> { + _delegate: ExecutionContextDelegate; + private _frame: frames.Frame; + private _injectedPromise: Promise> | null = null; private _documentPromise: Promise | null = null; - constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) { + constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) { this._delegate = delegate; this._frame = frame; } - frame(): frames.Frame | null { + frame(): frames.Frame | null { return this._frame; } - evaluate: types.Evaluate = (pageFunction, ...args) => { + evaluate: types.Evaluate> = (pageFunction, ...args) => { return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args); } - evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandle> = (pageFunction, ...args) => { return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args); } - _injected(): Promise { + _injected(): Promise> { if (!this._injectedPromise) { const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source]; const source = ` @@ -54,3 +58,58 @@ export class ExecutionContext, Response> { + _context: ExecutionContext; + _disposed = false; + + constructor(context: ExecutionContext) { + this._context = context; + } + + executionContext(): ExecutionContext { + return this._context; + } + + evaluate: types.EvaluateOn> = (pageFunction, ...args) => { + return this._context.evaluate(pageFunction, this, ...args); + } + + evaluateHandle: types.EvaluateHandleOn> = (pageFunction, ...args) => { + return this._context.evaluateHandle(pageFunction, this, ...args); + } + + async getProperty(propertyName: string): Promise | null> { + const objectHandle = await this.evaluateHandle((object, propertyName) => { + const result = {__proto__: null}; + result[propertyName] = object[propertyName]; + return result; + }, propertyName); + const properties = await objectHandle.getProperties(); + const result = properties.get(propertyName) || null; + await objectHandle.dispose(); + return result; + } + + getProperties(): Promise>> { + return this._context._delegate.getProperties(this); + } + + jsonValue(): Promise { + return this._context._delegate.handleJSONValue(this); + } + + asElement(): ElementHandle | null { + return null; + } + + async dispose() { + if (this._disposed) + return; + this._disposed = true; + await this._context._delegate.releaseHandle(this); + } + + toString(): string { + return this._context._delegate.handleToString(this); + } +} diff --git a/src/types.ts b/src/types.ts index 66651fdf3f..a206169056 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,18 +15,12 @@ export type $$Eval = (selector: string, pageFunct export type EvaluateOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateHandleOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; -export interface JSHandle, EHandle extends ElementHandle, Response> { - executionContext(): js.ExecutionContext; - dispose(): Promise; - asElement(): EHandle | null; -} - -export interface ElementHandle, EHandle extends ElementHandle, Response> extends JSHandle { +export interface ElementHandle, Response> extends js.JSHandle { $(selector: string): Promise; $x(expression: string): Promise; $$(selector: string): Promise; - $eval: $Eval; - $$eval: $$Eval; + $eval: $Eval>; + $$eval: $$Eval>; click(options?: input.ClickOptions): Promise; dblclick(options?: input.MultiClickOptions): Promise; tripleclick(options?: input.MultiClickOptions): Promise; diff --git a/src/waitTask.ts b/src/waitTask.ts index 9bd57cc61f..2c1b15ceac 100644 --- a/src/waitTask.ts +++ b/src/waitTask.ts @@ -15,12 +15,12 @@ export type WaitTaskParams = { args: any[]; }; -export class WaitTask, ElementHandle extends types.ElementHandle, Response> { - readonly promise: Promise; +export class WaitTask, Response> { + readonly promise: Promise>; private _cleanup: () => void; private _params: WaitTaskParams & { predicateBody: string }; private _runCount: number; - private _resolve: (result: JSHandle) => void; + private _resolve: (result: js.JSHandle) => void; private _reject: (reason: Error) => void; private _timeoutTimer: NodeJS.Timer; private _terminated: boolean; @@ -39,7 +39,7 @@ export class WaitTask((resolve, reject) => { + this.promise = new Promise>((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); @@ -57,9 +57,9 @@ export class WaitTask) { + async rerun(context: js.ExecutionContext) { const runCount = ++this._runCount; - let success: JSHandle | null = null; + let success: js.JSHandle | null = null; let error = null; try { success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args); diff --git a/src/webkit/ExecutionContext.ts b/src/webkit/ExecutionContext.ts index 95b56357df..23c8518cdc 100644 --- a/src/webkit/ExecutionContext.ts +++ b/src/webkit/ExecutionContext.ts @@ -17,8 +17,8 @@ import { TargetSession } from './Connection'; import { helper } from '../helper'; -import { valueFromRemoteObject } from './protocolHelper'; -import { createJSHandle, JSHandle, ElementHandle } from './JSHandle'; +import { valueFromRemoteObject, releaseObject } from './protocolHelper'; +import { createJSHandle, ElementHandle } from './JSHandle'; import { Protocol } from './protocol'; import { Response } from './NetworkManager'; import * as js from '../javascript'; @@ -26,9 +26,10 @@ import * as js from '../javascript'; export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; -export type ExecutionContext = js.ExecutionContext; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { private _globalObjectId?: string; _session: TargetSession; private _contextId: number; @@ -218,15 +219,16 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate> { + const response = await this._session.send('Runtime.getProperties', { + objectId: toRemoteObject(handle).objectId, + ownProperties: true + }); + const result = new Map(); + for (const property of response.properties) { + if (!property.enumerable) + continue; + result.set(property.name, createJSHandle(handle.executionContext(), property.value)); + } + return result; + } + + async releaseHandle(handle: JSHandle): Promise { + await releaseObject(this._session, toRemoteObject(handle)); + } + + async handleJSONValue(handle: JSHandle): Promise { + const remoteObject = toRemoteObject(handle); + if (remoteObject.objectId) { + const response = await this._session.send('Runtime.callFunctionOn', { + functionDeclaration: 'function() { return this; }', + objectId: remoteObject.objectId, + returnByValue: true + }); + return valueFromRemoteObject(response.result); + } + return valueFromRemoteObject(remoteObject); + } + + handleToString(handle: JSHandle): string { + const object = toRemoteObject(handle); + if (object.objectId) { + let type: string = object.subtype || object.type; + // FIXME: promise doesn't have special subtype in WebKit. + if (object.className === 'Promise') + type = 'promise'; + return 'JSHandle@' + type; + } + return 'JSHandle:' + valueFromRemoteObject(object); + } +} + +const remoteObjectSymbol = Symbol('RemoteObject'); + +export function toRemoteObject(handle: JSHandle): Protocol.Runtime.RemoteObject { + return (handle as any)[remoteObjectSymbol]; +} + +export function markJSHandle(handle: JSHandle, remoteObject: Protocol.Runtime.RemoteObject) { + (handle as any)[remoteObjectSymbol] = remoteObject; } diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 917039a306..f1b30e9ba9 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -21,8 +21,8 @@ import { Events } from './events'; import { assert, debugError, helper, RegisteredListener } from '../helper'; import { TimeoutSettings } from '../TimeoutSettings'; import { TargetSession } from './Connection'; -import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; -import { ElementHandle, JSHandle } from './JSHandle'; +import { JSHandle, ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; +import { ElementHandle } from './JSHandle'; import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; @@ -42,9 +42,9 @@ type FrameData = { id: string, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _session: TargetSession; _page: Page; _networkManager: NetworkManager; diff --git a/src/webkit/JSHandle.ts b/src/webkit/JSHandle.ts index 044efde7bd..55107b3bd6 100644 --- a/src/webkit/JSHandle.ts +++ b/src/webkit/JSHandle.ts @@ -19,13 +19,14 @@ import * as fs from 'fs'; import { assert, debugError, helper } from '../helper'; import * as input from '../input'; import { TargetSession } from './Connection'; -import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; +import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext'; +import { Response } from './NetworkManager'; import { FrameManager } from './FrameManager'; import { Page } from './Page'; import { Protocol } from './protocol'; -import { releaseObject, valueFromRemoteObject } from './protocolHelper'; import Injected from '../injected/injected'; import * as types from '../types'; +import * as js from '../javascript'; type SelectorRoot = Element | ShadowRoot | Document; @@ -38,104 +39,24 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol const frameManager = frame._delegate as FrameManager; return new ElementHandle(context, delegate._session, remoteObject, frameManager.page(), frameManager); } - return new JSHandle(context, delegate._session, remoteObject); + const handle = new js.JSHandle(context); + markJSHandle(handle, remoteObject); + return handle; } -export class JSHandle { - _context: ExecutionContext; - protected _client: TargetSession; - _remoteObject: Protocol.Runtime.RemoteObject; - _disposed = false; - - constructor(context: ExecutionContext, client: TargetSession, remoteObject: Protocol.Runtime.RemoteObject) { - this._context = context; - this._client = client; - this._remoteObject = remoteObject; - } - - executionContext(): ExecutionContext { - return this._context; - } - - evaluate: types.EvaluateOn = (pageFunction, ...args) => { - return this.executionContext().evaluate(pageFunction, this, ...args); - } - - evaluateHandle: types.EvaluateHandleOn = (pageFunction, ...args) => { - return this.executionContext().evaluateHandle(pageFunction, this, ...args); - } - - async getProperty(propertyName: string): Promise { - const objectHandle = await this.evaluateHandle((object, propertyName) => { - const result = {__proto__: null}; - result[propertyName] = object[propertyName]; - return result; - }, propertyName); - const properties = await objectHandle.getProperties(); - const result = properties.get(propertyName) || null; - await objectHandle.dispose(); - return result; - } - - async getProperties(): Promise> { - const response = await this._client.send('Runtime.getProperties', { - objectId: this._remoteObject.objectId, - ownProperties: true - }); - const result = new Map(); - for (const property of response.properties) { - if (!property.enumerable) - continue; - result.set(property.name, createJSHandle(this._context, property.value)); - } - return result; - } - - async jsonValue(): Promise { - if (this._remoteObject.objectId) { - const response = await this._client.send('Runtime.callFunctionOn', { - functionDeclaration: 'function() { return this; }', - objectId: this._remoteObject.objectId, - returnByValue: true - }); - return valueFromRemoteObject(response.result); - } - return valueFromRemoteObject(this._remoteObject); - } - - asElement(): ElementHandle | null { - return null; - } - - async dispose() { - if (this._disposed) - return; - this._disposed = true; - await releaseObject(this._client, this._remoteObject); - } - - toString(): string { - if (this._remoteObject.objectId) { - let type: string = this._remoteObject.subtype || this._remoteObject.type; - // FIXME: promise doesn't have special subtype in WebKit. - if (this._remoteObject.className === 'Promise') - type = 'promise'; - return 'JSHandle@' + type; - } - return 'JSHandle:' + valueFromRemoteObject(this._remoteObject); - } -} - -export class ElementHandle extends JSHandle { +export class ElementHandle extends js.JSHandle { + private _client: TargetSession; + private _remoteObject: Protocol.Runtime.RemoteObject; private _page: Page; private _frameManager: FrameManager; constructor(context: ExecutionContext, client: TargetSession, remoteObject: Protocol.Runtime.RemoteObject, page: Page, frameManager: FrameManager) { - super(context, client, remoteObject); + super(context); this._client = client; this._remoteObject = remoteObject; this._page = page; this._frameManager = frameManager; + markJSHandle(this, remoteObject); } asElement(): ElementHandle | null { diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index 9dcfc2affb..1f3a0229d1 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -26,7 +26,8 @@ import { TargetSession, TargetSessionEvents } from './Connection'; import { Events } from './events'; import { Frame, FrameManager, FrameManagerEvents } from './FrameManager'; import { RawKeyboardImpl, RawMouseImpl } from './Input'; -import { createJSHandle, ElementHandle, JSHandle } from './JSHandle'; +import { createJSHandle, ElementHandle } from './JSHandle'; +import { JSHandle, toRemoteObject } from './ExecutionContext'; import { NetworkManagerEvents, Response } from './NetworkManager'; import { Protocol } from './protocol'; import { valueFromRemoteObject } from './protocolHelper'; @@ -159,7 +160,7 @@ export class Page extends EventEmitter { this.emit('error', new Error('Page crashed!')); } - async _onConsoleMessage(event : Protocol.Console.messageAddedPayload) { + async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) { const { type, level, text, parameters, url, line: lineNumber, column: columnNumber } = event.message; let derivedType: string = type; if (type === 'log') @@ -179,7 +180,7 @@ export class Page extends EventEmitter { }); const textTokens = []; for (const handle of handles) { - const remoteObject = handle._remoteObject; + const remoteObject = toRemoteObject(handle); if (remoteObject.objectId) textTokens.push(handle.toString()); else diff --git a/src/webkit/api.ts b/src/webkit/api.ts index e4c6a24fda..d7581d3759 100644 --- a/src/webkit/api.ts +++ b/src/webkit/api.ts @@ -4,10 +4,10 @@ export { TimeoutError } from '../Errors'; export { Browser, BrowserContext } from './Browser'; export { BrowserFetcher } from './BrowserFetcher'; -export { ExecutionContext } from '../javascript'; +export { ExecutionContext, JSHandle } from '../javascript'; export { Frame } from './FrameManager'; export { Mouse, Keyboard } from '../input'; -export { ElementHandle, JSHandle } from './JSHandle'; +export { ElementHandle } from './JSHandle'; export { Request, Response } from './NetworkManager'; export { ConsoleMessage, Page } from './Page'; export { Playwright } from './Playwright';