diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index b719a15b92..683d0ca56c 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -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> { 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; } diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 208f94de60..f131c50683 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -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 { - 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); diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index 1c27dcb3de..18212c70ef 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -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 { await handle.evaluate(input.setFileInputFunction, files); } + + async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise { + 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 { + const {object} = await this._client.send('DOM.resolveNode', { + backendNodeId, + executionContextId: (to.context._delegate as ExecutionContextDelegate)._contextId, + }); + return toHandle(to.context, object).asElement()!; + } } diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index 7ce6e52e88..3dd1545e7c 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -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); } diff --git a/src/chromium/features/workers.ts b/src/chromium/features/workers.ts index 93409b3a78..ecdf1ba046 100644 --- a/src/chromium/features/workers.ts +++ b/src/chromium/features/workers.ts @@ -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. diff --git a/src/dom.ts b/src/dom.ts index 9a67ef218c..a140bd8e77 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -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; boundingBox(handle: ElementHandle): Promise; screenshot(handle: ElementHandle, options?: any): Promise; ensurePointerActionPoint(handle: ElementHandle, relativePoint?: types.Point): Promise; setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise; + adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise; +} + +export class DOMWorld { + readonly context: js.ExecutionContext; + readonly delegate: DOMWorldDelegate; + + private _injectedPromise?: Promise; + private _documentPromise?: Promise; + + constructor(context: js.ExecutionContext, delegate: DOMWorldDelegate) { + this.context = context; + this.delegate = delegate; + } + + injected(): Promise { + 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 { + if (!this._documentPromise) + this._documentPromise = this.context.evaluateHandle('document').then(handle => handle.asElement()!); + return this._documentPromise; + } + + async adoptElementHandle(handle: ElementHandle, dispose: boolean): Promise { + 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 { - 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, options?: input.PointerActionOptions): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { @@ -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 { - return this._delegate.boundingBox(this); + return this._world.delegate.boundingBox(this); } async screenshot(options: any = {}): Promise { - return this._delegate.screenshot(this, options); + return this._world.delegate.screenshot(this, options); } async $(selector: string): Promise { 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 { 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 { 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(); diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index f67e6d32db..0dff1c6497 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -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}) { diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 51ac44cfc3..142a1b8727 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -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 { - assert(false, 'Multiple isolated worlds are not implemented'); - return elementHandle; - } - async waitForFrameNavigation(frame: frames.Frame, options: { timeout?: number; waitUntil?: string | Array; } = {}) { const { timeout = this._timeoutSettings.navigationTimeout(), diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index c0abb32fd4..7fc9adc47f 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -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 { @@ -129,26 +134,9 @@ class DOMWorldDelegate implements dom.DOMWorldDelegate { async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise { 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 { + assert(false, 'Multiple isolated worlds are not implemented'); return handle; } - const handle = new js.JSHandle(context); - markJSHandle(handle, result); - return handle; } diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index 2aa3de2720..3a77f4555a 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -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); diff --git a/src/frames.ts b/src/frames.ts index fdeb4092fb..eff794c9db 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -48,7 +48,6 @@ export interface FrameDelegate { navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise; waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise; setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; - adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise; } export class Frame { @@ -89,12 +88,26 @@ export class Frame { return this._worlds.get('main').contextPromise; } + async _mainDOMWorld(): Promise { + const context = await this._mainContext(); + if (!context._domWorld) + throw new Error(`Execution Context does not belong to frame`); + return context._domWorld; + } + _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; } + async _utilityDOMWorld(): Promise { + const context = await this._utilityContext(); + if (!context._domWorld) + throw new Error(`Execution Context does not belong to frame`); + return context._domWorld; + } + executionContext(): Promise { return this._mainContext(); } @@ -110,32 +123,32 @@ export class Frame { } async $(selector: string): Promise { - 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 { - 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 { - 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 { - 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 { - if (elementHandle.executionContext() === context) - return elementHandle; - const handle = this._delegate.adoptElementHandle(elementHandle, context); - if (dispose) - await elementHandle.dispose(); - return handle; - } } diff --git a/src/javascript.ts b/src/javascript.ts index 777040f1fa..edfe2ef493 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -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; @@ -17,18 +14,15 @@ export interface ExecutionContextDelegate { } export class ExecutionContext { - _delegate: ExecutionContextDelegate; - private _frame: frames.Frame; - private _injectedPromise: Promise | null = null; - private _documentPromise: Promise | 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 { - 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; - } } export class JSHandle { - _context: ExecutionContext; + readonly _context: ExecutionContext; _disposed = false; constructor(context: ExecutionContext) { diff --git a/src/network.ts b/src/network.ts index cb24994ecc..52ce335654 100644 --- a/src/network.ts +++ b/src/network.ts @@ -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 = { diff --git a/src/webkit/ExecutionContext.ts b/src/webkit/ExecutionContext.ts index 11ca2e0490..9ad3c53896 100644 --- a/src/webkit/ExecutionContext.ts +++ b/src/webkit/ExecutionContext.ts @@ -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; } diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 5e2f5aac2a..09425e5b1d 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -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 { - assert(false, 'Multiple isolated worlds are not implemented'); - return elementHandle; - } - async setFrameContent(frame: frames.Frame, html: string, options: { timeout?: number; waitUntil?: string | Array; } | undefined = {}) { // We rely upon the fact that document.open() will trigger Page.loadEventFired. const watchDog = new NextNavigationWatchdog(this, frame, 1000); diff --git a/src/webkit/JSHandle.ts b/src/webkit/JSHandle.ts index b25904dbc3..1b54585753 100644 --- a/src/webkit/JSHandle.ts +++ b/src/webkit/JSHandle.ts @@ -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 { + assert(false, 'Multiple isolated worlds are not implemented'); + return handle; + } } diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index 407e3b3406..ffe9c2ff90 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -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); diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index 5f812e6db0..860c77d30f 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -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 = []) {