From 97c50c22abaa7fff042e08cee268050703ef3674 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 12 Dec 2019 21:11:52 -0800 Subject: [PATCH] chore: inherit FrameExecutionContext from ExecutionContext (#234) --- src/chromium/FrameManager.ts | 23 ++--- src/dom.ts | 134 ++++++++++++++-------------- src/firefox/FrameManager.ts | 16 ++-- src/frames.ts | 167 ++++++++++++++++------------------- src/javascript.ts | 5 +- src/page.ts | 8 +- src/webkit/FrameManager.ts | 19 ++-- 7 files changed, 183 insertions(+), 189 deletions(-) diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 2f2d8540b8..b0adbaa4a9 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -350,16 +350,17 @@ export class FrameManager extends EventEmitter implements PageDelegate { const frame = this._frames.get(frameId) || null; if (contextPayload.auxData && contextPayload.auxData.type === 'isolated') this._isolatedWorlds.add(contextPayload.name); - const context = new js.ExecutionContext(new ExecutionContextDelegate(this._client, contextPayload)); - if (frame) - context._domWorld = new dom.DOMWorld(context, frame); + const delegate = new ExecutionContextDelegate(this._client, contextPayload); if (frame) { + const context = new dom.FrameExecutionContext(delegate, frame); if (contextPayload.auxData && !!contextPayload.auxData.isDefault) frame._contextCreated('main', context); else if (contextPayload.name === UTILITY_WORLD_NAME) frame._contextCreated('utility', context); + this._contextIdToContext.set(contextPayload.id, context); + } else { + this._contextIdToContext.set(contextPayload.id, new js.ExecutionContext(delegate)); } - this._contextIdToContext.set(contextPayload.id, context); } _onExecutionContextDestroyed(executionContextId: number) { @@ -368,7 +369,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { return; this._contextIdToContext.delete(executionContextId); if (context.frame()) - context.frame()._contextDestroyed(context); + context.frame()._contextDestroyed(context as dom.FrameExecutionContext); } _onExecutionContextsCleared() { @@ -452,8 +453,8 @@ export class FrameManager extends EventEmitter implements PageDelegate { async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) { const frame = this.frame(event.frameId); - const utilityWorld = await frame._utilityDOMWorld(); - const handle = await this.adoptBackendNodeId(event.backendNodeId, utilityWorld); + const utilityContext = await frame._utilityContext(); + const handle = await this.adoptBackendNodeId(event.backendNodeId, utilityContext); this._page._onFileChooserOpened(handle); } @@ -617,21 +618,21 @@ export class FrameManager extends EventEmitter implements PageDelegate { await handle.evaluate(input.setFileInputFunction, files); } - async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise> { + async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { const nodeInfo = await this._client.send('DOM.describeNode', { objectId: toRemoteObject(handle).objectId, }); return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise>; } - async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.DOMWorld): Promise { + async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise { const result = await this._client.send('DOM.resolveNode', { backendNodeId, - executionContextId: (to.context._delegate as ExecutionContextDelegate)._contextId, + executionContextId: (to._delegate as ExecutionContextDelegate)._contextId, }).catch(debugError); if (!result) throw new Error('Unable to adopt element handle from a different document'); - return to.context._createHandle(result.object).asElement()!; + return to._createHandle(result.object).asElement()!; } } diff --git a/src/dom.ts b/src/dom.ts index d81585ba73..53951fe832 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -11,28 +11,32 @@ import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSourc import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource'; import { assert, helper, debugError } from './helper'; import Injected from './injected/injected'; +import { Page } from './page'; type ScopedSelector = types.Selector & { scope?: ElementHandle }; type ResolvedSelector = { scope?: ElementHandle, selector: string, visible?: boolean, disposeScope?: boolean }; -export class DOMWorld { - readonly context: js.ExecutionContext; - readonly frame: frames.Frame; +export class FrameExecutionContext extends js.ExecutionContext { + private readonly _frame: frames.Frame; private _injectedPromise?: Promise; - constructor(context: js.ExecutionContext, frame: frames.Frame) { - this.context = context; - this.frame = frame; + constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) { + super(delegate); + this._frame = frame; } - createHandle(remoteObject: any): ElementHandle | null { - if (this.frame._page._delegate.isElementHandle(remoteObject)) - return new ElementHandle(this.context, remoteObject); - return null; + frame(): frames.Frame | null { + return this._frame; } - injected(): Promise { + _createHandle(remoteObject: any): js.JSHandle | null { + if (this._frame._page._delegate.isElementHandle(remoteObject)) + return new ElementHandle(this, remoteObject); + return super._createHandle(remoteObject); + } + + _injected(): Promise { if (!this._injectedPromise) { const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source, zsSelectorEngineSource.source]; const source = ` @@ -40,36 +44,36 @@ export class DOMWorld { ${engineSources.join(',\n')} ]) `; - this._injectedPromise = this.context.evaluateHandle(source); + this._injectedPromise = this.evaluateHandle(source); } return this._injectedPromise; } - async adoptElementHandle(handle: ElementHandle): Promise> { - assert(handle.executionContext() !== this.context, 'Should not adopt to the same context'); - return this.frame._page._delegate.adoptElementHandle(handle, this); + async _adoptElementHandle(handle: ElementHandle): Promise> { + assert(handle.executionContext() !== this, 'Should not adopt to the same context'); + return this._frame._page._delegate.adoptElementHandle(handle, this); } - async resolveSelector(selector: string | ScopedSelector): Promise { + async _resolveSelector(selector: string | ScopedSelector): Promise { if (helper.isString(selector)) return { selector: normalizeSelector(selector) }; - if (selector.scope && selector.scope.executionContext() !== this.context) { - const scope = await this.adoptElementHandle(selector.scope); + if (selector.scope && selector.scope.executionContext() !== this) { + const scope = await this._adoptElementHandle(selector.scope); return { scope, selector: normalizeSelector(selector.selector), disposeScope: true, visible: selector.visible }; } return { scope: selector.scope, selector: normalizeSelector(selector.selector), visible: selector.visible }; } - async $(selector: string | ScopedSelector): Promise | null> { - const resolved = await this.resolveSelector(selector); - const handle = await this.context.evaluateHandle( + async _$(selector: string | ScopedSelector): Promise | null> { + const resolved = await this._resolveSelector(selector); + const handle = await this.evaluateHandle( (injected: Injected, selector: string, scope?: Node, visible?: boolean) => { const element = injected.querySelector(selector, scope || document); if (visible === undefined || !element) return element; return injected.isVisible(element) === visible ? element : undefined; }, - await this.injected(), resolved.selector, resolved.scope, resolved.visible + await this._injected(), resolved.selector, resolved.scope, resolved.visible ); if (resolved.disposeScope) await resolved.scope.dispose(); @@ -78,16 +82,16 @@ export class DOMWorld { return handle.asElement(); } - async $$(selector: string | ScopedSelector): Promise[]> { - const resolved = await this.resolveSelector(selector); - const arrayHandle = await this.context.evaluateHandle( + async _$$(selector: string | ScopedSelector): Promise[]> { + const resolved = await this._resolveSelector(selector); + const arrayHandle = await this.evaluateHandle( (injected: Injected, selector: string, scope?: Node, visible?: boolean) => { const elements = injected.querySelectorAll(selector, scope || document); if (visible !== undefined) return elements.filter(element => injected.isVisible(element) === visible); return elements; }, - await this.injected(), resolved.selector, resolved.scope, resolved.visible + await this._injected(), resolved.selector, resolved.scope, resolved.visible ); if (resolved.disposeScope) await resolved.scope.dispose(); @@ -104,8 +108,8 @@ export class DOMWorld { return result; } - $eval: types.$Eval = async (selector, pageFunction, ...args) => { - const elementHandle = await this.$(selector); + _$eval: types.$Eval = async (selector, pageFunction, ...args) => { + const elementHandle = await this._$(selector); if (!elementHandle) throw new Error(`Error: failed to find element matching selector "${types.selectorToString(selector)}"`); const result = await elementHandle.evaluate(pageFunction, ...args as any); @@ -113,16 +117,16 @@ export class DOMWorld { return result; } - $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { - const resolved = await this.resolveSelector(selector); - const arrayHandle = await this.context.evaluateHandle( + _$$eval: types.$$Eval = async (selector, pageFunction, ...args) => { + const resolved = await this._resolveSelector(selector); + const arrayHandle = await this.evaluateHandle( (injected: Injected, selector: string, scope?: Node, visible?: boolean) => { const elements = injected.querySelectorAll(selector, scope || document); if (visible !== undefined) return elements.filter(element => injected.isVisible(element) === visible); return elements; }, - await this.injected(), resolved.selector, resolved.scope, resolved.visible + await this._injected(), resolved.selector, resolved.scope, resolved.visible ); const result = await arrayHandle.evaluate(pageFunction, ...args as any); await arrayHandle.dispose(); @@ -131,12 +135,12 @@ export class DOMWorld { } export class ElementHandle extends js.JSHandle { - readonly _world: DOMWorld; + readonly _context: FrameExecutionContext; + readonly _page: Page; - constructor(context: js.ExecutionContext, remoteObject: any) { + constructor(context: FrameExecutionContext, remoteObject: any) { super(context, remoteObject); - assert(context._domWorld, 'Element handle should have a dom world'); - this._world = context._domWorld; + this._page = context.frame()._page; } asElement(): ElementHandle | null { @@ -144,7 +148,7 @@ export class ElementHandle extends js.JSHandle { } async contentFrame(): Promise { - return this._world.frame._page._delegate.getContentFrame(this); + return this._page._delegate.getContentFrame(this); } async _scrollIntoViewIfNeeded() { @@ -175,7 +179,7 @@ export class ElementHandle extends js.JSHandle { element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); } return false; - }, !!this._world.frame._page._state.javascriptEnabled); + }, !!this._page._state.javascriptEnabled); if (error) throw new Error(error); } @@ -222,8 +226,8 @@ export class ElementHandle extends js.JSHandle { }; const [quads, metrics] = await Promise.all([ - this._world.frame._page._delegate.getContentQuads(this), - this._world.frame._page._delegate.layoutViewport(), + this._page._delegate.getContentQuads(this), + this._page._delegate.layoutViewport(), ]); if (!quads || !quads.length) throw new Error('Node is either not visible or not an HTMLElement'); @@ -260,7 +264,7 @@ export class ElementHandle extends js.JSHandle { point.x += border.x; point.y += border.y; } - const metrics = await this._world.frame._page._delegate.layoutViewport(); + const metrics = await this._page._delegate.layoutViewport(); // Give 20 extra pixels to avoid any issues on viewport edge. let scrollX = 0; if (point.x < 20) @@ -279,26 +283,26 @@ export class ElementHandle extends js.JSHandle { const point = await this._ensurePointerActionPoint(options ? options.relativePoint : undefined); let restoreModifiers: input.Modifier[] | undefined; if (options && options.modifiers) - restoreModifiers = await this._world.frame._page.keyboard._ensureModifiers(options.modifiers); + restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); await action(point); if (restoreModifiers) - await this._world.frame._page.keyboard._ensureModifiers(restoreModifiers); + await this._page.keyboard._ensureModifiers(restoreModifiers); } hover(options?: input.PointerActionOptions): Promise { - return this._performPointerAction(point => this._world.frame._page.mouse.move(point.x, point.y), options); + return this._performPointerAction(point => this._page.mouse.move(point.x, point.y), options); } click(options?: input.ClickOptions): Promise { - return this._performPointerAction(point => this._world.frame._page.mouse.click(point.x, point.y, options), options); + return this._performPointerAction(point => this._page.mouse.click(point.x, point.y, options), options); } dblclick(options?: input.MultiClickOptions): Promise { - return this._performPointerAction(point => this._world.frame._page.mouse.dblclick(point.x, point.y, options), options); + return this._performPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options); } tripleclick(options?: input.MultiClickOptions): Promise { - return this._performPointerAction(point => this._world.frame._page.mouse.tripleclick(point.x, point.y, options), options); + return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options); } async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise { @@ -321,7 +325,7 @@ export class ElementHandle extends js.JSHandle { const error = await this.evaluate(input.fillFunction); if (error) throw new Error(error); - await this._world.frame._page.keyboard.sendCharacters(value); + await this._page.keyboard.sendCharacters(value); } async setInputFiles(...files: (string|input.FilePayload)[]) { @@ -332,7 +336,7 @@ export class ElementHandle extends js.JSHandle { return input.multiple; }); assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!'); - await this._world.frame._page._delegate.setInputFiles(this, await input.loadFiles(files)); + await this._page._delegate.setInputFiles(this, await input.loadFiles(files)); } async focus() { @@ -348,20 +352,20 @@ export class ElementHandle extends js.JSHandle { async type(text: string, options: { delay: (number | undefined); } | undefined) { await this.focus(); - await this._world.frame._page.keyboard.type(text, options); + await this._page.keyboard.type(text, options); } async press(key: string, options: { delay?: number; text?: string; } | undefined) { await this.focus(); - await this._world.frame._page.keyboard.press(key, options); + await this._page.keyboard.press(key, options); } async boundingBox(): Promise { - return this._world.frame._page._delegate.getBoundingBox(this); + return this._page._delegate.getBoundingBox(this); } async screenshot(options?: types.ElementScreenshotOptions): Promise { - return this._world.frame._page._screenshotter.screenshotElement(this, options); + return this._page._screenshotter.screenshotElement(this, options); } private _scopedSelector(selector: string | types.Selector): string | ScopedSelector { @@ -372,23 +376,23 @@ export class ElementHandle extends js.JSHandle { } $(selector: string | types.Selector): Promise { - return this._world.$(this._scopedSelector(selector)); + return this._context._$(this._scopedSelector(selector)); } $$(selector: string | types.Selector): Promise[]> { - return this._world.$$(this._scopedSelector(selector)); + return this._context._$$(this._scopedSelector(selector)); } $eval: types.$Eval = (selector, pageFunction, ...args) => { - return this._world.$eval(this._scopedSelector(selector), pageFunction, ...args as any); + return this._context._$eval(this._scopedSelector(selector), pageFunction, ...args as any); } $$eval: types.$$Eval = (selector, pageFunction, ...args) => { - return this._world.$$eval(this._scopedSelector(selector), pageFunction, ...args as any); + return this._context._$$eval(this._scopedSelector(selector), pageFunction, ...args as any); } $x(expression: string): Promise[]> { - return this._world.$$({ scope: this, selector: 'xpath=' + expression }); + return this._context._$$({ scope: this, selector: 'xpath=' + expression }); } isIntersectingViewport(): Promise { @@ -422,7 +426,7 @@ function normalizeSelector(selector: string): string { return 'css=' + selector; } -export type Task = (domWorld: DOMWorld) => Promise; +export type Task = (context: FrameExecutionContext) => Promise; export function waitForFunctionTask(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) { const { polling = 'raf' } = options; @@ -434,20 +438,20 @@ export function waitForFunctionTask(pageFunction: Function | string, options: ty throw new Error('Unknown polling options: ' + polling); const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)'; - return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, predicateBody: string, polling: types.Polling, timeout: number, ...args) => { + return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, predicateBody: string, polling: types.Polling, timeout: number, ...args) => { const predicate = new Function('...args', predicateBody); if (polling === 'raf') return injected.pollRaf(predicate, timeout, ...args); if (polling === 'mutation') return injected.pollMutation(predicate, timeout, ...args); return injected.pollInterval(polling, predicate, timeout, ...args); - }, await domWorld.injected(), predicateBody, polling, options.timeout, ...args); + }, await context._injected(), predicateBody, polling, options.timeout, ...args); } export function waitForSelectorTask(selector: string | types.Selector, timeout: number): Task { - return async (domWorld: DOMWorld) => { - const resolved = await domWorld.resolveSelector(selector); - return domWorld.context.evaluateHandle((injected: Injected, selector: string, scope: Node | undefined, visible: boolean | undefined, timeout: number) => { + return async (context: FrameExecutionContext) => { + const resolved = await context._resolveSelector(selector); + return context.evaluateHandle((injected: Injected, selector: string, scope: Node | undefined, visible: boolean | undefined, timeout: number) => { if (visible !== undefined) return injected.pollRaf(predicate, timeout); return injected.pollMutation(predicate, timeout); @@ -460,6 +464,6 @@ export function waitForSelectorTask(selector: string | types.Selector, timeout: return element; return injected.isVisible(element) === visible ? element : false; } - }, await domWorld.injected(), resolved.selector, resolved.scope, resolved.visible, timeout); + }, await context._injected(), resolved.selector, resolved.scope, resolved.visible, timeout); }; } diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 89c467803f..7d3b363d6d 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -109,13 +109,15 @@ export class FrameManager extends EventEmitter implements PageDelegate { _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)); + const delegate = new ExecutionContextDelegate(this._session, executionContextId); if (frame) { - context._domWorld = new dom.DOMWorld(context, frame); + const context = new dom.FrameExecutionContext(delegate, frame); frame._contextCreated('main', context); frame._contextCreated('utility', context); + this._contextIdToContext.set(executionContextId, context); + } else { + this._contextIdToContext.set(executionContextId, new js.ExecutionContext(delegate)); } - this._contextIdToContext.set(executionContextId, context); } _onExecutionContextDestroyed({executionContextId}) { @@ -124,7 +126,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { return; this._contextIdToContext.delete(executionContextId); if (context.frame()) - context.frame()._contextDestroyed(context); + context.frame()._contextDestroyed(context as dom.FrameExecutionContext); } _frameData(frame: frames.Frame): FrameData { @@ -440,7 +442,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { async getContentFrame(handle: dom.ElementHandle): Promise { const { frameId } = await this._session.send('Page.contentFrame', { - frameId: this._frameData(handle._world.frame).frameId, + frameId: this._frameData(handle._context.frame()).frameId, objectId: toRemoteObject(handle).objectId, }); if (!frameId) @@ -473,7 +475,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { async getContentQuads(handle: dom.ElementHandle): Promise { const result = await this._session.send('Page.getContentQuads', { - frameId: this._frameData(handle._world.frame).frameId, + frameId: this._frameData(handle._context.frame()).frameId, objectId: toRemoteObject(handle).objectId, }).catch(debugError); if (!result) @@ -489,7 +491,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { await handle.evaluate(input.setFileInputFunction, files); } - async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise> { + async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { assert(false, 'Multiple isolated worlds are not implemented'); return handle; } diff --git a/src/frames.ts b/src/frames.ts index 2e71e34096..33bc27a606 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -29,11 +29,11 @@ import { ConsoleMessage } from './console'; const readFileAsync = helper.promisify(fs.readFile); -type WorldType = 'main' | 'utility'; -type World = { - contextPromise: Promise; - contextResolveCallback: (c: js.ExecutionContext) => void; - context: js.ExecutionContext | null; +type ContextType = 'main' | 'utility'; +type ContextData = { + contextPromise: Promise; + contextResolveCallback: (c: dom.FrameExecutionContext) => void; + context: dom.FrameExecutionContext | null; rerunnableTasks: Set; }; @@ -55,7 +55,7 @@ export class Frame { private _parentFrame: Frame; private _url = ''; private _detached = false; - private _worlds = new Map(); + private _contextData = new Map(); private _childFrames = new Set(); private _name: string; @@ -65,8 +65,8 @@ export class Frame { this._page = page; this._parentFrame = parentFrame; - this._worlds.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() }); - this._worlds.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() }); + this._contextData.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() }); + this._contextData.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() }); this._setContext('main', null); this._setContext('utility', null); @@ -82,33 +82,19 @@ export class Frame { return this._page._delegate.waitForFrameNavigation(this, options); } - _mainContext(): Promise { + _mainContext(): Promise { if (this._detached) throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); - return this._worlds.get('main').contextPromise; + return this._contextData.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 { + _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; + return this._contextData.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 { + executionContext(): Promise { return this._mainContext(); } @@ -123,28 +109,28 @@ export class Frame { } async $(selector: string | types.Selector): Promise | null> { - const domWorld = await this._mainDOMWorld(); - return domWorld.$(types.clearSelector(selector)); + const context = await this._mainContext(); + return context._$(types.clearSelector(selector)); } async $x(expression: string): Promise[]> { - const domWorld = await this._mainDOMWorld(); - return domWorld.$$('xpath=' + expression); + const context = await this._mainContext(); + return context._$$('xpath=' + expression); } $eval: types.$Eval = async (selector, pageFunction, ...args) => { - const domWorld = await this._mainDOMWorld(); - return domWorld.$eval(selector, pageFunction, ...args as any); + const context = await this._mainContext(); + return context._$eval(selector, pageFunction, ...args as any); } $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { - const domWorld = await this._mainDOMWorld(); - return domWorld.$$eval(selector, pageFunction, ...args as any); + const context = await this._mainContext(); + return context._$$eval(selector, pageFunction, ...args as any); } async $$(selector: string | types.Selector): Promise[]> { - const domWorld = await this._mainDOMWorld(); - return domWorld.$$(types.clearSelector(selector)); + const context = await this._mainContext(); + return context._$$(types.clearSelector(selector)); } async content(): Promise { @@ -319,61 +305,61 @@ export class Frame { } async click(selector: string | types.Selector, options?: ClickOptions) { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); await handle.click(options); await handle.dispose(); } async dblclick(selector: string | types.Selector, options?: MultiClickOptions) { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); await handle.dblclick(options); await handle.dispose(); } async tripleclick(selector: string | types.Selector, options?: MultiClickOptions) { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); await handle.tripleclick(options); await handle.dispose(); } async fill(selector: string | types.Selector, value: string) { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); await handle.fill(value); await handle.dispose(); } async focus(selector: string | types.Selector) { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); await handle.focus(); await handle.dispose(); } async hover(selector: string | types.Selector, options?: PointerActionOptions) { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); await handle.hover(options); await handle.dispose(); } async select(selector: string | types.Selector, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); const toDispose: Promise[] = []; const adoptedValues = await Promise.all(values.map(async value => { - if (value instanceof dom.ElementHandle && value.executionContext() !== domWorld.context) { - const adopted = domWorld.adoptElementHandle(value); + if (value instanceof dom.ElementHandle && value.executionContext() !== context) { + const adopted = context._adoptElementHandle(value); toDispose.push(adopted); return adopted; } @@ -386,8 +372,8 @@ export class Frame { } async type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) { - const domWorld = await this._utilityDOMWorld(); - const handle = await domWorld.$(types.clearSelector(selector)); + const context = await this._utilityContext(); + const handle = await context._$(types.clearSelector(selector)); assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); await handle.type(text, options); await handle.dispose(); @@ -411,10 +397,10 @@ export class Frame { await handle.dispose(); return null; } - const mainDOMWorld = await this._mainDOMWorld(); - if (handle.executionContext() === mainDOMWorld.context) + const maincontext = await this._mainContext(); + if (handle.executionContext() === maincontext) return handle.asElement(); - const adopted = await mainDOMWorld.adoptElementHandle(handle.asElement()); + const adopted = await maincontext._adoptElementHandle(handle.asElement()); await handle.dispose(); return adopted; } @@ -465,8 +451,8 @@ export class Frame { _onDetached() { this._detached = true; - for (const world of this._worlds.values()) { - for (const rerunnableTask of world.rerunnableTasks) + for (const data of this._contextData.values()) { + for (const rerunnableTask of data.rerunnableTasks) rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.')); } if (this._parentFrame) @@ -476,51 +462,50 @@ export class Frame { watcher._onFrameDetached(this); } - private _scheduleRerunnableTask(task: dom.Task, worldType: WorldType, timeout?: number, title?: string): Promise { - const world = this._worlds.get(worldType); - const rerunnableTask = new RerunnableTask(world, task, timeout, title); - world.rerunnableTasks.add(rerunnableTask); - if (world.context) - rerunnableTask.rerun(world.context._domWorld); + private _scheduleRerunnableTask(task: dom.Task, contextType: ContextType, timeout?: number, title?: string): Promise { + const data = this._contextData.get(contextType); + const rerunnableTask = new RerunnableTask(data, task, timeout, title); + data.rerunnableTasks.add(rerunnableTask); + if (data.context) + rerunnableTask.rerun(data.context); return rerunnableTask.promise; } - private _setContext(worldType: WorldType, context: js.ExecutionContext | null) { - const world = this._worlds.get(worldType); - world.context = context; + private _setContext(contextType: ContextType, context: dom.FrameExecutionContext | null) { + const data = this._contextData.get(contextType); + data.context = context; if (context) { - assert(context._domWorld, 'Frame context must have a dom world'); - world.contextResolveCallback.call(null, context); - for (const rerunnableTask of world.rerunnableTasks) - rerunnableTask.rerun(context._domWorld); + data.contextResolveCallback.call(null, context); + for (const rerunnableTask of data.rerunnableTasks) + rerunnableTask.rerun(context); } else { - world.contextPromise = new Promise(fulfill => { - world.contextResolveCallback = fulfill; + data.contextPromise = new Promise(fulfill => { + data.contextResolveCallback = fulfill; }); } } - _contextCreated(worldType: WorldType, context: js.ExecutionContext) { - const world = this._worlds.get(worldType); + _contextCreated(contextType: ContextType, context: dom.FrameExecutionContext) { + const data = this._contextData.get(contextType); // In case of multiple sessions to the same target, there's a race between // connections so we might end up creating multiple isolated worlds. // We can use either. - if (world.context) - this._setContext(worldType, null); - this._setContext(worldType, context); + if (data.context) + this._setContext(contextType, null); + this._setContext(contextType, context); } - _contextDestroyed(context: js.ExecutionContext) { - for (const [worldType, world] of this._worlds) { - if (world.context === context) - this._setContext(worldType, null); + _contextDestroyed(context: dom.FrameExecutionContext) { + for (const [contextType, data] of this._contextData) { + if (data.context === context) + this._setContext(contextType, null); } } } class RerunnableTask { readonly promise: Promise; - private _world: World; + private _contextData: ContextData; private _task: dom.Task; private _runCount: number; private _resolve: (result: js.JSHandle) => void; @@ -528,8 +513,8 @@ class RerunnableTask { private _timeoutTimer: NodeJS.Timer; private _terminated: boolean; - constructor(world: World, task: dom.Task, timeout?: number, title?: string) { - this._world = world; + constructor(data: ContextData, task: dom.Task, timeout?: number, title?: string) { + this._contextData = data; this._task = task; this._runCount = 0; this.promise = new Promise((resolve, reject) => { @@ -550,12 +535,12 @@ class RerunnableTask { this._doCleanup(); } - async rerun(domWorld: dom.DOMWorld) { + async rerun(context: dom.FrameExecutionContext) { const runCount = ++this._runCount; let success: js.JSHandle | null = null; let error = null; try { - success = await this._task(domWorld); + success = await this._task(context); } catch (e) { error = e; } @@ -569,7 +554,7 @@ class RerunnableTask { // Ignore timeouts in pageScript - we track timeouts ourselves. // If execution context has been already destroyed, `context.evaluate` will // throw an error - ignore this predicate run altogether. - if (!error && await domWorld.context.evaluate(s => !s, success).catch(e => true)) { + if (!error && await context.evaluate(s => !s, success).catch(e => true)) { await success.dispose(); return; } @@ -594,7 +579,7 @@ class RerunnableTask { _doCleanup() { clearTimeout(this._timeoutTimer); - this._world.rerunnableTasks.delete(this); + this._contextData.rerunnableTasks.delete(this); } } diff --git a/src/javascript.ts b/src/javascript.ts index 9a559ca6f7..4e15162901 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -15,14 +15,13 @@ export interface ExecutionContextDelegate { export class ExecutionContext { readonly _delegate: ExecutionContextDelegate; - _domWorld?: dom.DOMWorld; constructor(delegate: ExecutionContextDelegate) { this._delegate = delegate; } frame(): frames.Frame | null { - return this._domWorld ? this._domWorld.frame : null; + return null; } evaluate: types.Evaluate = (pageFunction, ...args) => { @@ -34,7 +33,7 @@ export class ExecutionContext { } _createHandle(remoteObject: any): JSHandle { - return (this._domWorld && this._domWorld.createHandle(remoteObject)) || new JSHandle(this, remoteObject); + return new JSHandle(this, remoteObject); } } diff --git a/src/page.ts b/src/page.ts index b2aa6539ed..53fd921fdf 100644 --- a/src/page.ts +++ b/src/page.ts @@ -64,7 +64,7 @@ export interface PageDelegate { resetViewport(oldSize: types.Size): Promise; isElementHandle(remoteObject: any): boolean; - adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise>; + adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise>; getContentFrame(handle: dom.ElementHandle): Promise; getContentQuads(handle: dom.ElementHandle): Promise; layoutViewport(): Promise<{ width: number, height: number }>; @@ -198,10 +198,10 @@ export class Page extends EventEmitter { } async _createSelector(name: string, handle: dom.ElementHandle): Promise { - const mainWorld = await this.mainFrame()._mainDOMWorld(); - return mainWorld.context.evaluate((injected: Injected, target: Element, name: string) => { + const mainContext = await this.mainFrame()._mainContext(); + return mainContext.evaluate((injected: Injected, target: Element, name: string) => { return injected.engines.get(name).create(document.documentElement, target); - }, await mainWorld.injected(), handle, name); + }, await mainContext._injected(), handle, name); } evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 01181b2203..2eb66c0cd1 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -146,7 +146,8 @@ export class FrameManager extends EventEmitter implements PageDelegate { disconnectFromTarget() { for (const context of this._contextIdToContext.values()) { (context._delegate as ExecutionContextDelegate)._dispose(); - context.frame()._contextDestroyed(context); + if (context.frame()) + context.frame()._contextDestroyed(context as dom.FrameExecutionContext); } // this._mainFrame = null; } @@ -250,7 +251,7 @@ export class FrameManager extends EventEmitter implements PageDelegate { const delegate = context._delegate as ExecutionContextDelegate; delegate._dispose(); this._contextIdToContext.delete(delegate._contextId); - frame._contextDestroyed(context); + frame._contextDestroyed(context as dom.FrameExecutionContext); } } @@ -289,15 +290,17 @@ export class FrameManager extends EventEmitter implements PageDelegate { const frame = this._frames.get(frameId) || null; if (!frame) return; - const context = new js.ExecutionContext(new ExecutionContextDelegate(this._session, contextPayload)); + const delegate = new ExecutionContextDelegate(this._session, contextPayload); if (frame) { - context._domWorld = new dom.DOMWorld(context, frame); + const context = new dom.FrameExecutionContext(delegate, frame); if (contextPayload.isPageContext) frame._contextCreated('main', context); else if (contextPayload.name === UTILITY_WORLD_NAME) frame._contextCreated('utility', context); + this._contextIdToContext.set(contextPayload.id, context); + } else { + this._contextIdToContext.set(contextPayload.id, new js.ExecutionContext(delegate)); } - this._contextIdToContext.set(contextPayload.id, context); } executionContextById(contextId: number): js.ExecutionContext { @@ -593,12 +596,12 @@ export class FrameManager extends EventEmitter implements PageDelegate { await this._session.send('DOM.setInputFiles', { objectId, files }); } - async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise> { + async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { const result = await this._session.send('DOM.resolveNode', { objectId: toRemoteObject(handle).objectId, - executionContextId: (to.context._delegate as ExecutionContextDelegate)._contextId + executionContextId: (to._delegate as ExecutionContextDelegate)._contextId }); - return to.context._createHandle(result.object) as dom.ElementHandle; + return to._createHandle(result.object) as dom.ElementHandle; } }