chore: encapsulate dom-related logic to DOMWorld, part 1 (#112)

This commit is contained in:
Dmitry Gozman 2019-11-28 12:50:52 -08:00 committed by GitHub
parent 336338c97e
commit 8efd258e40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 252 additions and 236 deletions

View File

@ -18,7 +18,6 @@
import { CDPSession } from './Connection';
import { helper } from '../helper';
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper';
import { createJSHandle } from './JSHandle';
import { Protocol } from './protocol';
import * as js from '../javascript';
import * as dom from '../dom';
@ -51,7 +50,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
}).catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return returnByValue ? valueFromRemoteObject(remoteObject) : createJSHandle(context, remoteObject);
return returnByValue ? valueFromRemoteObject(remoteObject) : toHandle(context, remoteObject);
}
if (typeof pageFunction !== 'function')
@ -92,7 +91,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return returnByValue ? valueFromRemoteObject(remoteObject) : createJSHandle(context, remoteObject);
return returnByValue ? valueFromRemoteObject(remoteObject) : toHandle(context, remoteObject);
function convertArgument(arg: any): any {
if (typeof arg === 'bigint') // eslint-disable-line valid-typeof
@ -133,14 +132,6 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
}
}
async adoptBackendNodeId(context: js.ExecutionContext, backendNodeId: Protocol.DOM.BackendNodeId) {
const {object} = await this._client.send('DOM.resolveNode', {
backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(context, object) as dom.ElementHandle;
}
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const response = await this._client.send('Runtime.getProperties', {
objectId: toRemoteObject(handle).objectId,
@ -150,7 +141,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
for (const property of response.result) {
if (!property.enumerable)
continue;
result.set(property.name, createJSHandle(handle.executionContext(), property.value));
result.set(property.name, toHandle(handle.executionContext(), property.value));
}
return result;
}
@ -189,6 +180,13 @@ export function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObje
return (handle as any)[remoteObjectSymbol];
}
export function markJSHandle(handle: js.JSHandle, remoteObject: Protocol.Runtime.RemoteObject) {
export function toHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
if (remoteObject.subtype === 'node' && context.frame()) {
const handle = new dom.ElementHandle(context);
(handle as any)[remoteObjectSymbol] = remoteObject;
return handle;
}
const handle = new js.JSHandle(context);
(handle as any)[remoteObjectSymbol] = remoteObject;
return handle;
}

View File

@ -16,14 +16,15 @@
*/
import { EventEmitter } from 'events';
import * as dom from '../dom';
import * as frames from '../frames';
import { assert, debugError } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import * as network from '../network';
import { TimeoutSettings } from '../TimeoutSettings';
import { CDPSession } from './Connection';
import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate, toRemoteObject } from './ExecutionContext';
import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext';
import { DOMWorldDelegate } from './JSHandle';
import { LifecycleWatcher } from './LifecycleWatcher';
import { NetworkManager } from './NetworkManager';
import { Page } from './Page';
@ -178,13 +179,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
throw error;
}
async adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise<dom.ElementHandle> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(elementHandle).objectId,
});
return (context._delegate as ExecutionContextDelegate).adoptBackendNodeId(context, nodeInfo.node.backendNodeId);
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
const frame = this._frames.get(event.frameId);
if (!frame)
@ -324,7 +318,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
const frame = this._frames.get(frameId) || null;
if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated')
this._isolatedWorlds.add(contextPayload.name);
const context: js.ExecutionContext = new js.ExecutionContext(new ExecutionContextDelegate(this._client, contextPayload), frame);
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._client, contextPayload));
if (frame)
context._domWorld = new dom.DOMWorld(context, new DOMWorldDelegate(this, frame));
if (frame) {
if (contextPayload.auxData && !!contextPayload.auxData['isDefault'])
frame._contextCreated('main', context);

View File

@ -16,7 +16,6 @@
*/
import { assert, debugError } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import * as input from '../input';
import * as types from '../types';
@ -24,29 +23,20 @@ import * as frames from '../frames';
import { CDPSession } from './Connection';
import { FrameManager } from './FrameManager';
import { Protocol } from './protocol';
import { ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
import { toRemoteObject, toHandle, ExecutionContextDelegate } from './ExecutionContext';
export function createJSHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager;
const page = frameManager.page();
const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._client, frameManager);
const handle = new dom.ElementHandle(context, page.keyboard, page.mouse, delegate);
markJSHandle(handle, remoteObject);
return handle;
}
const handle = new js.JSHandle(context);
markJSHandle(handle, remoteObject);
return handle;
}
class DOMWorldDelegate implements dom.DOMWorldDelegate {
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
readonly keyboard: input.Keyboard;
readonly mouse: input.Mouse;
readonly frame: frames.Frame;
private _client: CDPSession;
private _frameManager: FrameManager;
constructor(client: CDPSession, frameManager: FrameManager) {
this._client = client;
constructor(frameManager: FrameManager, frame: frames.Frame) {
this.keyboard = frameManager.page().keyboard;
this.mouse = frameManager.page().mouse;
this.frame = frame;
this._client = frameManager._client;
this._frameManager = frameManager;
}
@ -183,8 +173,8 @@ class DOMWorldDelegate implements dom.DOMWorldDelegate {
// Filter out quads that have too small area to click into.
const { clientWidth, clientHeight } = layoutMetrics.layoutViewport;
const quads = result.quads.map(fromProtocolQuad)
.map(quad => intersectQuadWithViewport(quad, clientWidth, clientHeight))
.filter(quad => computeQuadArea(quad) > 1);
.map(quad => intersectQuadWithViewport(quad, clientWidth, clientHeight))
.filter(quad => computeQuadArea(quad) > 1);
if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad.
@ -234,4 +224,19 @@ class DOMWorldDelegate implements dom.DOMWorldDelegate {
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise<dom.ElementHandle> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId,
});
return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to);
}
async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.DOMWorld): Promise<dom.ElementHandle> {
const {object} = await this._client.send('DOM.resolveNode', {
backendNodeId,
executionContextId: (to.context._delegate as ExecutionContextDelegate)._contextId,
});
return toHandle(to.context, object).asElement()!;
}
}

