From 06ba0f7a7fad168c3b877c51f82799bda4e730b4 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 27 Nov 2019 12:39:53 -0800 Subject: [PATCH] chore: reuse ExecutionContext between browsers (#102) --- src/chromium/ExecutionContext.ts | 66 ++++++-------------------- src/chromium/FrameManager.ts | 13 +++--- src/chromium/JSHandle.ts | 7 +-- src/chromium/Page.ts | 3 +- src/chromium/api.ts | 2 +- src/chromium/features/workers.ts | 5 +- src/firefox/ExecutionContext.ts | 79 +++++++++++--------------------- src/firefox/FrameManager.ts | 10 ++-- src/firefox/JSHandle.ts | 7 +-- src/firefox/api.ts | 2 +- src/frames.ts | 53 ++++++++++----------- src/javascript.ts | 56 ++++++++++++++++++++++ src/types.ts | 13 ++---- src/waitTask.ts | 5 +- src/webkit/ExecutionContext.ts | 64 ++++++-------------------- src/webkit/FrameManager.ts | 13 +++--- src/webkit/JSHandle.ts | 7 +-- src/webkit/api.ts | 2 +- 18 files changed, 185 insertions(+), 222 deletions(-) create mode 100644 src/javascript.ts diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index 2a795cf333..f9a914da38 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -16,45 +16,28 @@ */ import { CDPSession } from './Connection'; -import { Frame } from './FrameManager'; import { helper } from '../helper'; import { valueFromRemoteObject, getExceptionMessage } from './protocolHelper'; -import { createJSHandle, ElementHandle, JSHandle } from './JSHandle'; +import { createJSHandle, JSHandle, ElementHandle } from './JSHandle'; import { Protocol } from './protocol'; -import * as injectedSource from '../generated/injectedSource'; -import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource'; -import * as xpathSelectorEngineSource from '../generated/xpathSelectorEngineSource'; -import * as types from '../types'; +import { Response } from './NetworkManager'; +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 class ExecutionContext { - _client: CDPSession; - private _frame: Frame; - private _injectedPromise: Promise | null = null; - private _documentPromise: Promise | null = null; - private _contextId: number; +export type ExecutionContext = js.ExecutionContext; - constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, frame: Frame | null) { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { + _client: CDPSession; + _contextId: number; + + constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription) { this._client = client; - this._frame = frame; this._contextId = contextPayload.id; } - frame(): Frame | null { - return this._frame; - } - - evaluate: types.Evaluate = (pageFunction, ...args) => { - return this._evaluateInternal(true /* returnByValue */, pageFunction, ...args); - } - - evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => { - return this._evaluateInternal(false /* returnByValue */, pageFunction, ...args); - } - - async _evaluateInternal(returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { + async evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; if (helper.isString(pageFunction)) { @@ -70,7 +53,7 @@ export class ExecutionContext { }).catch(rewriteError); if (exceptionDetails) throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); - return returnByValue ? valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject); + return returnByValue ? valueFromRemoteObject(remoteObject) : createJSHandle(context, remoteObject); } if (typeof pageFunction !== 'function') @@ -111,7 +94,7 @@ export class ExecutionContext { const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError); if (exceptionDetails) throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); - return returnByValue ? valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject); + return returnByValue ? valueFromRemoteObject(remoteObject) : createJSHandle(context, remoteObject); function convertArgument(arg: any): any { if (typeof arg === 'bigint') // eslint-disable-line valid-typeof @@ -126,7 +109,7 @@ export class ExecutionContext { return { unserializableValue: 'NaN' }; const objectHandle = arg && (arg instanceof JSHandle) ? arg : null; if (objectHandle) { - if (objectHandle._context !== this) + if (objectHandle._context !== context) throw new Error('JSHandles can be evaluated only in the context they were created!'); if (objectHandle._disposed) throw new Error('JSHandle is disposed!'); @@ -151,30 +134,11 @@ export class ExecutionContext { } } - async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId): Promise { + async adoptBackendNodeId(context: ExecutionContext, backendNodeId: Protocol.DOM.BackendNodeId) { const {object} = await this._client.send('DOM.resolveNode', { backendNodeId, executionContextId: this._contextId, }); - return createJSHandle(this, object) as ElementHandle; - } - - _injected(): Promise { - if (!this._injectedPromise) { - const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source]; - const source = ` - new (${injectedSource.source})([ - ${engineSources.join(',\n')} - ]) - `; - this._injectedPromise = this.evaluateHandle(source); - } - return this._injectedPromise; - } - - _document(): Promise { - if (!this._documentPromise) - this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!); - return this._documentPromise; + return createJSHandle(context, object) as ElementHandle; } } diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index a180c058ee..c7d65a6114 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -19,13 +19,14 @@ import { EventEmitter } from 'events'; import { assert, debugError } from '../helper'; import { TimeoutSettings } from '../TimeoutSettings'; import { CDPSession } from './Connection'; -import { EVALUATION_SCRIPT_URL, ExecutionContext } from './ExecutionContext'; +import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate, ExecutionContext } 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 } from './JSHandle'; +import { ElementHandle, JSHandle, createJSHandle } from './JSHandle'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -44,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; @@ -186,7 +187,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate !!element.multiple); diff --git a/src/chromium/api.ts b/src/chromium/api.ts index 3d222aee9f..feaf7b0946 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 './ExecutionContext'; +export { ExecutionContext } from '../javascript'; export { Accessibility } from './features/accessibility'; export { Coverage } from './features/coverage'; export { Overrides } from './features/overrides'; diff --git a/src/chromium/features/workers.ts b/src/chromium/features/workers.ts index c67d64c686..3184b002d3 100644 --- a/src/chromium/features/workers.ts +++ b/src/chromium/features/workers.ts @@ -16,12 +16,13 @@ */ import { EventEmitter } from 'events'; import { CDPSession, Connection } from '../Connection'; -import { ExecutionContext } from '../ExecutionContext'; 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'; type AddToConsoleCallback = (type: string, args: JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void; type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void; @@ -68,7 +69,7 @@ export class Worker extends EventEmitter { let jsHandleFactory: (o: Protocol.Runtime.RemoteObject) => JSHandle; this._client.once('Runtime.executionContextCreated', async event => { jsHandleFactory = remoteObject => new JSHandle(executionContext, client, remoteObject); - const executionContext = new ExecutionContext(client, event.context, null); + const executionContext = new js.ExecutionContext(new ExecutionContextDelegate(client, event.context), null); this._executionContextCallback(executionContext); }); // This might fail if the target is closed before we recieve all execution contexts. diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index 4ddc3ddce0..eda9a382bf 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -16,33 +16,42 @@ */ import {helper} from '../helper'; -import {JSHandle, createHandle, ElementHandle} from './JSHandle'; -import { Frame } from './FrameManager'; -import * as injectedSource from '../generated/injectedSource'; -import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource'; -import * as xpathSelectorEngineSource from '../generated/xpathSelectorEngineSource'; -import * as types from '../types'; +import { JSHandle, createHandle, ElementHandle } from './JSHandle'; +import { Response } from './NetworkManager'; +import * as js from '../javascript'; +import { JugglerSession } from './Connection'; -export class ExecutionContext { - _session: any; - _frame: Frame; +export type ExecutionContext = js.ExecutionContext; + +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { + _session: JugglerSession; _executionContextId: string; - private _injectedPromise: Promise | null = null; - private _documentPromise: Promise | null = null; - constructor(session: any, frame: Frame | null, executionContextId: string) { + constructor(session: JugglerSession, executionContextId: string) { this._session = session; - this._frame = frame; this._executionContextId = executionContextId; } - evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { + async evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { + if (returnByValue) { + try { + const handle = await this.evaluate(context, false /* returnByValue */, pageFunction, ...args as any); + const result = await handle.jsonValue(); + await handle.dispose(); + return result; + } catch (e) { + if (e.message.includes('cyclic object value') || e.message.includes('Object is not serializable')) + return undefined; + throw e; + } + } + if (helper.isString(pageFunction)) { const payload = await this._session.send('Runtime.evaluate', { expression: pageFunction.trim(), executionContextId: this._executionContextId, }).catch(rewriteError); - return createHandle(this, payload.result, payload.exceptionDetails); + return createHandle(context, payload.result, payload.exceptionDetails); } if (typeof pageFunction !== 'function') throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); @@ -66,7 +75,7 @@ export class ExecutionContext { } const protocolArgs = args.map(arg => { if (arg instanceof JSHandle) { - if (arg._context !== this) + 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!'); @@ -95,7 +104,7 @@ export class ExecutionContext { throw err; } const payload = await callFunctionPromise.catch(rewriteError); - return createHandle(this, payload.result, payload.exceptionDetails); + return createHandle(context, payload.result, payload.exceptionDetails); function rewriteError(error) { if (error.message.includes('Failed to find execution context with id')) @@ -103,40 +112,4 @@ export class ExecutionContext { throw error; } } - - frame() { - return this._frame; - } - - evaluate: types.Evaluate = async (pageFunction, ...args) => { - try { - const handle = await this.evaluateHandle(pageFunction, ...args as any); - const result = await handle.jsonValue(); - await handle.dispose(); - return result; - } catch (e) { - if (e.message.includes('cyclic object value') || e.message.includes('Object is not serializable')) - return undefined; - throw e; - } - } - - _injected(): Promise { - if (!this._injectedPromise) { - const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source]; - const source = ` - new (${injectedSource.source})([ - ${engineSources.join(',\n')} - ]) - `; - this._injectedPromise = this.evaluateHandle(source); - } - return this._injectedPromise; - } - - _document(): Promise { - if (!this._documentPromise) - this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!); - return this._documentPromise; - } } diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index d0feaf1a8c..63ac6e3f7d 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -20,11 +20,13 @@ import { Page } from './Page'; import {RegisteredListener, helper, assert} from '../helper'; import {TimeoutError} from '../Errors'; import {EventEmitter} from 'events'; -import {ExecutionContext} from './ExecutionContext'; +import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; import {NavigationWatchdog, NextNavigationWatchdog} from './NavigationWatchdog'; import { JSHandle, ElementHandle } from './JSHandle'; import { TimeoutSettings } from '../TimeoutSettings'; +import { Response } from './NetworkManager'; import * as frames from '../frames'; +import * as js from '../javascript'; export const FrameManagerEvents = { FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), @@ -41,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; @@ -80,7 +82,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, ElementHandle extends types.ElementHandle, ExecutionContext> = { - contextPromise: Promise; - contextResolveCallback: (c: ExecutionContext) => void; - context: ExecutionContext | null; - waitTasks: Set>; +type World, ElementHandle extends types.ElementHandle, Response> = { + contextPromise: Promise>; + contextResolveCallback: (c: js.ExecutionContext) => void; + context: js.ExecutionContext | null; + waitTasks: Set>; }; export type NavigateOptions = { @@ -41,24 +42,24 @@ export type GotoOptions = NavigateOptions & { referer?: string, }; -export interface FrameDelegate, ElementHandle extends types.ElementHandle, ExecutionContext extends types.ExecutionContext, Response> { +export interface FrameDelegate, ElementHandle extends types.ElementHandle, 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: 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, ExecutionContext extends types.ExecutionContext, Response> { - _delegate: FrameDelegate; - private _parentFrame: Frame; +export class Frame, ElementHandle extends types.ElementHandle, 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; @@ -79,19 +80,19 @@ export class Frame, Ele return this._delegate.waitForFrameNavigation(this, options); } - _mainContext(): Promise { + _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(); } @@ -159,11 +160,11 @@ export class Frame, Ele return this._url; } - parentFrame(): Frame | null { + parentFrame(): Frame | null { return this._parentFrame; } - childFrames(): Frame[] { + childFrames(): Frame[] { return Array.from(this._childFrames); } @@ -450,7 +451,7 @@ export class Frame, Ele this._parentFrame = null; } - private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise { + private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise { const task = new WaitTask(params, () => world.waitTasks.delete(task)); world.waitTasks.add(task); if (world.context) @@ -458,7 +459,7 @@ export class Frame, Ele return task.promise; } - private _setContext(worldType: WorldType, context: ExecutionContext | null) { + private _setContext(worldType: WorldType, context: js.ExecutionContext | null) { const world = this._worlds.get(worldType); world.context = context; if (context) { @@ -472,7 +473,7 @@ export class Frame, Ele } } - _contextCreated(worldType: WorldType, context: ExecutionContext) { + _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. @@ -481,14 +482,14 @@ export class Frame, Ele this._setContext(worldType, context); } - _contextDestroyed(context: ExecutionContext) { + _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: 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 new file mode 100644 index 0000000000..e97885e3f5 --- /dev/null +++ b/src/javascript.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as frames from './frames'; +import * as types from './types'; +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 class ExecutionContext, ElementHandle extends types.ElementHandle, Response> { + _delegate: ExecutionContextDelegate; + private _frame: frames.Frame; + private _injectedPromise: Promise | null = null; + private _documentPromise: Promise | null = null; + + constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) { + this._delegate = delegate; + this._frame = frame; + } + + frame(): frames.Frame | null { + return this._frame; + } + + evaluate: types.Evaluate = (pageFunction, ...args) => { + return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args); + } + + evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => { + return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args); + } + + _injected(): Promise { + if (!this._injectedPromise) { + const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source]; + const source = ` + new (${injectedSource.source})([ + ${engineSources.join(',\n')} + ]) + `; + this._injectedPromise = this.evaluateHandle(source); + } + return this._injectedPromise; + } + + _document(): Promise { + if (!this._documentPromise) + this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!); + return this._documentPromise; + } +} + diff --git a/src/types.ts b/src/types.ts index ab2e34fef0..66651fdf3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import * as input from './input'; +import * as js from './javascript'; type Boxed = { [Index in keyof Args]: Args[Index] | Handle }; type PageFunction = string | ((...args: Args) => R | Promise); @@ -14,19 +15,13 @@ export type $$Eval = (selector: string, pageFunct export type EvaluateOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateHandleOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; -export interface ExecutionContext, EHandle extends ElementHandle> { - evaluate: Evaluate; - evaluateHandle: EvaluateHandle; - _document(): Promise; -} - -export interface JSHandle, EHandle extends ElementHandle> { - executionContext(): ExecutionContext; +export interface JSHandle, EHandle extends ElementHandle, Response> { + executionContext(): js.ExecutionContext; dispose(): Promise; asElement(): EHandle | null; } -export interface ElementHandle, EHandle extends ElementHandle> extends JSHandle { +export interface ElementHandle, EHandle extends ElementHandle, Response> extends JSHandle { $(selector: string): Promise; $x(expression: string): Promise; $$(selector: string): Promise; diff --git a/src/waitTask.ts b/src/waitTask.ts index f3561e7916..9bd57cc61f 100644 --- a/src/waitTask.ts +++ b/src/waitTask.ts @@ -3,6 +3,7 @@ import { assert, helper } from './helper'; import * as types from './types'; +import * as js from './javascript'; import { TimeoutError } from './Errors'; export type WaitTaskParams = { @@ -14,7 +15,7 @@ export type WaitTaskParams = { args: any[]; }; -export class WaitTask, ElementHandle extends types.ElementHandle> { +export class WaitTask, ElementHandle extends types.ElementHandle, Response> { readonly promise: Promise; private _cleanup: () => void; private _params: WaitTaskParams & { predicateBody: string }; @@ -56,7 +57,7 @@ export class WaitTask, this._doCleanup(); } - async rerun(context: types.ExecutionContext) { + async rerun(context: js.ExecutionContext) { const runCount = ++this._runCount; let success: JSHandle | null = null; let error = null; diff --git a/src/webkit/ExecutionContext.ts b/src/webkit/ExecutionContext.ts index 9dd1bfd449..95b56357df 100644 --- a/src/webkit/ExecutionContext.ts +++ b/src/webkit/ExecutionContext.ts @@ -16,32 +16,27 @@ */ import { TargetSession } from './Connection'; -import { Frame } from './FrameManager'; import { helper } from '../helper'; import { valueFromRemoteObject } from './protocolHelper'; import { createJSHandle, JSHandle, ElementHandle } from './JSHandle'; import { Protocol } from './protocol'; -import * as injectedSource from '../generated/injectedSource'; -import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource'; -import * as xpathSelectorEngineSource from '../generated/xpathSelectorEngineSource'; -import * as types from '../types'; +import { Response } from './NetworkManager'; +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 class ExecutionContext { - _globalObjectId?: string; - _session: TargetSession; - _frame: Frame; - _contextId: number; - private _contextDestroyedCallback: any; - private _executionContextDestroyedPromise: Promise; - private _injectedPromise: Promise | null = null; - private _documentPromise: Promise | null = null; +export type ExecutionContext = js.ExecutionContext; - constructor(client: TargetSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, frame: Frame | null) { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { + private _globalObjectId?: string; + _session: TargetSession; + private _contextId: number; + private _contextDestroyedCallback: () => void; + private _executionContextDestroyedPromise: Promise; + + constructor(client: TargetSession, contextPayload: Protocol.Runtime.ExecutionContextDescription) { this._session = client; - this._frame = frame; this._contextId = contextPayload.id; this._contextDestroyedCallback = null; this._executionContextDestroyedPromise = new Promise((resolve, reject) => { @@ -53,19 +48,7 @@ export class ExecutionContext { this._contextDestroyedCallback(); } - frame(): Frame | null { - return this._frame; - } - - evaluate: types.Evaluate = (pageFunction, ...args) => { - return this._evaluateInternal(true /* returnByValue */, pageFunction, ...args); - } - - evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => { - return this._evaluateInternal(false /* returnByValue */, pageFunction, ...args); - } - - async _evaluateInternal(returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { + async evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; if (helper.isString(pageFunction)) { @@ -98,7 +81,7 @@ export class ExecutionContext { if (response.wasThrown) throw new Error('Evaluation failed: ' + response.result.description); if (!returnByValue) - return createJSHandle(this, response.result); + return createJSHandle(context, response.result); if (response.result.objectId) { const serializeFunction = function() { try { @@ -204,7 +187,7 @@ export class ExecutionContext { if (response.wasThrown) throw new Error('Evaluation failed: ' + response.result.description); if (!returnByValue) - return createJSHandle(this, response.result); + return createJSHandle(context, response.result); if (response.result.objectId) { const serializeFunction = function() { try { @@ -305,23 +288,4 @@ export class ExecutionContext { } return this._globalObjectId; } - - _injected(): Promise { - if (!this._injectedPromise) { - const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source]; - const source = ` - new (${injectedSource.source})([ - ${engineSources.join(',\n')} - ]) - `; - this._injectedPromise = this.evaluateHandle(source); - } - return this._injectedPromise; - } - - _document(): Promise { - if (!this._documentPromise) - this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!); - return this._documentPromise; - } } diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index bcf0b59682..917039a306 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -21,12 +21,13 @@ import { Events } from './events'; import { assert, debugError, helper, RegisteredListener } from '../helper'; import { TimeoutSettings } from '../TimeoutSettings'; import { TargetSession } from './Connection'; -import { ExecutionContext } from './ExecutionContext'; +import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext'; import { ElementHandle, JSHandle } from './JSHandle'; import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; import * as frames from '../frames'; +import * as js from '../javascript'; export const FrameManagerEvents = { FrameNavigatedWithinDocument: Symbol('FrameNavigatedWithinDocument'), @@ -41,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; @@ -102,7 +103,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate