diff --git a/src/chromium/DOMWorld.ts b/src/chromium/DOMWorld.ts index 5d02b8b9f2..2e58d0f64c 100644 --- a/src/chromium/DOMWorld.ts +++ b/src/chromium/DOMWorld.ts @@ -17,30 +17,23 @@ import { ExecutionContext } from './ExecutionContext'; import { Frame } from './Frame'; -import { ElementHandle, JSHandle } from './JSHandle'; -import { TimeoutSettings } from '../TimeoutSettings'; -import { WaitTask, WaitTaskParams, waitForSelectorOrXPath } from '../waitTask'; +import { JSHandle } from './JSHandle'; +import { WaitTask, WaitTaskParams } from '../waitTask'; export class DOMWorld { private _frame: Frame; - private _timeoutSettings: TimeoutSettings; private _contextPromise: Promise; private _contextResolveCallback: ((c: ExecutionContext) => void) | null; - private _context: ExecutionContext | null; + _context: ExecutionContext | null; _waitTasks = new Set>(); private _detached = false; - constructor(frame: Frame, timeoutSettings: TimeoutSettings) { + constructor(frame: Frame) { this._frame = frame; - this._timeoutSettings = timeoutSettings; this._contextPromise; this._setContext(null); } - frame(): Frame { - return this._frame; - } - _setContext(context: ExecutionContext | null) { this._context = context; if (context) { @@ -55,10 +48,6 @@ export class DOMWorld { } } - _hasContext(): boolean { - return !this._contextResolveCallback; - } - _detach() { this._detached = true; for (const waitTask of this._waitTasks) @@ -71,42 +60,7 @@ export class DOMWorld { return this._contextPromise; } - async waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise { - const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options }); - const handle = await this._scheduleWaitTask(params); - if (!handle.asElement()) { - await handle.dispose(); - return null; - } - return handle.asElement(); - } - - async waitForXPath(xpath: string, options: { visible?: boolean, hidden?: boolean, timeout?: number } = {}): Promise { - const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options }); - const handle = await this._scheduleWaitTask(params); - if (!handle.asElement()) { - await handle.dispose(); - return null; - } - return handle.asElement(); - } - - waitForFunction(pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } = {}, ...args): Promise { - const { - polling = 'raf', - timeout = this._timeoutSettings.timeout(), - } = options; - const params: WaitTaskParams = { - predicateBody: pageFunction, - title: 'function', - polling, - timeout, - args - }; - return this._scheduleWaitTask(params); - } - - private _scheduleWaitTask(params: WaitTaskParams): Promise { + scheduleWaitTask(params: WaitTaskParams): Promise { const task = new WaitTask(params, () => this._waitTasks.delete(task)); this._waitTasks.add(task); if (this._context) diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index bb1dfaffc0..ed56e6c4ac 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -16,7 +16,6 @@ */ import { CDPSession } from './Connection'; -import { DOMWorld } from './DOMWorld'; import { Frame } from './Frame'; import { assert, helper } from '../helper'; import { valueFromRemoteObject, getExceptionMessage } from './protocolHelper'; @@ -32,19 +31,19 @@ const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; export class ExecutionContext implements types.EvaluationContext { _client: CDPSession; - _world: DOMWorld; + private _frame: Frame; private _injectedPromise: Promise | null = null; private _documentPromise: Promise | null = null; private _contextId: number; - constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: DOMWorld | null) { + constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, frame: Frame | null) { this._client = client; - this._world = world; + this._frame = frame; this._contextId = contextPayload.id; } frame(): Frame | null { - return this._world ? this._world.frame() : null; + return this._frame; } evaluate: types.Evaluate = (pageFunction, ...args) => { @@ -154,7 +153,7 @@ export class ExecutionContext implements types.EvaluationContext { async _adoptElementHandle(elementHandle: ElementHandle): Promise { assert(elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context'); - assert(this._world, 'Cannot adopt handle without DOMWorld'); + assert(this._frame, 'Cannot adopt handle without a Frame'); const nodeInfo = await this._client.send('DOM.describeNode', { objectId: elementHandle._remoteObject.objectId, }); diff --git a/src/chromium/Frame.ts b/src/chromium/Frame.ts index b028ac565f..cb6af9d2dd 100644 --- a/src/chromium/Frame.ts +++ b/src/chromium/Frame.ts @@ -27,6 +27,7 @@ import { ElementHandle, JSHandle } from './JSHandle'; import { Response } from './NetworkManager'; import { Protocol } from './protocol'; import { LifecycleWatcher } from './LifecycleWatcher'; +import { waitForSelectorOrXPath, WaitTaskParams } from '../waitTask'; const readFileAsync = helper.promisify(fs.readFile); @@ -51,8 +52,8 @@ export class Frame { this._parentFrame = parentFrame; this._id = frameId; - this._mainWorld = new DOMWorld(this, frameManager._timeoutSettings); - this._secondaryWorld = new DOMWorld(this, frameManager._timeoutSettings); + this._mainWorld = new DOMWorld(this); + this._secondaryWorld = new DOMWorld(this); if (this._parentFrame) this._parentFrame._childFrames.add(this); @@ -383,11 +384,13 @@ export class Frame { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise { - const handle = await this._secondaryWorld.waitForSelector(selector, options); - if (!handle) - return null; - const mainExecutionContext = await this._mainWorld.executionContext(); - const result = await mainExecutionContext._adoptElementHandle(handle); + const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._frameManager._timeoutSettings.timeout(), ...options }); + const handle = await this._secondaryWorld.scheduleWaitTask(params); + let result = null; + if (handle.asElement()) { + const mainExecutionContext = await this._mainWorld.executionContext(); + result = await mainExecutionContext._adoptElementHandle(handle.asElement()); + } await handle.dispose(); return result; } @@ -396,11 +399,13 @@ export class Frame { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise { - const handle = await this._secondaryWorld.waitForXPath(xpath, options); - if (!handle) - return null; - const mainExecutionContext = await this._mainWorld.executionContext(); - const result = await mainExecutionContext._adoptElementHandle(handle); + const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._frameManager._timeoutSettings.timeout(), ...options }); + const handle = await this._secondaryWorld.scheduleWaitTask(params); + let result = null; + if (handle.asElement()) { + const mainExecutionContext = await this._mainWorld.executionContext(); + result = await mainExecutionContext._adoptElementHandle(handle.asElement()); + } await handle.dispose(); return result; } @@ -409,7 +414,18 @@ export class Frame { pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } = {}, ...args): Promise { - return this._mainWorld.waitForFunction(pageFunction, options, ...args); + const { + polling = 'raf', + timeout = this._frameManager._timeoutSettings.timeout(), + } = options; + const params: WaitTaskParams = { + predicateBody: pageFunction, + title: 'function', + polling, + timeout, + args + }; + return this._mainWorld.scheduleWaitTask(params); } async title(): Promise { diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 1fac2444f8..5a445fc877 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -25,6 +25,7 @@ import { LifecycleWatcher } from './LifecycleWatcher'; import { NetworkManager, Response } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; +import { DOMWorld } from './DOMWorld'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -258,11 +259,11 @@ export class FrameManager extends EventEmitter { _onExecutionContextCreated(contextPayload) { const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null; const frame = this._frames.get(frameId) || null; - let world = null; + let world: DOMWorld | null = null; if (frame) { if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) { world = frame._mainWorld; - } else if (contextPayload.name === UTILITY_WORLD_NAME && !frame._secondaryWorld._hasContext()) { + } else if (contextPayload.name === UTILITY_WORLD_NAME && !frame._secondaryWorld._context) { // 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. @@ -271,7 +272,7 @@ export class FrameManager extends EventEmitter { } if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated') this._isolatedWorlds.add(contextPayload.name); - const context: ExecutionContext = new ExecutionContext(this._client, contextPayload, world); + const context: ExecutionContext = new ExecutionContext(this._client, contextPayload, frame); if (world) world._setContext(context); this._contextIdToContext.set(contextPayload.id, context); @@ -282,16 +283,18 @@ export class FrameManager extends EventEmitter { if (!context) return; this._contextIdToContext.delete(executionContextId); - if (context._world) - context._world._setContext(null); + const frame = context.frame(); + if (frame) { + if (frame._mainWorld._context === context) + frame._mainWorld._setContext(null); + if (frame._secondaryWorld._context === context) + frame._secondaryWorld._setContext(null); + } } _onExecutionContextsCleared() { - for (const context of this._contextIdToContext.values()) { - if (context._world) - context._world._setContext(null); - } - this._contextIdToContext.clear(); + for (const contextId of Array.from(this._contextIdToContext.keys())) + this._onExecutionContextDestroyed(contextId); } executionContextById(contextId: number): ExecutionContext { diff --git a/src/firefox/DOMWorld.ts b/src/firefox/DOMWorld.ts index 694ca88569..a6bc3b958d 100644 --- a/src/firefox/DOMWorld.ts +++ b/src/firefox/DOMWorld.ts @@ -15,9 +15,9 @@ * limitations under the License. */ -import {ElementHandle, JSHandle} from './JSHandle'; +import { JSHandle } from './JSHandle'; import { ExecutionContext } from './ExecutionContext'; -import { WaitTaskParams, WaitTask, waitForSelectorOrXPath } from '../waitTask'; +import { WaitTaskParams, WaitTask } from '../waitTask'; export class DOMWorld { _frame: any; @@ -69,42 +69,7 @@ export class DOMWorld { return this._contextPromise; } - async waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise { - const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options }); - const handle = await this._scheduleWaitTask(params); - if (!handle.asElement()) { - await handle.dispose(); - return null; - } - return handle.asElement(); - } - - async waitForXPath(xpath: string, options: { visible?: boolean, hidden?: boolean, timeout?: number } = {}): Promise { - const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options }); - const handle = await this._scheduleWaitTask(params); - if (!handle.asElement()) { - await handle.dispose(); - return null; - } - return handle.asElement(); - } - - waitForFunction(pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } | undefined = {}, ...args): Promise { - const { - polling = 'raf', - timeout = this._timeoutSettings.timeout(), - } = options; - const params: WaitTaskParams = { - predicateBody: pageFunction, - title: 'function', - polling, - timeout, - args - }; - return this._scheduleWaitTask(params); - } - - private _scheduleWaitTask(params: WaitTaskParams): Promise { + scheduleWaitTask(params: WaitTaskParams): Promise { const task = new WaitTask(params, () => this._waitTasks.delete(task)); this._waitTasks.add(task); if (this._context) diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 8a7641fdfe..881e601888 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -29,6 +29,7 @@ import { TimeoutSettings } from '../TimeoutSettings'; import { NetworkManager } from './NetworkManager'; import { MultiClickOptions, ClickOptions, SelectOption } from '../input'; import * as types from '../types'; +import { waitForSelectorOrXPath, WaitTaskParams } from '../waitTask'; const readFileAsync = helper.promisify(fs.readFile); @@ -375,16 +376,48 @@ export class Frame { return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout))); } - waitForFunction(pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } | undefined = {}, ...args): Promise { - return this._mainWorld.waitForFunction(pageFunction, options, ...args); + waitForFunction( + pageFunction: Function | string, + options: { polling?: string | number; timeout?: number; } = {}, + ...args): Promise { + const { + polling = 'raf', + timeout = this._frameManager._timeoutSettings.timeout(), + } = options; + const params: WaitTaskParams = { + predicateBody: pageFunction, + title: 'function', + polling, + timeout, + args + }; + return this._mainWorld.scheduleWaitTask(params); } - waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined): Promise { - return this._mainWorld.waitForSelector(selector, options); + async waitForSelector(selector: string, options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; } | undefined): Promise { + const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._frameManager._timeoutSettings.timeout(), ...options }); + const handle = await this._mainWorld.scheduleWaitTask(params); + if (!handle.asElement()) { + await handle.dispose(); + return null; + } + return handle.asElement(); } - waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined): Promise { - return this._mainWorld.waitForXPath(xpath, options); + async waitForXPath(xpath: string, options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; } | undefined): Promise { + const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._frameManager._timeoutSettings.timeout(), ...options }); + const handle = await this._mainWorld.scheduleWaitTask(params); + if (!handle.asElement()) { + await handle.dispose(); + return null; + } + return handle.asElement(); } async content(): Promise {