chore: reuse ExecutionContext between browsers (#102)

This commit is contained in:
Dmitry Gozman 2019-11-27 12:39:53 -08:00 committed by GitHub
parent dfc5592910
commit 06ba0f7a7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 185 additions and 222 deletions

View file

@ -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<JSHandle> | null = null;
private _documentPromise: Promise<ElementHandle> | null = null;
private _contextId: number;
export type ExecutionContext = js.ExecutionContext<JSHandle, ElementHandle, Response>;
constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, frame: Frame | null) {
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<JSHandle, ElementHandle, Response> {
_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<JSHandle> = (pageFunction, ...args) => {
return this._evaluateInternal(true /* returnByValue */, pageFunction, ...args);
}
evaluateHandle: types.EvaluateHandle<JSHandle> = (pageFunction, ...args) => {
return this._evaluateInternal(false /* returnByValue */, pageFunction, ...args);
}
async _evaluateInternal(returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
async evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
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<ElementHandle> {
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<JSHandle> {
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<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
return createJSHandle(context, object) as ElementHandle;
}
}

View file

@ -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<string>,
};
export type Frame = frames.Frame<JSHandle, ElementHandle, ExecutionContext, Response>;
export type Frame = frames.Frame<JSHandle, ElementHandle, Response>;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response> {
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, Response> {
_client: CDPSession;
private _page: Page;
private _networkManager: NetworkManager;
@ -186,7 +187,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<J
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: elementHandle._remoteObject.objectId,
});
return context._adoptBackendNodeId(nodeInfo.node.backendNodeId);
return (context._delegate as ExecutionContextDelegate).adoptBackendNodeId(context, nodeInfo.node.backendNodeId);
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
@ -328,7 +329,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<J
const frame = this._frames.get(frameId) || null;
if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated')
this._isolatedWorlds.add(contextPayload.name);
const context: ExecutionContext = new ExecutionContext(this._client, contextPayload, frame);
const context: ExecutionContext = new js.ExecutionContext(new ExecutionContextDelegate(this._client, contextPayload), frame);
if (frame) {
if (contextPayload.auxData && !!contextPayload.auxData['isDefault'])
frame._contextCreated('main', context);

View file

@ -20,12 +20,12 @@ import Injected from '../injected/injected';
import * as input from '../input';
import * as types from '../types';
import { CDPSession } from './Connection';
import { ExecutionContext } from './ExecutionContext';
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';
type SelectorRoot = Element | ShadowRoot | Document;
@ -35,12 +35,13 @@ type Point = {
};
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): JSHandle {
const delegate = context._delegate as ExecutionContextDelegate;
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager;
return new ElementHandle(context, context._client, remoteObject, frameManager.page(), frameManager);
return new ElementHandle(context, delegate._client, remoteObject, frameManager.page(), frameManager);
}
return new JSHandle(context, context._client, remoteObject);
return new JSHandle(context, delegate._client, remoteObject);
}
export class JSHandle {

View file

@ -44,6 +44,7 @@ import { Target } from './Target';
import { TaskQueue } from './TaskQueue';
import * as input from '../input';
import * as types from '../types';
import { ExecutionContextDelegate } from './ExecutionContext';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -155,7 +156,7 @@ export class Page extends EventEmitter {
return;
const frame = this._frameManager.frame(event.frameId);
const context = await frame._utilityContext();
const handle = await context._adoptBackendNodeId(event.backendNodeId);
const handle = await (context._delegate as ExecutionContextDelegate).adoptBackendNodeId(context, event.backendNodeId);
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);

View file

@ -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';

View file

@ -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.

View file

@ -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<JSHandle, ElementHandle, Response>;
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<JSHandle, ElementHandle, Response> {
_session: JugglerSession;
_executionContextId: string;
private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<ElementHandle> | 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<JSHandle> = async (pageFunction, ...args) => {
async evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
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<JSHandle> = 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<JSHandle> {
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<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
}

View file

@ -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<string>,
};
export type Frame = frames.Frame<JSHandle, ElementHandle, ExecutionContext, Response>;
export type Frame = frames.Frame<JSHandle, ElementHandle, Response>;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response> {
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, Response> {
_session: JugglerSession;
_page: Page;
_networkManager: any;
@ -80,7 +82,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<J
_onExecutionContextCreated({executionContextId, auxData}) {
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
const context = new ExecutionContext(this._session, frame, executionContextId);
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._session, executionContextId), frame);
if (frame) {
frame._contextCreated('main', context);
frame._contextCreated('utility', context);

View file

@ -20,9 +20,9 @@ import Injected from '../injected/injected';
import * as input from '../input';
import * as types from '../types';
import { JugglerSession } from './Connection';
import { ExecutionContext } from './ExecutionContext';
import { Frame, FrameManager } from './FrameManager';
import { Page } from './Page';
import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext';
type SelectorRoot = Element | ShadowRoot | Document;
@ -38,8 +38,9 @@ export class JSHandle {
constructor(context: ExecutionContext, payload: any) {
this._context = context;
this._session = this._context._session;
this._executionContextId = this._context._executionContextId;
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;

View file

@ -6,7 +6,7 @@ export { Keyboard, Mouse } from '../input';
export { Browser, BrowserContext, Target } from './Browser';
export { BrowserFetcher } from './BrowserFetcher';
export { Dialog } from './Dialog';
export { ExecutionContext } from './ExecutionContext';
export { ExecutionContext } from '../javascript';
export { Accessibility } from './features/accessibility';
export { Interception } from './features/interception';
export { Permissions } from './features/permissions';

View file

@ -17,6 +17,7 @@
import * as types from './types';
import * as fs from 'fs';
import * as js from './javascript';
import { helper, assert } from './helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
import { waitForSelectorOrXPath, WaitTaskParams, WaitTask } from './waitTask';
@ -25,11 +26,11 @@ import { TimeoutSettings } from './TimeoutSettings';
const readFileAsync = helper.promisify(fs.readFile);
type WorldType = 'main' | 'utility';
type World<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>, ExecutionContext> = {
contextPromise: Promise<ExecutionContext>;
contextResolveCallback: (c: ExecutionContext) => void;
context: ExecutionContext | null;
waitTasks: Set<WaitTask<JSHandle, ElementHandle>>;
type World<JSHandle extends types.JSHandle<JSHandle, ElementHandle, Response>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle, Response>, Response> = {
contextPromise: Promise<js.ExecutionContext<JSHandle, ElementHandle, Response>>;
contextResolveCallback: (c: js.ExecutionContext<JSHandle, ElementHandle, Response>) => void;
context: js.ExecutionContext<JSHandle, ElementHandle, Response> | null;
waitTasks: Set<WaitTask<JSHandle, ElementHandle, Response>>;
};
export type NavigateOptions = {
@ -41,24 +42,24 @@ export type GotoOptions = NavigateOptions & {
referer?: string,
};
export interface FrameDelegate<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>, ExecutionContext extends types.ExecutionContext<JSHandle, ElementHandle>, Response> {
export interface FrameDelegate<JSHandle extends types.JSHandle<JSHandle, ElementHandle, Response>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle, Response>, Response> {
timeoutSettings(): TimeoutSettings;
navigateFrame(frame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>, url: string, options?: GotoOptions): Promise<Response | null>;
waitForFrameNavigation(frame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>, options?: NavigateOptions): Promise<Response | null>;
setFrameContent(frame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>, html: string, options?: NavigateOptions): Promise<void>;
adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle>;
navigateFrame(frame: Frame<JSHandle, ElementHandle, Response>, url: string, options?: GotoOptions): Promise<Response | null>;
waitForFrameNavigation(frame: Frame<JSHandle, ElementHandle, Response>, options?: NavigateOptions): Promise<Response | null>;
setFrameContent(frame: Frame<JSHandle, ElementHandle, Response>, html: string, options?: NavigateOptions): Promise<void>;
adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<JSHandle, ElementHandle, Response>): Promise<ElementHandle>;
}
export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>, ExecutionContext extends types.ExecutionContext<JSHandle, ElementHandle>, Response> {
_delegate: FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response>;
private _parentFrame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>;
export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle, Response>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle, Response>, Response> {
_delegate: FrameDelegate<JSHandle, ElementHandle, Response>;
private _parentFrame: Frame<JSHandle, ElementHandle, Response>;
private _url = '';
private _detached = false;
private _worlds = new Map<WorldType, World<JSHandle, ElementHandle, ExecutionContext>>();
private _childFrames = new Set<Frame<JSHandle, ElementHandle, ExecutionContext, Response>>();
private _worlds = new Map<WorldType, World<JSHandle, ElementHandle, Response>>();
private _childFrames = new Set<Frame<JSHandle, ElementHandle, Response>>();
private _name: string;
constructor(delegate: FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response>, parentFrame: Frame<JSHandle, ElementHandle, ExecutionContext, Response> | null) {
constructor(delegate: FrameDelegate<JSHandle, ElementHandle, Response>, parentFrame: Frame<JSHandle, ElementHandle, Response> | null) {
this._delegate = delegate;
this._parentFrame = parentFrame;
@ -79,19 +80,19 @@ export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, Ele
return this._delegate.waitForFrameNavigation(this, options);
}
_mainContext(): Promise<ExecutionContext> {
_mainContext(): Promise<js.ExecutionContext<JSHandle, ElementHandle, Response>> {
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<ExecutionContext> {
_utilityContext(): Promise<js.ExecutionContext<JSHandle, ElementHandle, Response>> {
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> {
executionContext(): Promise<js.ExecutionContext<JSHandle, ElementHandle, Response>> {
return this._mainContext();
}
@ -159,11 +160,11 @@ export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, Ele
return this._url;
}
parentFrame(): Frame<JSHandle, ElementHandle, ExecutionContext, Response> | null {
parentFrame(): Frame<JSHandle, ElementHandle, Response> | null {
return this._parentFrame;
}
childFrames(): Frame<JSHandle, ElementHandle, ExecutionContext, Response>[] {
childFrames(): Frame<JSHandle, ElementHandle, Response>[] {
return Array.from(this._childFrames);
}
@ -450,7 +451,7 @@ export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, Ele
this._parentFrame = null;
}
private _scheduleWaitTask(params: WaitTaskParams, world: World<JSHandle, ElementHandle, ExecutionContext>): Promise<JSHandle> {
private _scheduleWaitTask(params: WaitTaskParams, world: World<JSHandle, ElementHandle, Response>): Promise<JSHandle> {
const task = new WaitTask(params, () => world.waitTasks.delete(task));
world.waitTasks.add(task);
if (world.context)
@ -458,7 +459,7 @@ export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, Ele
return task.promise;
}
private _setContext(worldType: WorldType, context: ExecutionContext | null) {
private _setContext(worldType: WorldType, context: js.ExecutionContext<JSHandle, ElementHandle, Response> | null) {
const world = this._worlds.get(worldType);
world.context = context;
if (context) {
@ -472,7 +473,7 @@ export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, Ele
}
}
_contextCreated(worldType: WorldType, context: ExecutionContext) {
_contextCreated(worldType: WorldType, context: js.ExecutionContext<JSHandle, ElementHandle, Response>) {
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<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, Ele
this._setContext(worldType, context);
}
_contextDestroyed(context: ExecutionContext) {
_contextDestroyed(context: js.ExecutionContext<JSHandle, ElementHandle, Response>) {
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<ElementHandle> {
private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<JSHandle, ElementHandle, Response>, dispose: boolean): Promise<ElementHandle> {
if (elementHandle.executionContext() === context)
return elementHandle;
const handle = this._delegate.adoptElementHandle(elementHandle, context);

56
src/javascript.ts Normal file
View file

@ -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<JSHandle extends types.JSHandle<JSHandle, ElementHandle, Response>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle, Response>, Response> {
evaluate(context: ExecutionContext<JSHandle, ElementHandle, Response>, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
}
export class ExecutionContext<JSHandle extends types.JSHandle<JSHandle, ElementHandle, Response>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle, Response>, Response> {
_delegate: ExecutionContextDelegate<JSHandle, ElementHandle, Response>;
private _frame: frames.Frame<JSHandle, ElementHandle, Response>;
private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<ElementHandle> | null = null;
constructor(delegate: ExecutionContextDelegate<JSHandle, ElementHandle, Response>, frame: frames.Frame<JSHandle, ElementHandle, Response> | null) {
this._delegate = delegate;
this._frame = frame;
}
frame(): frames.Frame<JSHandle, ElementHandle, Response> | null {
return this._frame;
}
evaluate: types.Evaluate<JSHandle> = (pageFunction, ...args) => {
return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args);
}
evaluateHandle: types.EvaluateHandle<JSHandle> = (pageFunction, ...args) => {
return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args);
}
_injected(): Promise<JSHandle> {
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<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
}

View file

@ -2,6 +2,7 @@
// Licensed under the MIT license.
import * as input from './input';
import * as js from './javascript';
type Boxed<Args extends any[], Handle> = { [Index in keyof Args]: Args[Index] | Handle };
type PageFunction<Args extends any[], R = any> = string | ((...args: Args) => R | Promise<R>);
@ -14,19 +15,13 @@ export type $$Eval<Handle> = <Args extends any[], R>(selector: string, pageFunct
export type EvaluateOn<Handle> = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args, Handle>) => Promise<R>;
export type EvaluateHandleOn<Handle> = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args, Handle>) => Promise<Handle>;
export interface ExecutionContext<Handle extends JSHandle<Handle, EHandle>, EHandle extends ElementHandle<Handle, EHandle>> {
evaluate: Evaluate<Handle>;
evaluateHandle: EvaluateHandle<Handle>;
_document(): Promise<EHandle>;
}
export interface JSHandle<Handle extends JSHandle<Handle, EHandle>, EHandle extends ElementHandle<Handle, EHandle>> {
executionContext(): ExecutionContext<Handle, EHandle>;
export interface JSHandle<Handle extends JSHandle<Handle, EHandle, Response>, EHandle extends ElementHandle<Handle, EHandle, Response>, Response> {
executionContext(): js.ExecutionContext<Handle, EHandle, Response>;
dispose(): Promise<void>;
asElement(): EHandle | null;
}
export interface ElementHandle<Handle extends JSHandle<Handle, EHandle>, EHandle extends ElementHandle<Handle, EHandle>> extends JSHandle<Handle, EHandle> {
export interface ElementHandle<Handle extends JSHandle<Handle, EHandle, Response>, EHandle extends ElementHandle<Handle, EHandle, Response>, Response> extends JSHandle<Handle, EHandle, Response> {
$(selector: string): Promise<EHandle | null>;
$x(expression: string): Promise<EHandle[]>;
$$(selector: string): Promise<EHandle[]>;

View file

@ -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<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>> {
export class WaitTask<JSHandle extends types.JSHandle<JSHandle, ElementHandle, Response>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle, Response>, Response> {
readonly promise: Promise<JSHandle>;
private _cleanup: () => void;
private _params: WaitTaskParams & { predicateBody: string };
@ -56,7 +57,7 @@ export class WaitTask<JSHandle extends types.JSHandle<JSHandle, ElementHandle>,
this._doCleanup();
}
async rerun(context: types.ExecutionContext<JSHandle, ElementHandle>) {
async rerun(context: js.ExecutionContext<JSHandle, ElementHandle, Response>) {
const runCount = ++this._runCount;
let success: JSHandle | null = null;
let error = null;

View file

@ -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<unknown>;
private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<ElementHandle> | null = null;
export type ExecutionContext = js.ExecutionContext<JSHandle, ElementHandle, Response>;
constructor(client: TargetSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, frame: Frame | null) {
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<JSHandle, ElementHandle, Response> {
private _globalObjectId?: string;
_session: TargetSession;
private _contextId: number;
private _contextDestroyedCallback: () => void;
private _executionContextDestroyedPromise: Promise<unknown>;
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<JSHandle> = (pageFunction, ...args) => {
return this._evaluateInternal(true /* returnByValue */, pageFunction, ...args);
}
evaluateHandle: types.EvaluateHandle<JSHandle> = (pageFunction, ...args) => {
return this._evaluateInternal(false /* returnByValue */, pageFunction, ...args);
}
async _evaluateInternal(returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
async evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
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<JSHandle> {
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<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
}

View file

@ -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<JSHandle, ElementHandle, ExecutionContext, Response>;
export type Frame = frames.Frame<JSHandle, ElementHandle, Response>;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response> {
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, Response> {
_session: TargetSession;
_page: Page;
_networkManager: NetworkManager;
@ -102,7 +103,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<J
disconnectFromTarget() {
for (const context of this._contextIdToContext.values()) {
context._dispose();
(context._delegate as ExecutionContextDelegate)._dispose();
context.frame()._contextDestroyed(context);
}
// this._mainFrame = null;
@ -198,7 +199,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<J
frame._navigated(framePayload.url, framePayload.name);
for (const context of this._contextIdToContext.values()) {
if (context.frame() === frame) {
context._dispose();
(context._delegate as ExecutionContextDelegate)._dispose();
frame._contextDestroyed(context);
}
}
@ -230,7 +231,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<J
const frame = this._frames.get(frameId) || null;
if (!frame)
return;
const context: ExecutionContext = new ExecutionContext(this._session, contextPayload, frame);
const context: ExecutionContext = new js.ExecutionContext(new ExecutionContextDelegate(this._session, contextPayload), frame);
if (frame) {
frame._contextCreated('main', context);
frame._contextCreated('utility', context);

View file

@ -19,7 +19,7 @@ import * as fs from 'fs';
import { assert, debugError, helper } from '../helper';
import * as input from '../input';
import { TargetSession } from './Connection';
import { ExecutionContext } from './ExecutionContext';
import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext';
import { FrameManager } from './FrameManager';
import { Page } from './Page';
import { Protocol } from './protocol';
@ -32,12 +32,13 @@ type SelectorRoot = Element | ShadowRoot | Document;
const writeFileAsync = helper.promisify(fs.writeFile);
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
const delegate = context._delegate as ExecutionContextDelegate;
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager;
return new ElementHandle(context, context._session, remoteObject, frameManager.page(), frameManager);
return new ElementHandle(context, delegate._session, remoteObject, frameManager.page(), frameManager);
}
return new JSHandle(context, context._session, remoteObject);
return new JSHandle(context, delegate._session, remoteObject);
}
export class JSHandle {

View file

@ -4,7 +4,7 @@
export { TimeoutError } from '../Errors';
export { Browser, BrowserContext } from './Browser';
export { BrowserFetcher } from './BrowserFetcher';
export { ExecutionContext } from './ExecutionContext';
export { ExecutionContext } from '../javascript';
export { Frame } from './FrameManager';
export { Mouse, Keyboard } from '../input';
export { ElementHandle, JSHandle } from './JSHandle';