View File

@ -35,8 +35,7 @@ import { PDF } from './features/pdf';
import { Workers } from './features/workers';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { createJSHandle } from './JSHandle';
import { toRemoteObject } from './ExecutionContext';
import { toHandle, toRemoteObject } from './ExecutionContext';
import { NetworkManagerEvents } from './NetworkManager';
import { Protocol } from './protocol';
import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper';
@ -48,7 +47,7 @@ import * as dom from '../dom';
import * as frames from '../frames';
import * as js from '../javascript';
import * as network from '../network';
import { ExecutionContextDelegate } from './ExecutionContext';
import { DOMWorldDelegate } from './JSHandle';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -159,8 +158,8 @@ export class Page extends EventEmitter {
if (!this._fileChooserInterceptors.size)
return;
const frame = this._frameManager.frame(event.frameId);
const context = await frame._utilityContext();
const handle = await (context._delegate as ExecutionContextDelegate).adoptBackendNodeId(context, event.backendNodeId);
const utilityWorld = await frame._utilityDOMWorld();
const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld);
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
@ -321,7 +320,7 @@ export class Page extends EventEmitter {
return;
}
const context = this._frameManager.executionContextById(event.executionContextId);
const values = event.args.map(arg => createJSHandle(context, arg));
const values = event.args.map(arg => toHandle(context, arg));
this._addConsoleMessage(event.type, values, event.stackTrace);
}

View File

@ -21,8 +21,7 @@ import { Protocol } from '../protocol';
import { Events } from '../events';
import * as types from '../../types';
import * as js from '../../javascript';
import { ExecutionContextDelegate } from '../ExecutionContext';
import { createJSHandle } from '../JSHandle';
import { toHandle, ExecutionContextDelegate } from '../ExecutionContext';
type AddToConsoleCallback = (type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void;
type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void;
@ -68,8 +67,8 @@ export class Worker extends EventEmitter {
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
let jsHandleFactory: (o: Protocol.Runtime.RemoteObject) => js.JSHandle;
this._client.once('Runtime.executionContextCreated', async event => {
jsHandleFactory = remoteObject => createJSHandle(executionContext, remoteObject);
const executionContext = new js.ExecutionContext(new ExecutionContextDelegate(client, event.context), null);
jsHandleFactory = remoteObject => toHandle(executionContext, remoteObject);
const executionContext = new js.ExecutionContext(new ExecutionContextDelegate(client, event.context));
this._executionContextCallback(executionContext);
});
// This might fail if the target is closed before we recieve all execution contexts.

View File

@ -7,28 +7,73 @@ import Injected from './injected/injected';
import * as input from './input';
import * as js from './javascript';
import * as types from './types';
import * as injectedSource from './generated/injectedSource';
import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
type SelectorRoot = Element | ShadowRoot | Document;
export interface DOMWorldDelegate {
keyboard: input.Keyboard;
mouse: input.Mouse;
frame: frames.Frame;
isJavascriptEnabled(): boolean;
contentFrame(handle: ElementHandle): Promise<frames.Frame | null>;
boundingBox(handle: ElementHandle): Promise<types.Rect | null>;
screenshot(handle: ElementHandle, options?: any): Promise<string | Buffer>;
ensurePointerActionPoint(handle: ElementHandle, relativePoint?: types.Point): Promise<types.Point>;
setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise<void>;
adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise<ElementHandle>;
}
export class DOMWorld {
readonly context: js.ExecutionContext;
readonly delegate: DOMWorldDelegate;
private _injectedPromise?: Promise<js.JSHandle>;
private _documentPromise?: Promise<ElementHandle>;
constructor(context: js.ExecutionContext, delegate: DOMWorldDelegate) {
this.context = context;
this.delegate = delegate;
}
injected(): Promise<js.JSHandle> {
if (!this._injectedPromise) {
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source];
const source = `
new (${injectedSource.source})([
${engineSources.join(',\n')}
])
`;
this._injectedPromise = this.context.evaluateHandle(source);
}
return this._injectedPromise;
}
_document(): Promise<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.context.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
async adoptElementHandle(handle: ElementHandle, dispose: boolean): Promise<ElementHandle> {
if (handle.executionContext() === this.context)
return handle;
const adopted = this.delegate.adoptElementHandle(handle, this);
if (dispose)
await handle.dispose();
return adopted;
}
}
export class ElementHandle extends js.JSHandle {
private _delegate: DOMWorldDelegate;
private _keyboard: input.Keyboard;
private _mouse: input.Mouse;
private readonly _world: DOMWorld;
constructor(context: js.ExecutionContext, keyboard: input.Keyboard, mouse: input.Mouse, delegate: DOMWorldDelegate) {
constructor(context: js.ExecutionContext) {
super(context);
this._delegate = delegate;
this._keyboard = keyboard;
this._mouse = mouse;
assert(context._domWorld, 'Element handle should have a dom world');
this._world = context._domWorld;
}
asElement(): ElementHandle | null {
@ -36,7 +81,7 @@ export class ElementHandle extends js.JSHandle {
}
async contentFrame(): Promise<frames.Frame | null> {
return this._delegate.contentFrame(this);
return this._world.delegate.contentFrame(this);
}
async _scrollIntoViewIfNeeded() {
@ -63,35 +108,35 @@ export class ElementHandle extends js.JSHandle {
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}, this._delegate.isJavascriptEnabled());
}, this._world.delegate.isJavascriptEnabled());
if (error)
throw new Error(error);
}
async _performPointerAction(action: (point: types.Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> {
const point = await this._delegate.ensurePointerActionPoint(this, options ? options.relativePoint : undefined);
const point = await this._world.delegate.ensurePointerActionPoint(this, options ? options.relativePoint : undefined);
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._keyboard._ensureModifiers(options.modifiers);
restoreModifiers = await this._world.delegate.keyboard._ensureModifiers(options.modifiers);
await action(point);
if (restoreModifiers)
await this._keyboard._ensureModifiers(restoreModifiers);
await this._world.delegate.keyboard._ensureModifiers(restoreModifiers);
}
hover(options?: input.PointerActionOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.move(point.x, point.y), options);
return this._performPointerAction(point => this._world.delegate.mouse.move(point.x, point.y), options);
}
click(options?: input.ClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.click(point.x, point.y, options), options);
return this._performPointerAction(point => this._world.delegate.mouse.click(point.x, point.y, options), options);
}
dblclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.dblclick(point.x, point.y, options), options);
return this._performPointerAction(point => this._world.delegate.mouse.dblclick(point.x, point.y, options), options);
}
tripleclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.tripleclick(point.x, point.y, options), options);
return this._performPointerAction(point => this._world.delegate.mouse.tripleclick(point.x, point.y, options), options);
}
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
@ -115,13 +160,13 @@ export class ElementHandle extends js.JSHandle {
if (error)
throw new Error(error);
await this.focus();
await this._keyboard.sendCharacters(value);
await this._world.delegate.keyboard.sendCharacters(value);
}
async setInputFiles(...files: (string|input.FilePayload)[]) {
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
await this._delegate.setInputFiles(this, await input.loadFiles(files));
await this._world.delegate.setInputFiles(this, await input.loadFiles(files));
}
async focus() {
@ -130,26 +175,26 @@ export class ElementHandle extends js.JSHandle {
async type(text: string, options: { delay: (number | undefined); } | undefined) {
await this.focus();
await this._keyboard.type(text, options);
await this._world.delegate.keyboard.type(text, options);
}
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
await this.focus();
await this._keyboard.press(key, options);
await this._world.delegate.keyboard.press(key, options);
}
async boundingBox(): Promise<types.Rect | null> {
return this._delegate.boundingBox(this);
return this._world.delegate.boundingBox(this);
}
async screenshot(options: any = {}): Promise<string | Buffer> {
return this._delegate.screenshot(this, options);
return this._world.delegate.screenshot(this, options);
}
async $(selector: string): Promise<ElementHandle | null> {
const handle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
selector, await this._context._injected()
selector, await this._world.injected()
);
const element = handle.asElement();
if (element)
@ -161,7 +206,7 @@ export class ElementHandle extends js.JSHandle {
async $$(selector: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
selector, await this._context._injected()
selector, await this._world.injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
@ -186,7 +231,7 @@ export class ElementHandle extends js.JSHandle {
$$eval: types.$$Eval = async (selector, pageFunction, ...args) => {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
selector, await this._context._injected()
selector, await this._world.injected()
);
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
@ -197,7 +242,7 @@ export class ElementHandle extends js.JSHandle {
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
expression, await this._context._injected()
expression, await this._world.injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();

View File

@ -16,8 +16,8 @@
*/
import {helper, debugError} from '../helper';
import { createHandle } from './JSHandle';
import * as js from '../javascript';
import * as dom from '../dom';
import { JugglerSession } from './Connection';
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
@ -48,7 +48,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
expression: pageFunction.trim(),
executionContextId: this._executionContextId,
}).catch(rewriteError);
return createHandle(context, payload.result, payload.exceptionDetails);
return toHandle(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.`);
@ -101,7 +101,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
throw err;
}
const payload = await callFunctionPromise.catch(rewriteError);
return createHandle(context, payload.result, payload.exceptionDetails);
return toHandle(context, payload.result, payload.exceptionDetails);
function rewriteError(error) {
if (error.message.includes('Failed to find execution context with id'))
@ -117,7 +117,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
});
const result = new Map();
for (const property of response.properties)
result.set(property.name, createHandle(handle.executionContext(), property.value, null));
result.set(property.name, toHandle(handle.executionContext(), property.value, null));
return result;
}
@ -163,8 +163,21 @@ export function toPayload(handle: js.JSHandle): any {
return (handle as any)[payloadSymbol];
}
export function markJSHandle(handle: js.JSHandle, payload: any) {
(handle as any)[payloadSymbol] = payload;
export function toHandle(context: js.ExecutionContext, result: any, exceptionDetails?: any) {
if (exceptionDetails) {
if (exceptionDetails.value)
throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value));
else
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack);
}
if (result.subtype === 'node') {
const handle = new dom.ElementHandle(context);
(handle as any)[payloadSymbol] = result;
return handle;
}
const handle = new js.JSHandle(context);
(handle as any)[payloadSymbol] = result;
return handle;
}
export function deserializeValue({unserializableValue, value}) {

View File

@ -27,6 +27,7 @@ import { ExecutionContextDelegate } from './ExecutionContext';
import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog';
import { Page } from './Page';
import { NetworkManager } from './NetworkManager';
import { DOMWorldDelegate } from './JSHandle';
export const FrameManagerEvents = {
FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'),
@ -80,8 +81,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
_onExecutionContextCreated({executionContextId, auxData}) {
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._session, executionContextId), frame);
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._session, executionContextId));
if (frame) {
context._domWorld = new dom.DOMWorld(context, new DOMWorldDelegate(this, frame));
frame._contextCreated('main', context);
frame._contextCreated('utility', context);
}
@ -175,11 +177,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
helper.removeEventListeners(this._eventListeners);
}
async adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise<dom.ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return elementHandle;
}
async waitForFrameNavigation(frame: frames.Frame, options: { timeout?: number; waitUntil?: string | Array<string>; } = {}) {
const {
timeout = this._timeoutSettings.navigationTimeout(),

View File

@ -16,24 +16,29 @@
*/
import { assert, debugError } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import * as input from '../input';
import * as types from '../types';
import * as frames from '../frames';
import { JugglerSession } from './Connection';
import { FrameManager } from './FrameManager';
import { markJSHandle, ExecutionContextDelegate, toPayload } from './ExecutionContext';
import { toPayload } from './ExecutionContext';
class DOMWorldDelegate implements dom.DOMWorldDelegate {
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
readonly keyboard: input.Keyboard;
readonly mouse: input.Mouse;
readonly frame: frames.Frame;
private _session: JugglerSession;
private _frameManager: FrameManager;
private _frameId: string;
constructor(session: JugglerSession, frameManager: FrameManager, frameId: string) {
this._session = session;
constructor(frameManager: FrameManager, frame: frames.Frame) {
this.keyboard = frameManager._page.keyboard;
this.mouse = frameManager._page.mouse;
this.frame = frame;
this._session = frameManager._session;
this._frameManager = frameManager;
this._frameId = frameId;
this._frameId = frameManager._frameData(frame).frameId;
}
async contentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
@ -129,26 +134,9 @@ class DOMWorldDelegate implements dom.DOMWorldDelegate {
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
await handle.evaluate(input.setFileInputFunction, files);
}
}
export function createHandle(context: js.ExecutionContext, result: any, exceptionDetails?: any) {
if (exceptionDetails) {
if (exceptionDetails.value)
throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value));
else
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack);
}
if (result.subtype === 'node') {
const frame = context.frame();
const frameManager = frame._delegate as FrameManager;
const frameId = frameManager._frameData(frame).frameId;
const session = (context._delegate as ExecutionContextDelegate)._session;
const delegate = new DOMWorldDelegate(session, frameManager, frameId);
const handle = new dom.ElementHandle(context, frameManager._page.keyboard, frameManager._page.mouse, delegate);
markJSHandle(handle, result);
async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise<dom.ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return handle;
}
const handle = new js.JSHandle(context);
markJSHandle(handle, result);
return handle;
}

View File

@ -29,7 +29,6 @@ import { Accessibility } from './features/accessibility';
import { Interception } from './features/interception';
import { FrameManager, FrameManagerEvents, normalizeWaitUntil } from './FrameManager';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { createHandle } from './JSHandle';
import { NavigationWatchdog } from './NavigationWatchdog';
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
import * as input from '../input';
@ -38,7 +37,7 @@ import * as dom from '../dom';
import * as js from '../javascript';
import * as network from '../network';
import * as frames from '../frames';
import { toPayload, deserializeValue } from './ExecutionContext';
import { toHandle, toPayload, deserializeValue } from './ExecutionContext';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -547,7 +546,7 @@ export class Page extends EventEmitter {
_onConsole({type, args, executionContextId, location}) {
const context = this._frameManager.executionContextById(executionContextId);
this.emit(Events.Page.Console, new ConsoleMessage(type, args.map(arg => createHandle(context, arg)), location));
this.emit(Events.Page.Console, new ConsoleMessage(type, args.map(arg => toHandle(context, arg)), location));
}
isClosed(): boolean {
@ -571,7 +570,7 @@ export class Page extends EventEmitter {
if (!this._fileChooserInterceptors.size)
return;
const context = this._frameManager.executionContextById(executionContextId);
const handle = createHandle(context, element) as dom.ElementHandle;
const handle = toHandle(context, element) as dom.ElementHandle;
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);

View File

@ -48,7 +48,6 @@ export interface FrameDelegate {
navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise<network.Response | null>;
waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise<network.Response | null>;
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise<dom.ElementHandle>;
}
export class Frame {
@ -89,12 +88,26 @@ export class Frame {
return this._worlds.get('main').contextPromise;
}
async _mainDOMWorld(): Promise<dom.DOMWorld> {
const context = await this._mainContext();
if (!context._domWorld)
throw new Error(`Execution Context does not belong to frame`);
return context._domWorld;
}
_utilityContext(): Promise<js.ExecutionContext> {
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;
}
async _utilityDOMWorld(): Promise<dom.DOMWorld> {
const context = await this._utilityContext();
if (!context._domWorld)
throw new Error(`Execution Context does not belong to frame`);
return context._domWorld;
}
executionContext(): Promise<js.ExecutionContext> {
return this._mainContext();
}
@ -110,32 +123,32 @@ export class Frame {
}
async $(selector: string): Promise<dom.ElementHandle | null> {
const context = await this._mainContext();
const document = await context._document();
const domWorld = await this._mainDOMWorld();
const document = await domWorld._document();
return document.$(selector);
}
async $x(expression: string): Promise<dom.ElementHandle[]> {
const context = await this._mainContext();
const document = await context._document();
const domWorld = await this._mainDOMWorld();
const document = await domWorld._document();
return document.$x(expression);
}
$eval: types.$Eval = async (selector, pageFunction, ...args) => {
const context = await this._mainContext();
const document = await context._document();
const domWorld = await this._mainDOMWorld();
const document = await domWorld._document();
return document.$eval(selector, pageFunction, ...args as any);
}
$$eval: types.$$Eval = async (selector, pageFunction, ...args) => {
const context = await this._mainContext();
const document = await context._document();
const domWorld = await this._mainDOMWorld();
const document = await domWorld._document();
return document.$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string): Promise<dom.ElementHandle[]> {
const context = await this._mainContext();
const document = await context._document();
const domWorld = await this._mainDOMWorld();
const document = await domWorld._document();
return document.$$(selector);
}
@ -293,8 +306,8 @@ export class Frame {
}
async click(selector: string, options?: ClickOptions) {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.click(options);
@ -302,8 +315,8 @@ export class Frame {
}
async dblclick(selector: string, options?: MultiClickOptions) {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.dblclick(options);
@ -311,8 +324,8 @@ export class Frame {
}
async tripleclick(selector: string, options?: MultiClickOptions) {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.tripleclick(options);
@ -320,8 +333,8 @@ export class Frame {
}
async fill(selector: string, value: string) {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.fill(value);
@ -329,8 +342,8 @@ export class Frame {
}
async focus(selector: string) {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.focus();
@ -338,8 +351,8 @@ export class Frame {
}
async hover(selector: string, options?: PointerActionOptions) {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.hover(options);
@ -347,14 +360,13 @@ export class Frame {
}
async select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const utilityContext = await this._utilityContext();
const adoptedValues = await Promise.all(values.map(async value => {
if (value instanceof dom.ElementHandle)
return this._adoptElementHandle(value, utilityContext, false /* dispose */);
return domWorld.adoptElementHandle(value, false /* dispose */);
return value;
}));
const result = await handle.select(...adoptedValues);
@ -363,8 +375,8 @@ export class Frame {
}
async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
const context = await this._utilityContext();
const document = await context._document();
const domWorld = await this._utilityDOMWorld();
const document = await domWorld._document();
const handle = await document.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.type(text, options);
@ -397,8 +409,8 @@ export class Frame {
await handle.dispose();
return null;
}
const mainContext = await this._mainContext();
return this._adoptElementHandle(handle.asElement(), mainContext, true /* dispose */);
const mainDOMWorld = await this._mainDOMWorld();
return mainDOMWorld.adoptElementHandle(handle.asElement(), true /* dispose */);
}
async waitForXPath(xpath: string, options: {
@ -411,8 +423,8 @@ export class Frame {
await handle.dispose();
return null;
}
const mainContext = await this._mainContext();
return this._adoptElementHandle(handle.asElement(), mainContext, true /* dispose */);
const mainDOMWorld = await this._mainDOMWorld();
return mainDOMWorld.adoptElementHandle(handle.asElement(), true /* dispose */);
}
waitForFunction(
@ -491,13 +503,4 @@ export class Frame {
this._setContext(worldType, null);
}
}
private async _adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise<dom.ElementHandle> {
if (elementHandle.executionContext() === context)
return elementHandle;
const handle = this._delegate.adoptElementHandle(elementHandle, context);
if (dispose)
await elementHandle.dispose();
return handle;
}
}

View File

@ -4,9 +4,6 @@
import * as frames from './frames';
import * as types from './types';
import * as dom from './dom';
import * as injectedSource from './generated/injectedSource';
import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
export interface ExecutionContextDelegate {
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
@ -17,18 +14,15 @@ export interface ExecutionContextDelegate {
}
export class ExecutionContext {
_delegate: ExecutionContextDelegate;
private _frame: frames.Frame;
private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<dom.ElementHandle> | null = null;
readonly _delegate: ExecutionContextDelegate;
_domWorld?: dom.DOMWorld;
constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) {
constructor(delegate: ExecutionContextDelegate) {
this._delegate = delegate;
this._frame = frame;
}
frame(): frames.Frame | null {
return this._frame;
return this._domWorld ? this._domWorld.delegate.frame : null;
}
evaluate: types.Evaluate = (pageFunction, ...args) => {
@ -38,29 +32,10 @@ export class ExecutionContext {
evaluateHandle: types.EvaluateHandle = (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<dom.ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
}
export class JSHandle {
_context: ExecutionContext;
readonly _context: ExecutionContext;
_disposed = false;
constructor(context: ExecutionContext) {

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as types from './types';
import * as frames from './frames';
export type NetworkCookie = {

View File

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

View File

@ -29,6 +29,7 @@ import { ExecutionContextDelegate } from './ExecutionContext';
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
import { Page } from './Page';
import { Protocol } from './protocol';
import { DOMWorldDelegate } from './JSHandle';
export const FrameManagerEvents = {
FrameNavigatedWithinDocument: Symbol('FrameNavigatedWithinDocument'),
@ -235,8 +236,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
const frame = this._frames.get(frameId) || null;
if (!frame)
return;
const context: js.ExecutionContext = new js.ExecutionContext(new ExecutionContextDelegate(this._session, contextPayload), frame);
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._session, contextPayload));
if (frame) {
context._domWorld = new dom.DOMWorld(context, new DOMWorldDelegate(this, frame));
frame._contextCreated('main', context);
frame._contextCreated('utility', context);
}
@ -272,11 +274,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
return watchDog.waitForNavigation();
}
async adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise<dom.ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return elementHandle;
}
async setFrameContent(frame: frames.Frame, html: string, options: { timeout?: number; waitUntil?: string | Array<string>; } | undefined = {}) {
// We rely upon the fact that document.open() will trigger Page.loadEventFired.
const watchDog = new NextNavigationWatchdog(this, frame, 1000);

View File

@ -16,39 +16,29 @@
*/
import * as fs from 'fs';
import { debugError, helper } from '../helper';
import { debugError, helper, assert } from '../helper';
import * as input from '../input';
import * as dom from '../dom';
import * as frames from '../frames';
import * as types from '../types';
import { TargetSession } from './Connection';
import { ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
import { toRemoteObject } from './ExecutionContext';
import { FrameManager } from './FrameManager';
import { Protocol } from './protocol';
import * as js from '../javascript';
const writeFileAsync = helper.promisify(fs.writeFile);
export function createJSHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager;
const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._session, frameManager);
const handle = new dom.ElementHandle(context, frameManager.page().keyboard, frameManager.page().mouse, delegate);
markJSHandle(handle, remoteObject);
return handle;
}
const handle = new js.JSHandle(context);
markJSHandle(handle, remoteObject);
return handle;
}
class DOMWorldDelegate implements dom.DOMWorldDelegate {
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
readonly keyboard: input.Keyboard;
readonly mouse: input.Mouse;
readonly frame: frames.Frame;
private _client: TargetSession;
private _frameManager: FrameManager;
constructor(client: TargetSession, frameManager: FrameManager) {
this._client = client;
constructor(frameManager: FrameManager, frame: frames.Frame) {
this.keyboard = frameManager.page().keyboard;
this.mouse = frameManager.page().mouse;
this.frame = frame;
this._client = frameManager._session;
this._frameManager = frameManager;
}
@ -124,8 +114,8 @@ class DOMWorldDelegate implements dom.DOMWorldDelegate {
// Filter out quads that have too small area to click into.
const {clientWidth, clientHeight} = viewport;
const quads = result.quads.map(fromProtocolQuad)
.map(quad => intersectQuadWithViewport(quad, clientWidth, clientHeight))
.filter(quad => computeQuadArea(quad) > 1);
.map(quad => intersectQuadWithViewport(quad, clientWidth, clientHeight))
.filter(quad => computeQuadArea(quad) > 1);
if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad.
@ -146,4 +136,9 @@ class DOMWorldDelegate implements dom.DOMWorldDelegate {
const objectId = toRemoteObject(handle).objectId;
await this._client.send('DOM.setInputFiles', { objectId, files });
}
async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise<dom.ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return handle;
}
}

View File

@ -26,8 +26,7 @@ import { TargetSession, TargetSessionEvents } from './Connection';
import { Events } from './events';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawKeyboardImpl, RawMouseImpl } from './Input';
import { createJSHandle } from './JSHandle';
import { toRemoteObject } from './ExecutionContext';
import { toHandle, toRemoteObject } from './ExecutionContext';
import { NetworkManagerEvents } from './NetworkManager';
import { Protocol } from './protocol';
import { valueFromRemoteObject } from './protocolHelper';
@ -183,7 +182,7 @@ export class Page extends EventEmitter {
} else {
context = mainFrameContext;
}
return createJSHandle(context, p);
return toHandle(context, p);
});
const textTokens = [];
for (const handle of handles) {
@ -462,7 +461,7 @@ export class Page extends EventEmitter {
if (!this._fileChooserInterceptors.size)
return;
const context = await this._frameManager.frame(event.frameId)._utilityContext();
const handle = createJSHandle(context, event.element) as dom.ElementHandle;
const handle = toHandle(context, event.element) as dom.ElementHandle;
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);

View File

@ -104,12 +104,14 @@ function checkSources(sources) {
function parentClass(classNode) {
for (const herigateClause of classNode.heritageClauses || []) {
for (const heritageType of herigateClause.types) {
const parentClassName = heritageType.expression.escapedText;
return parentClassName;
let expression = heritageType.expression;
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression)
expression = expression.name;
if (classNode.name.escapedText !== expression.escapedText)
return expression.escapedText;
}
}
return null;
}
function serializeSymbol(symbol, circular = []) {