diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index 42177aae89..2a795cf333 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -16,7 +16,7 @@ */ import { CDPSession } from './Connection'; -import { Frame } from './Frame'; +import { Frame } from './FrameManager'; import { helper } from '../helper'; import { valueFromRemoteObject, getExceptionMessage } from './protocolHelper'; import { createJSHandle, ElementHandle, JSHandle } from './JSHandle'; @@ -29,7 +29,7 @@ import * as types from '../types'; export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; -export class ExecutionContext implements types.EvaluationContext { +export class ExecutionContext { _client: CDPSession; private _frame: Frame; private _injectedPromise: Promise | null = null; diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 6026b608e6..a180c058ee 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -20,12 +20,12 @@ import { assert, debugError } from '../helper'; import { TimeoutSettings } from '../TimeoutSettings'; import { CDPSession } from './Connection'; import { EVALUATION_SCRIPT_URL, ExecutionContext } from './ExecutionContext'; -import { Frame, NavigateOptions, FrameDelegate } from './Frame'; +import * as frames from '../frames'; import { LifecycleWatcher } from './LifecycleWatcher'; import { NetworkManager, Response } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; -import { ElementHandle } from './JSHandle'; +import { ElementHandle, JSHandle } from './JSHandle'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -44,7 +44,9 @@ type FrameData = { lifecycleEvents: Set, }; -export class FrameManager extends EventEmitter implements FrameDelegate { +export type Frame = frames.Frame; + +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _client: CDPSession; private _page: Page; private _networkManager: NetworkManager; @@ -153,7 +155,7 @@ export class FrameManager extends EventEmitter implements FrameDelegate { return watcher.navigationResponse(); } - async setFrameContent(frame: Frame, html: string, options: NavigateOptions = {}) { + async setFrameContent(frame: Frame, html: string, options: frames.NavigateOptions = {}) { const { waitUntil = ['load'], timeout = this._timeoutSettings.navigationTimeout(), @@ -242,7 +244,7 @@ export class FrameManager extends EventEmitter implements FrameDelegate { return; assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new Frame(this, parentFrame); + const frame = new frames.Frame(this, parentFrame); const data: FrameData = { id: frameId, loaderId: '', @@ -273,7 +275,7 @@ export class FrameManager extends EventEmitter implements FrameDelegate { data.id = framePayload.id; } else { // Initial main frame navigation. - frame = new Frame(this, null); + frame = new frames.Frame(this, null); const data: FrameData = { id: framePayload.id, loaderId: '', diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index c1f1cbbe3f..19dc2fcf48 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -21,7 +21,7 @@ import * as input from '../input'; import * as types from '../types'; import { CDPSession } from './Connection'; import { ExecutionContext } from './ExecutionContext'; -import { Frame } from './Frame'; +import { Frame } from './FrameManager'; import { FrameManager } from './FrameManager'; import { Page } from './Page'; import { Protocol } from './protocol'; diff --git a/src/chromium/LifecycleWatcher.ts b/src/chromium/LifecycleWatcher.ts index 5a6778475d..93303c324c 100644 --- a/src/chromium/LifecycleWatcher.ts +++ b/src/chromium/LifecycleWatcher.ts @@ -17,7 +17,7 @@ import { CDPSessionEvents } from './Connection'; import { TimeoutError } from '../Errors'; -import { Frame } from './Frame'; +import { Frame } from './FrameManager'; import { FrameManager, FrameManagerEvents } from './FrameManager'; import { assert, helper, RegisteredListener } from '../helper'; import { NetworkManagerEvents, Request, Response } from './NetworkManager'; diff --git a/src/chromium/NetworkManager.ts b/src/chromium/NetworkManager.ts index 7d742d8ae4..347310a655 100644 --- a/src/chromium/NetworkManager.ts +++ b/src/chromium/NetworkManager.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'events'; import { CDPSession } from './Connection'; -import { Frame } from './Frame'; +import { Frame } from './FrameManager'; import { FrameManager } from './FrameManager'; import { assert, debugError, helper } from '../helper'; import { Protocol } from './protocol'; diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index 1396999fad..609467d5c1 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -33,7 +33,7 @@ import { Overrides } from './features/overrides'; import { Interception } from './features/interception'; import { PDF } from './features/pdf'; import { Workers } from './features/workers'; -import { Frame } from './Frame'; +import { Frame } from './FrameManager'; import { FrameManager, FrameManagerEvents } from './FrameManager'; import { RawMouseImpl, RawKeyboardImpl } from './Input'; import { createJSHandle, ElementHandle, JSHandle } from './JSHandle'; diff --git a/src/chromium/api.ts b/src/chromium/api.ts index 0fe5e9072e..3d222aee9f 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -16,7 +16,7 @@ export { Interception } from './features/interception'; export { PDF } from './features/pdf'; export { Permissions } from './features/permissions'; export { Worker, Workers } from './features/workers'; -export { Frame } from './Frame'; +export { Frame } from '../frames'; export { Keyboard, Mouse } from '../input'; export { ElementHandle, JSHandle } from './JSHandle'; export { Request, Response } from './NetworkManager'; diff --git a/src/firefox/DOMWorld.ts b/src/firefox/DOMWorld.ts deleted file mode 100644 index a6bc3b958d..0000000000 --- a/src/firefox/DOMWorld.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright 2019 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { JSHandle } from './JSHandle'; -import { ExecutionContext } from './ExecutionContext'; -import { WaitTaskParams, WaitTask } from '../waitTask'; - -export class DOMWorld { - _frame: any; - _timeoutSettings: any; - _contextPromise: any; - _contextResolveCallback: any; - private _context: ExecutionContext | null; - _waitTasks: Set>; - _detached: boolean; - constructor(frame, timeoutSettings) { - this._frame = frame; - this._timeoutSettings = timeoutSettings; - - this._contextPromise; - this._contextResolveCallback = null; - this._setContext(null); - - this._waitTasks = new Set(); - this._detached = false; - } - - frame() { - return this._frame; - } - - _setContext(context: ExecutionContext) { - this._context = context; - if (context) { - this._contextResolveCallback.call(null, context); - this._contextResolveCallback = null; - for (const waitTask of this._waitTasks) - waitTask.rerun(context); - } else { - this._contextPromise = new Promise(fulfill => { - this._contextResolveCallback = fulfill; - }); - } - } - - _detach() { - this._detached = true; - for (const waitTask of this._waitTasks) - waitTask.terminate(new Error('waitForFunction failed: frame got detached.')); - } - - async executionContext(): Promise { - if (this._detached) - throw new Error(`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`); - return this._contextPromise; - } - - scheduleWaitTask(params: WaitTaskParams): Promise { - const task = new WaitTask(params, () => this._waitTasks.delete(task)); - this._waitTasks.add(task); - if (this._context) - task.rerun(this._context); - return task.promise; - } -} diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index ea6bd44455..4ddc3ddce0 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -23,7 +23,7 @@ import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource'; import * as xpathSelectorEngineSource from '../generated/xpathSelectorEngineSource'; import * as types from '../types'; -export class ExecutionContext implements types.EvaluationContext { +export class ExecutionContext { _session: any; _frame: Frame; _executionContextId: string; diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 881e601888..d0feaf1a8c 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -17,21 +17,14 @@ import { JugglerSession } from './Connection'; import { Page } from './Page'; -import * as fs from 'fs'; import {RegisteredListener, helper, assert} from '../helper'; import {TimeoutError} from '../Errors'; import {EventEmitter} from 'events'; import {ExecutionContext} from './ExecutionContext'; import {NavigationWatchdog, NextNavigationWatchdog} from './NavigationWatchdog'; -import {DOMWorld} from './DOMWorld'; import { JSHandle, ElementHandle } from './JSHandle'; 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); +import * as frames from '../frames'; export const FrameManagerEvents = { FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), @@ -40,7 +33,17 @@ export const FrameManagerEvents = { Load: Symbol('FrameManagerEvents.Load'), DOMContentLoaded: Symbol('FrameManagerEvents.DOMContentLoaded'), }; -export class FrameManager extends EventEmitter { + +const frameDataSymbol = Symbol('frameData'); +type FrameData = { + frameId: string, + lastCommittedNavigationId: string, + firedEvents: Set, +}; + +export type Frame = frames.Frame; + +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _session: JugglerSession; _page: Page; _networkManager: any; @@ -49,6 +52,7 @@ export class FrameManager extends EventEmitter { _frames: Map; _contextIdToContext: Map; _eventListeners: RegisteredListener[]; + constructor(session: JugglerSession, page: Page, networkManager, timeoutSettings) { super(); this._session = session; @@ -77,8 +81,10 @@ export class FrameManager extends EventEmitter { const frameId = auxData ? auxData.frameId : null; const frame = this._frames.get(frameId) || null; const context = new ExecutionContext(this._session, frame, executionContextId); - if (frame) - frame._mainWorld._setContext(context); + if (frame) { + frame._contextCreated('main', context); + frame._contextCreated('utility', context); + } this._contextIdToContext.set(executionContextId, context); } @@ -87,11 +93,15 @@ export class FrameManager extends EventEmitter { if (!context) return; this._contextIdToContext.delete(executionContextId); - if (context._frame) - context._frame._mainWorld._setContext(null); + if (context.frame()) + context.frame()._contextDestroyed(context); } - frame(frameId) { + _frameData(frame: Frame): FrameData { + return (frame as any)[frameDataSymbol]; + } + + frame(frameId: string): Frame { return this._frames.get(frameId); } @@ -104,32 +114,38 @@ export class FrameManager extends EventEmitter { collect(this._mainFrame); return frames; - function collect(frame) { + function collect(frame: Frame) { frames.push(frame); - for (const subframe of frame._children) + for (const subframe of frame.childFrames()) collect(subframe); } } _onNavigationCommitted(params) { const frame = this._frames.get(params.frameId); - frame._navigated(params.url, params.name, params.navigationId); + frame._navigated(params.url, params.name); + const data = this._frameData(frame); + data.lastCommittedNavigationId = params.navigationId; + data.firedEvents.clear(); this.emit(FrameManagerEvents.FrameNavigated, frame); } _onSameDocumentNavigation(params) { const frame = this._frames.get(params.frameId); - frame._url = params.url; + frame._navigated(params.url, frame.name()); this.emit(FrameManagerEvents.FrameNavigated, frame); } _onFrameAttached(params) { - const frame = new Frame(this._session, this, this._networkManager, this._page, params.frameId, this._timeoutSettings); const parentFrame = this._frames.get(params.parentFrameId) || null; - if (parentFrame) { - frame._parentFrame = parentFrame; - parentFrame._children.add(frame); - } else { + const frame = new frames.Frame(this, parentFrame); + const data: FrameData = { + frameId: params.frameId, + lastCommittedNavigationId: '', + firedEvents: new Set(), + }; + frame[frameDataSymbol] = data; + if (!parentFrame) { assert(!this._mainFrame, 'INTERNAL ERROR: re-attaching main frame!'); this._mainFrame = frame; } @@ -146,7 +162,7 @@ export class FrameManager extends EventEmitter { _onEventFired({frameId, name}) { const frame = this._frames.get(frameId); - frame._firedEvents.add(name.toLowerCase()); + this._frameData(frame).firedEvents.add(name.toLowerCase()); if (frame === this._mainFrame) { if (name === 'load') this.emit(FrameManagerEvents.Load); @@ -158,45 +174,17 @@ export class FrameManager extends EventEmitter { dispose() { helper.removeEventListeners(this._eventListeners); } -} -export class Frame { - _parentFrame: Frame|null = null; - private _session: JugglerSession; - _page: Page; - _frameManager: FrameManager; - private _networkManager: NetworkManager; - private _timeoutSettings: TimeoutSettings; - _frameId: string; - _url: string = ''; - private _name: string = ''; - _children: Set; - private _detached: boolean; - _firedEvents: Set; - _mainWorld: DOMWorld; - _lastCommittedNavigationId: string; - - constructor(session: JugglerSession, frameManager : FrameManager, networkManager, page: Page, frameId: string, timeoutSettings) { - this._session = session; - this._page = page; - this._frameManager = frameManager; - this._networkManager = networkManager; - this._timeoutSettings = timeoutSettings; - this._frameId = frameId; - this._children = new Set(); - this._detached = false; - - - this._firedEvents = new Set(); - - this._mainWorld = new DOMWorld(this, timeoutSettings); + timeoutSettings(): TimeoutSettings { + return this._timeoutSettings; } - async executionContext() { - return this._mainWorld.executionContext(); + async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise { + assert(false, 'Multiple isolated worlds are not implemented'); + return elementHandle; } - async waitForNavigation(options: { timeout?: number; waitUntil?: string | Array; } = {}) { + async waitForFrameNavigation(frame: Frame, options: { timeout?: number; waitUntil?: string | Array; } = {}) { const { timeout = this._timeoutSettings.navigationTimeout(), waitUntil = ['load'], @@ -208,7 +196,7 @@ export class Frame { const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - const nextNavigationDog = new NextNavigationWatchdog(this._session, this); + const nextNavigationDog = new NextNavigationWatchdog(this, frame); const error1 = await Promise.race([ nextNavigationDog.promise(), timeoutPromise, @@ -229,7 +217,7 @@ export class Frame { return null; } - const watchDog = new NavigationWatchdog(this._session, this, this._networkManager, navigationId, url, normalizedWaitUntil); + const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, url, normalizedWaitUntil); const error = await Promise.race([ timeoutPromise, watchDog.promise(), @@ -241,7 +229,7 @@ export class Frame { return watchDog.navigationResponse(); } - async goto(url: string, options: { timeout?: number; waitUntil?: string | Array; referer?: string; } = {}) { + async navigateFrame(frame: Frame, url: string, options: { timeout?: number; waitUntil?: string | Array; referer?: string; } = {}) { const { timeout = this._timeoutSettings.navigationTimeout(), waitUntil = ['load'], @@ -249,7 +237,7 @@ export class Frame { } = options; const normalizedWaitUntil = normalizeWaitUntil(waitUntil); const {navigationId} = await this._session.send('Page.navigate', { - frameId: this._frameId, + frameId: this._frameData(frame).frameId, referer, url, }); @@ -261,7 +249,7 @@ export class Frame { const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - const watchDog = new NavigationWatchdog(this._session, this, this._networkManager, navigationId, url, normalizedWaitUntil); + const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, url, normalizedWaitUntil); const error = await Promise.race([ timeoutPromise, watchDog.promise(), @@ -273,355 +261,14 @@ export class Frame { return watchDog.navigationResponse(); } - async click(selector: string, options?: ClickOptions) { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.click(options); - await handle.dispose(); - } - - async dblclick(selector: string, options?: MultiClickOptions) { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.dblclick(options); - await handle.dispose(); - } - - async tripleclick(selector: string, options?: MultiClickOptions) { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.tripleclick(options); - await handle.dispose(); - } - - async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - const result = await handle.select(...values); - await handle.dispose(); - return result; - } - - async fill(selector: string, value: string) { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.fill(value); - await handle.dispose(); - } - - async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.type(text, options); - await handle.dispose(); - } - - async focus(selector: string) { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.focus(); - await handle.dispose(); - } - - async hover(selector: string) { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - const handle = await document.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.hover(); - await handle.dispose(); - } - - _detach() { - this._parentFrame._children.delete(this); - this._parentFrame = null; - this._detached = true; - this._mainWorld._detach(); - } - - _navigated(url, name, navigationId) { - this._url = url; - this._name = name; - this._lastCommittedNavigationId = navigationId; - this._firedEvents.clear(); - } - - waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: { polling?: string | number; timeout?: number; visible?: boolean; hidden?: boolean; } | undefined, ...args: Array): Promise { - const xPathPattern = '//'; - - if (helper.isString(selectorOrFunctionOrTimeout)) { - const string = selectorOrFunctionOrTimeout; - if (string.startsWith(xPathPattern)) - return this.waitForXPath(string, options); - return this.waitForSelector(string, options); - } - if (helper.isNumber(selectorOrFunctionOrTimeout)) - return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout)); - if (typeof selectorOrFunctionOrTimeout === 'function') - return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args); - return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout))); - } - - 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); - } - - 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(); - } - - 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 { - const context = await this._mainWorld.executionContext(); - return context.evaluate(() => { - let retVal = ''; - if (document.doctype) - retVal = new XMLSerializer().serializeToString(document.doctype); - if (document.documentElement) - retVal += document.documentElement.outerHTML; - return retVal; - }); - } - - async setContent(html: string) { - const context = await this._mainWorld.executionContext(); + async setFrameContent(frame: Frame, html: string) { + const context = await frame._utilityContext(); await context.evaluate(html => { document.open(); document.write(html); document.close(); }, html); } - - evaluate: types.Evaluate = async (pageFunction, ...args) => { - const context = await this._mainWorld.executionContext(); - return context.evaluate(pageFunction, ...args as any); - } - - async $(selector: string): Promise { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - return document.$(selector); - } - - async $$(selector: string): Promise> { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - return document.$$(selector); - } - - $eval: types.$Eval = async (selector, pageFunction, ...args) => { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - return document.$eval(selector, pageFunction, ...args as any); - } - - $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - return document.$$eval(selector, pageFunction, ...args as any); - } - - async $x(expression: string): Promise> { - const context = await this._mainWorld.executionContext(); - const document = await context._document(); - return document.$x(expression); - } - - evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { - const context = await this._mainWorld.executionContext(); - return context.evaluateHandle(pageFunction, ...args as any); - } - - async addScriptTag(options: { - url?: string; path?: string; - content?: string; - type?: string; - }): Promise { - const { - url = null, - path = null, - content = null, - type = '' - } = options; - if (url !== null) { - try { - const context = await this._mainWorld.executionContext(); - return (await context.evaluateHandle(addScriptUrl, url, type)).asElement(); - } catch (error) { - throw new Error(`Loading script from ${url} failed`); - } - } - - if (path !== null) { - let contents = await readFileAsync(path, 'utf8'); - contents += '//# sourceURL=' + path.replace(/\n/g, ''); - const context = await this._mainWorld.executionContext(); - return (await context.evaluateHandle(addScriptContent, contents, type)).asElement(); - } - - if (content !== null) { - const context = await this._mainWorld.executionContext(); - return (await context.evaluateHandle(addScriptContent, content, type)).asElement(); - } - - throw new Error('Provide an object with a `url`, `path` or `content` property'); - - async function addScriptUrl(url: string, type: string): Promise { - const script = document.createElement('script'); - script.src = url; - if (type) - script.type = type; - const promise = new Promise((res, rej) => { - script.onload = res; - script.onerror = rej; - }); - document.head.appendChild(script); - await promise; - return script; - } - - function addScriptContent(content: string, type: string = 'text/javascript'): HTMLElement { - const script = document.createElement('script'); - script.type = type; - script.text = content; - let error = null; - script.onerror = e => error = e; - document.head.appendChild(script); - if (error) - throw error; - return script; - } - } - - async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { - const { - url = null, - path = null, - content = null - } = options; - if (url !== null) { - try { - const context = await this._mainWorld.executionContext(); - return (await context.evaluateHandle(addStyleUrl, url)).asElement(); - } catch (error) { - throw new Error(`Loading style from ${url} failed`); - } - } - - if (path !== null) { - let contents = await readFileAsync(path, 'utf8'); - contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/'; - const context = await this._mainWorld.executionContext(); - return (await context.evaluateHandle(addStyleContent, contents)).asElement(); - } - - if (content !== null) { - const context = await this._mainWorld.executionContext(); - return (await context.evaluateHandle(addStyleContent, content)).asElement(); - } - - throw new Error('Provide an object with a `url`, `path` or `content` property'); - - async function addStyleUrl(url: string): Promise { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = url; - const promise = new Promise((res, rej) => { - link.onload = res; - link.onerror = rej; - }); - document.head.appendChild(link); - await promise; - return link; - } - - async function addStyleContent(content: string): Promise { - const style = document.createElement('style'); - style.type = 'text/css'; - style.appendChild(document.createTextNode(content)); - const promise = new Promise((res, rej) => { - style.onload = res; - style.onerror = rej; - }); - document.head.appendChild(style); - await promise; - return style; - } - } - - async title(): Promise { - const context = await this._mainWorld.executionContext(); - return context.evaluate(() => document.title); - } - - name() { - return this._name; - } - - isDetached() { - return this._detached; - } - - childFrames() { - return Array.from(this._children); - } - - url() { - return this._url; - } - - parentFrame() { - return this._parentFrame; - } } export function normalizeWaitUntil(waitUntil) { diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index 767ef8e0f2..b6a1b864ca 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -21,7 +21,8 @@ import * as input from '../input'; import * as types from '../types'; import { JugglerSession } from './Connection'; import { ExecutionContext } from './ExecutionContext'; -import { Frame } from './FrameManager'; +import { Frame, FrameManager } from './FrameManager'; +import { Page } from './Page'; type SelectorRoot = Element | ShadowRoot | Document; @@ -137,11 +138,14 @@ export class JSHandle { export class ElementHandle extends JSHandle { _frame: Frame; - _frameId: any; - constructor(frame: Frame, context: ExecutionContext, payload: any) { + _frameId: string; + _page: Page; + + constructor(frame: Frame, frameId: string, page: Page, context: ExecutionContext, payload: any) { super(context, payload); this._frame = frame; - this._frameId = frame._frameId; + this._frameId = frameId; + this._page = page; } async contentFrame(): Promise { @@ -151,7 +155,7 @@ export class ElementHandle extends JSHandle { }); if (!frameId) return null; - const frame = this._frame._frameManager.frame(frameId); + const frame = this._page._frameManager.frame(frameId); return frame; } @@ -177,7 +181,7 @@ export class ElementHandle extends JSHandle { assert(clip.height, 'Node has 0 height.'); await this._scrollIntoViewIfNeeded(); - return await this._frame._page.screenshot(Object.assign({}, options, { + return await this._page.screenshot(Object.assign({}, options, { clip: { x: clip.x, y: clip.y, @@ -294,19 +298,19 @@ export class ElementHandle extends JSHandle { async click(options?: input.ClickOptions) { await this._scrollIntoViewIfNeeded(); const {x, y} = await this._clickablePoint(); - await this._frame._page.mouse.click(x, y, options); + await this._page.mouse.click(x, y, options); } async dblclick(options?: input.MultiClickOptions): Promise { await this._scrollIntoViewIfNeeded(); const {x, y} = await this._clickablePoint(); - await this._frame._page.mouse.dblclick(x, y, options); + await this._page.mouse.dblclick(x, y, options); } async tripleclick(options?: input.MultiClickOptions): Promise { await this._scrollIntoViewIfNeeded(); const {x, y} = await this._clickablePoint(); - await this._frame._page.mouse.tripleclick(x, y, options); + await this._page.mouse.tripleclick(x, y, options); } async setInputFiles(...files: (string|input.FilePayload)[]) { @@ -318,7 +322,7 @@ export class ElementHandle extends JSHandle { async hover() { await this._scrollIntoViewIfNeeded(); const {x, y} = await this._clickablePoint(); - await this._frame._page.mouse.move(x, y); + await this._page.mouse.move(x, y); } async focus() { @@ -327,12 +331,12 @@ export class ElementHandle extends JSHandle { async type(text: string, options: { delay: (number | undefined); } | undefined) { await this.focus(); - await this._frame._page.keyboard.type(text, options); + await this._page.keyboard.type(text, options); } async press(key: string, options: { delay?: number; } | undefined) { await this.focus(); - await this._frame._page.keyboard.press(key, options); + await this._page.keyboard.press(key, options); } async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise { @@ -356,7 +360,7 @@ export class ElementHandle extends JSHandle { if (error) throw new Error(error); await this.focus(); - await this._frame._page.keyboard.sendCharacters(value); + await this._page.keyboard.sendCharacters(value); } async _clickablePoint(): Promise<{ x: number; y: number; }> { @@ -376,14 +380,20 @@ export class ElementHandle extends JSHandle { } export function createHandle(context: ExecutionContext, result: any, exceptionDetails?: any) { - const frame = context.frame(); 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); } - return result.subtype === 'node' ? new ElementHandle(frame, context, result) : new JSHandle(context, result); + if (result.subtype === 'node') { + const frame = context.frame(); + const frameManager = frame._delegate as FrameManager; + const frameId = frameManager._frameData(frame).frameId; + const page = frameManager._page; + return new ElementHandle(frame, frameId, page, context, result); + } + return new JSHandle(context, result); } function computeQuadArea(quad) { diff --git a/src/firefox/NavigationWatchdog.ts b/src/firefox/NavigationWatchdog.ts index 6e62527693..7c11448da4 100644 --- a/src/firefox/NavigationWatchdog.ts +++ b/src/firefox/NavigationWatchdog.ts @@ -1,20 +1,23 @@ import { helper, RegisteredListener } from '../helper'; -import { JugglerSession, JugglerSessionEvents } from './Connection'; -import { Frame, FrameManagerEvents } from './FrameManager'; +import { JugglerSessionEvents } from './Connection'; +import { Frame, FrameManagerEvents, FrameManager } from './FrameManager'; import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; export class NextNavigationWatchdog { + private _frameManager: FrameManager; private _navigatedFrame: Frame; private _promise: Promise; private _resolveCallback: (value?: unknown) => void; private _navigation: {navigationId: number|null, url?: string} = null; private _eventListeners: RegisteredListener[]; - constructor(session : JugglerSession, navigatedFrame : Frame) { + + constructor(frameManager: FrameManager, navigatedFrame: Frame) { + this._frameManager = frameManager; this._navigatedFrame = navigatedFrame; this._promise = new Promise(x => this._resolveCallback = x); this._eventListeners = [ - helper.addEventListener(session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)), - helper.addEventListener(session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), + helper.addEventListener(frameManager._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)), + helper.addEventListener(frameManager._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), ]; } @@ -27,7 +30,7 @@ export class NextNavigationWatchdog { } _onNavigationStarted(params) { - if (params.frameId === this._navigatedFrame._frameId) { + if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId) { this._navigation = { navigationId: params.navigationId, url: params.url, @@ -37,7 +40,7 @@ export class NextNavigationWatchdog { } _onSameDocumentNavigation(params) { - if (params.frameId === this._navigatedFrame._frameId) { + if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId) { this._navigation = { navigationId: null, }; @@ -51,6 +54,7 @@ export class NextNavigationWatchdog { } export class NavigationWatchdog { + private _frameManager: FrameManager; private _navigatedFrame: Frame; private _targetNavigationId: any; private _firedEvents: any; @@ -59,7 +63,9 @@ export class NavigationWatchdog { private _resolveCallback: (value?: unknown) => void; private _navigationRequest: any; private _eventListeners: RegisteredListener[]; - constructor(session : JugglerSession, navigatedFrame : Frame, networkManager : NetworkManager, targetNavigationId, targetURL, firedEvents) { + + constructor(frameManager: FrameManager, navigatedFrame: Frame, networkManager: NetworkManager, targetNavigationId, targetURL, firedEvents) { + this._frameManager = frameManager; this._navigatedFrame = navigatedFrame; this._targetNavigationId = targetNavigationId; this._firedEvents = firedEvents; @@ -70,15 +76,15 @@ export class NavigationWatchdog { const check = this._checkNavigationComplete.bind(this); this._eventListeners = [ - helper.addEventListener(session, JugglerSessionEvents.Disconnected, () => this._resolveCallback(new Error('Navigation failed because browser has disconnected!'))), - helper.addEventListener(session, 'Page.eventFired', check), - helper.addEventListener(session, 'Page.frameAttached', check), - helper.addEventListener(session, 'Page.frameDetached', check), - helper.addEventListener(session, 'Page.navigationStarted', check), - helper.addEventListener(session, 'Page.navigationCommitted', check), - helper.addEventListener(session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)), + helper.addEventListener(frameManager._session, JugglerSessionEvents.Disconnected, () => this._resolveCallback(new Error('Navigation failed because browser has disconnected!'))), + helper.addEventListener(frameManager._session, 'Page.eventFired', check), + helper.addEventListener(frameManager._session, 'Page.frameAttached', check), + helper.addEventListener(frameManager._session, 'Page.frameDetached', check), + helper.addEventListener(frameManager._session, 'Page.navigationStarted', check), + helper.addEventListener(frameManager._session, 'Page.navigationCommitted', check), + helper.addEventListener(frameManager._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)), helper.addEventListener(networkManager, NetworkManagerEvents.Request, this._onRequest.bind(this)), - helper.addEventListener(navigatedFrame._frameManager, FrameManagerEvents.FrameDetached, check), + helper.addEventListener(frameManager, FrameManagerEvents.FrameDetached, check), ]; check(); } @@ -94,24 +100,23 @@ export class NavigationWatchdog { } _checkNavigationComplete() { - if (this._navigatedFrame.isDetached()) - this._resolveCallback(new Error('Navigating frame was detached')); - else if (this._navigatedFrame._lastCommittedNavigationId === this._targetNavigationId - && checkFiredEvents(this._navigatedFrame, this._firedEvents)) - this._resolveCallback(null); - - - function checkFiredEvents(frame, firedEvents) { - for (const subframe of frame._children) { + const checkFiredEvents = (frame: Frame, firedEvents) => { + for (const subframe of frame.childFrames()) { if (!checkFiredEvents(subframe, firedEvents)) return false; } - return firedEvents.every(event => frame._firedEvents.has(event)); - } + return firedEvents.every(event => this._frameManager._frameData(frame).firedEvents.has(event)); + }; + + if (this._navigatedFrame.isDetached()) + this._resolveCallback(new Error('Navigating frame was detached')); + else if (this._frameManager._frameData(this._navigatedFrame).lastCommittedNavigationId === this._targetNavigationId + && checkFiredEvents(this._navigatedFrame, this._firedEvents)) + this._resolveCallback(null); } _onNavigationAborted(params) { - if (params.frameId === this._navigatedFrame._frameId && params.navigationId === this._targetNavigationId) + if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId && params.navigationId === this._targetNavigationId) this._resolveCallback(new Error('Navigation to ' + this._targetURL + ' failed: ' + params.errorText)); } diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index b3476d75a0..c74b0e1db0 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -31,7 +31,7 @@ export class Page extends EventEmitter { private _closed: boolean; private _pageBindings: Map; private _networkManager: NetworkManager; - private _frameManager: FrameManager; + _frameManager: FrameManager; private _eventListeners: RegisteredListener[]; private _viewport: Viewport; private _disconnectPromise: Promise; @@ -312,7 +312,7 @@ export class Page extends EventEmitter { const frame = this._frameManager.mainFrame(); const normalizedWaitUntil = normalizeWaitUntil(waitUntil); const {navigationId, navigationURL} = await this._session.send('Page.goBack', { - frameId: frame._frameId, + frameId: this._frameManager._frameData(frame).frameId, }); if (!navigationId) return null; @@ -322,7 +322,7 @@ export class Page extends EventEmitter { const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); + const watchDog = new NavigationWatchdog(this._frameManager, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); const error = await Promise.race([ timeoutPromise, watchDog.promise(), @@ -342,7 +342,7 @@ export class Page extends EventEmitter { const frame = this._frameManager.mainFrame(); const normalizedWaitUntil = normalizeWaitUntil(waitUntil); const {navigationId, navigationURL} = await this._session.send('Page.goForward', { - frameId: frame._frameId, + frameId: this._frameManager._frameData(frame).frameId, }); if (!navigationId) return null; @@ -352,7 +352,7 @@ export class Page extends EventEmitter { const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); + const watchDog = new NavigationWatchdog(this._frameManager, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); const error = await Promise.race([ timeoutPromise, watchDog.promise(), @@ -372,7 +372,7 @@ export class Page extends EventEmitter { const frame = this._frameManager.mainFrame(); const normalizedWaitUntil = normalizeWaitUntil(waitUntil); const {navigationId, navigationURL} = await this._session.send('Page.reload', { - frameId: frame._frameId, + frameId: this._frameManager._frameData(frame).frameId, }); if (!navigationId) return null; @@ -382,7 +382,7 @@ export class Page extends EventEmitter { const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); + const watchDog = new NavigationWatchdog(this._frameManager, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); const error = await Promise.race([ timeoutPromise, watchDog.promise(), diff --git a/src/chromium/Frame.ts b/src/frames.ts similarity index 87% rename from src/chromium/Frame.ts rename to src/frames.ts index e6e64bccbd..52cca45362 100644 --- a/src/chromium/Frame.ts +++ b/src/frames.ts @@ -15,24 +15,21 @@ * limitations under the License. */ -import * as types from '../types'; +import * as types from './types'; import * as fs from 'fs'; -import { helper, assert } from '../helper'; -import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input'; -import { ExecutionContext } from './ExecutionContext'; -import { ElementHandle, JSHandle } from './JSHandle'; -import { Response } from './NetworkManager'; -import { waitForSelectorOrXPath, WaitTaskParams, WaitTask } from '../waitTask'; -import { TimeoutSettings } from '../TimeoutSettings'; +import { helper, assert } from './helper'; +import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input'; +import { waitForSelectorOrXPath, WaitTaskParams, WaitTask } from './waitTask'; +import { TimeoutSettings } from './TimeoutSettings'; const readFileAsync = helper.promisify(fs.readFile); type WorldType = 'main' | 'utility'; -type World = { +type World, ElementHandle extends types.ElementHandle, ExecutionContext> = { contextPromise: Promise; contextResolveCallback: (c: ExecutionContext) => void; context: ExecutionContext | null; - waitTasks: Set>; + waitTasks: Set>; }; export type NavigateOptions = { @@ -44,24 +41,24 @@ export type GotoOptions = NavigateOptions & { referer?: string, }; -export interface FrameDelegate { +export interface FrameDelegate, ElementHandle extends types.ElementHandle, ExecutionContext extends types.ExecutionContext, Response> { timeoutSettings(): TimeoutSettings; - navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise; - waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise; - setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; + navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise; + waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise; + setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise; } -export class Frame { - _delegate: FrameDelegate; - private _parentFrame: Frame; +export class Frame, ElementHandle extends types.ElementHandle, ExecutionContext extends types.ExecutionContext, Response> { + _delegate: FrameDelegate; + private _parentFrame: Frame; private _url = ''; private _detached = false; - private _worlds = new Map(); - private _childFrames = new Set(); + private _worlds = new Map>(); + private _childFrames = new Set>(); private _name: string; - constructor(delegate: FrameDelegate, parentFrame: Frame | null) { + constructor(delegate: FrameDelegate, parentFrame: Frame | null) { this._delegate = delegate; this._parentFrame = parentFrame; @@ -162,11 +159,11 @@ export class Frame { return this._url; } - parentFrame(): Frame | null { + parentFrame(): Frame | null { return this._parentFrame; } - childFrames(): Frame[] { + childFrames(): Frame[] { return Array.from(this._childFrames); } @@ -351,7 +348,11 @@ export class Frame { 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 => value instanceof ElementHandle ? this._adoptElementHandle(value, utilityContext, false /* dispose */) : value)); + const adoptedValues = await Promise.all(values.map(async value => { + if (typeof value === 'object' && (value as any).asElement && (value as any).asElement() === value) + return this._adoptElementHandle(value as ElementHandle, utilityContext, false /* dispose */); + return value; + })); const result = await handle.select(...adoptedValues); await handle.dispose(); return result; @@ -372,8 +373,8 @@ export class Frame { if (helper.isString(selectorOrFunctionOrTimeout)) { const string = selectorOrFunctionOrTimeout as string; if (string.startsWith(xPathPattern)) - return this.waitForXPath(string, options); - return this.waitForSelector(string, options); + return this.waitForXPath(string, options) as any; + return this.waitForSelector(string, options) as any; } if (helper.isNumber(selectorOrFunctionOrTimeout)) return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout as number)); @@ -449,7 +450,7 @@ export class Frame { this._parentFrame = null; } - private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise { + private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise { const task = new WaitTask(params, () => world.waitTasks.delete(task)); world.waitTasks.add(task); if (world.context) diff --git a/src/types.ts b/src/types.ts index c8cc550ab3..ab2e34fef0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import * as input from './input'; + type Boxed = { [Index in keyof Args]: Args[Index] | Handle }; type PageFunction = string | ((...args: Args) => R | Promise); type PageFunctionOn = string | ((on: On, ...args: Args) => R | Promise); @@ -12,11 +14,30 @@ export type $$Eval = (selector: string, pageFunct export type EvaluateOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateHandleOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; -export interface EvaluationContext { +export interface ExecutionContext, EHandle extends ElementHandle> { evaluate: Evaluate; evaluateHandle: EvaluateHandle; + _document(): Promise; } -export interface Handle { +export interface JSHandle, EHandle extends ElementHandle> { + executionContext(): ExecutionContext; dispose(): Promise; + asElement(): EHandle | null; +} + +export interface ElementHandle, EHandle extends ElementHandle> extends JSHandle { + $(selector: string): Promise; + $x(expression: string): Promise; + $$(selector: string): Promise; + $eval: $Eval; + $$eval: $$Eval; + click(options?: input.ClickOptions): Promise; + dblclick(options?: input.MultiClickOptions): Promise; + tripleclick(options?: input.MultiClickOptions): Promise; + fill(value: string): Promise; + focus(): Promise; + hover(options?: input.PointerActionOptions): Promise; + select(...values: (string | EHandle | input.SelectOption)[]): Promise; + type(text: string, options: { delay: (number | undefined); } | undefined): Promise; } diff --git a/src/waitTask.ts b/src/waitTask.ts index dc40687bc5..f3561e7916 100644 --- a/src/waitTask.ts +++ b/src/waitTask.ts @@ -14,12 +14,12 @@ export type WaitTaskParams = { args: any[]; }; -export class WaitTask { - readonly promise: Promise; +export class WaitTask, ElementHandle extends types.ElementHandle> { + readonly promise: Promise; private _cleanup: () => void; private _params: WaitTaskParams & { predicateBody: string }; private _runCount: number; - private _resolve: (result: Handle) => void; + private _resolve: (result: JSHandle) => void; private _reject: (reason: Error) => void; private _timeoutTimer: NodeJS.Timer; private _terminated: boolean; @@ -38,7 +38,7 @@ export class WaitTask { }; this._cleanup = cleanup; this._runCount = 0; - this.promise = new Promise((resolve, reject) => { + this.promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); @@ -56,9 +56,9 @@ export class WaitTask { this._doCleanup(); } - async rerun(context: types.EvaluationContext) { + async rerun(context: types.ExecutionContext) { const runCount = ++this._runCount; - let success: Handle | null = null; + let success: JSHandle | null = null; let error = null; try { success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args); diff --git a/src/webkit/ExecutionContext.ts b/src/webkit/ExecutionContext.ts index 08d94a894d..9dd1bfd449 100644 --- a/src/webkit/ExecutionContext.ts +++ b/src/webkit/ExecutionContext.ts @@ -19,7 +19,7 @@ import { TargetSession } from './Connection'; import { Frame } from './FrameManager'; import { helper } from '../helper'; import { valueFromRemoteObject } from './protocolHelper'; -import { createJSHandle, JSHandle } from './JSHandle'; +import { createJSHandle, JSHandle, ElementHandle } from './JSHandle'; import { Protocol } from './protocol'; import * as injectedSource from '../generated/injectedSource'; import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource'; @@ -29,7 +29,7 @@ import * as types from '../types'; export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; -export class ExecutionContext implements types.EvaluationContext { +export class ExecutionContext { _globalObjectId?: string; _session: TargetSession; _frame: Frame; @@ -37,6 +37,7 @@ export class ExecutionContext implements types.EvaluationContext { private _contextDestroyedCallback: any; private _executionContextDestroyedPromise: Promise; private _injectedPromise: Promise | null = null; + private _documentPromise: Promise | null = null; constructor(client: TargetSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, frame: Frame | null) { this._session = client; @@ -317,4 +318,10 @@ export class ExecutionContext implements types.EvaluationContext { } return this._injectedPromise; } + + _document(): Promise { + if (!this._documentPromise) + this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!); + return this._documentPromise; + } } diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 9691d253af..bcf0b59682 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -16,8 +16,6 @@ */ import * as EventEmitter from 'events'; -import * as fs from 'fs'; -import * as types from '../types'; import { TimeoutError } from '../Errors'; import { Events } from './events'; import { assert, debugError, helper, RegisteredListener } from '../helper'; @@ -28,10 +26,7 @@ import { ElementHandle, JSHandle } from './JSHandle'; import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; -import { MultiClickOptions, ClickOptions, SelectOption } from '../input'; -import { WaitTask, WaitTaskParams, waitForSelectorOrXPath } from '../waitTask'; - -const readFileAsync = helper.promisify(fs.readFile); +import * as frames from '../frames'; export const FrameManagerEvents = { FrameNavigatedWithinDocument: Symbol('FrameNavigatedWithinDocument'), @@ -41,7 +36,14 @@ export const FrameManagerEvents = { FrameNavigated: Symbol('FrameNavigated'), }; -export class FrameManager extends EventEmitter { +const frameDataSymbol = Symbol('frameData'); +type FrameData = { + id: string, +}; + +export type Frame = frames.Frame; + +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _session: TargetSession; _page: Page; _networkManager: NetworkManager; @@ -99,8 +101,10 @@ export class FrameManager extends EventEmitter { } disconnectFromTarget() { - for (const frame of this.frames()) - frame._setContext(null); + for (const context of this._contextIdToContext.values()) { + context._dispose(); + context.frame()._contextDestroyed(context); + } // this._mainFrame = null; } @@ -112,7 +116,6 @@ export class FrameManager extends EventEmitter { const frame = this._frames.get(frameId); if (!frame) return; - frame._onLoadingStopped(); } _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { @@ -142,13 +145,21 @@ export class FrameManager extends EventEmitter { return this._frames.get(frameId) || null; } + _frameData(frame: Frame): FrameData { + return (frame as any)[frameDataSymbol]; + } + _onFrameAttached(frameId: string, parentFrameId: string | null) { if (this._frames.has(frameId)) return; assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new Frame(this, this._session, parentFrame, frameId); - this._frames.set(frame._id, frame); + const frame = new frames.Frame(this, parentFrame); + const data: FrameData = { + id: frameId, + }; + frame[frameDataSymbol] = data; + this._frames.set(frameId, frame); this.emit(FrameManagerEvents.FrameAttached, frame); return frame; } @@ -163,13 +174,17 @@ export class FrameManager extends EventEmitter { this._removeFramesRecursively(child); if (isMainFrame) { // Update frame id to retain frame identity on cross-process navigation. - this._frames.delete(frame._id); - frame._id = framePayload.id; - this._frames.set(framePayload.id, frame); + const data = this._frameData(frame); + this._frames.delete(data.id); + data.id = framePayload.id; } } else if (isMainFrame) { // Initial frame navigation. - frame = new Frame(this, this._session, null, framePayload.id); + frame = new frames.Frame(this, null); + const data: FrameData = { + id: framePayload.id, + }; + frame[frameDataSymbol] = data; this._frames.set(framePayload.id, frame); } else { // FIXME(WebKit): there is no Page.frameAttached event in WK. @@ -180,7 +195,13 @@ export class FrameManager extends EventEmitter { this._mainFrame = frame; // Update frame payload. - frame._navigated(framePayload); + frame._navigated(framePayload.url, framePayload.name); + for (const context of this._contextIdToContext.values()) { + if (context.frame() === frame) { + context._dispose(); + frame._contextDestroyed(context); + } + } this.emit(FrameManagerEvents.FrameNavigated, frame); } @@ -189,7 +210,7 @@ export class FrameManager extends EventEmitter { const frame = this._frames.get(frameId); if (!frame) return; - frame._navigatedWithinDocument(url); + frame._navigated(url, frame.name()); this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame); this.emit(FrameManagerEvents.FrameNavigated, frame); } @@ -209,12 +230,11 @@ export class FrameManager extends EventEmitter { const frame = this._frames.get(frameId) || null; if (!frame) return; - // FIXME(WebKit): we ignore duplicate Runtime.executionContextCreated events here. - if (frame._executionContext && frame._executionContext._contextId === contextPayload.id) - return; - /** @type {!ExecutionContext} */ const context: ExecutionContext = new ExecutionContext(this._session, contextPayload, frame); - frame._setContext(context); + if (frame) { + frame._contextCreated('main', context); + frame._contextCreated('utility', context); + } this._contextIdToContext.set(contextPayload.id, context); } @@ -228,458 +248,52 @@ export class FrameManager extends EventEmitter { for (const child of frame.childFrames()) this._removeFramesRecursively(child); frame._detach(); - this._frames.delete(frame._id); + this._frames.delete(this._frameData(frame).id); this.emit(FrameManagerEvents.FrameDetached, frame); } -} -export class Frame { - _id: string; - _frameManager: FrameManager; - _session: any; - _parentFrame: Frame; - _url: string; - _detached: boolean; - _loaderId: string; - _lifecycleEvents: Set; - _waitTasks: Set>; - _executionContext: ExecutionContext | null; - _contextPromise: Promise; - _contextResolveCallback: (arg: ExecutionContext) => void; - _childFrames: Set; - _documentPromise: Promise; - _name: string; - _navigationURL: any; - constructor(frameManager: FrameManager, client: TargetSession, parentFrame: Frame | null, frameId: string) { - this._frameManager = frameManager; - this._session = client; - this._parentFrame = parentFrame; - this._url = ''; - this._id = frameId; - this._detached = false; - - this._loaderId = ''; - /** @type {!Set} */ - this._lifecycleEvents = new Set(); - - /** @type {!Set} */ - this._waitTasks = new Set(); - - this._executionContext = null; - this._contextPromise = null; - this._contextResolveCallback = null; - this._setContext(null); - - /** @type {!Set} */ - this._childFrames = new Set(); - if (this._parentFrame) - this._parentFrame._childFrames.add(this); + timeoutSettings(): TimeoutSettings { + return this._timeoutSettings; } - async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | Array; } | undefined = {}): Promise { + async navigateFrame(frame: Frame, url: string, options: { referer?: string; timeout?: number; waitUntil?: string | Array; } | undefined = {}): Promise { const { - timeout = this._frameManager._timeoutSettings.navigationTimeout(), + timeout = this._timeoutSettings.navigationTimeout(), } = options; - const watchDog = new NextNavigationWatchdog(this, timeout); + const watchDog = new NextNavigationWatchdog(this, frame, timeout); await this._session.send('Page.navigate', {url}); return watchDog.waitForNavigation(); } - async waitForNavigation(): Promise { + async waitForFrameNavigation(frame: Frame, options?: frames.NavigateOptions): Promise { // FIXME: this method only works for main frames. - const watchDog = new NextNavigationWatchdog(this, 10000); + const watchDog = new NextNavigationWatchdog(this, frame, 10000); return watchDog.waitForNavigation(); } - 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._scheduleWaitTask(params); - if (!handle.asElement()) { - await handle.dispose(); - return null; - } - return handle.asElement(); + async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise { + assert(false, 'Multiple isolated worlds are not implemented'); + return elementHandle; } - async waitForXPath(xpath: string, options: { visible?: boolean, hidden?: boolean, timeout?: number } = {}): Promise { - const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._frameManager._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._frameManager._timeoutSettings.timeout(), - } = options; - const params: WaitTaskParams = { - predicateBody: pageFunction, - title: 'function', - polling, - timeout, - args - }; - return this._scheduleWaitTask(params); - } - - async executionContext(): Promise { - if (this._detached) - throw new Error(`Execution Context is not available in detached frame`); - return this._contextPromise; - } - - evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { - const context = await this.executionContext(); - return context.evaluateHandle(pageFunction, ...args as any); - } - - evaluate: types.Evaluate = async (pageFunction, ...args) => { - const context = await this.executionContext(); - return context.evaluate(pageFunction, ...args as any); - } - - async $(selector: string): Promise { - const document = await this._document(); - const value = await document.$(selector); - return value; - } - - _document(): Promise { - if (!this._documentPromise) { - this._documentPromise = this.executionContext().then(async context => { - const document = await context.evaluateHandle('document'); - return document.asElement(); - }); - } - return this._documentPromise; - } - - async $x(expression: string): Promise> { - const document = await this._document(); - const value = await document.$x(expression); - return value; - } - - $eval: types.$Eval = async (selector, pageFunction, ...args) => { - const document = await this._document(); - return document.$eval(selector, pageFunction, ...args as any); - } - - $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { - const document = await this._document(); - const value = await document.$$eval(selector, pageFunction, ...args as any); - return value; - } - - async $$(selector: string): Promise> { - const document = await this._document(); - const value = await document.$$(selector); - return value; - } - - async content(): Promise { - return await this.evaluate(() => { - let retVal = ''; - if (document.doctype) - retVal = new XMLSerializer().serializeToString(document.doctype); - if (document.documentElement) - retVal += document.documentElement.outerHTML; - return retVal; - }); - } - - async setContent(html: string, options: { timeout?: number; waitUntil?: string | Array; } | undefined = {}) { + async setFrameContent(frame: 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, 1000); - await this.evaluate(html => { + const watchDog = new NextNavigationWatchdog(this, frame, 1000); + await frame.evaluate(html => { document.open(); document.write(html); document.close(); }, html); await watchDog.waitForNavigation(); } - - async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise { - const { - url = null, - path = null, - content = null, - type = '' - } = options; - if (url !== null) { - try { - const context = await this.executionContext(); - return (await context.evaluateHandle(addScriptUrl, url, type)).asElement(); - } catch (error) { - throw new Error(`Loading script from ${url} failed`); - } - } - - if (path !== null) { - let contents = await readFileAsync(path, 'utf8'); - contents += '//# sourceURL=' + path.replace(/\n/g, ''); - const context = await this.executionContext(); - return (await context.evaluateHandle(addScriptContent, contents, type)).asElement(); - } - - if (content !== null) { - const context = await this.executionContext(); - return (await context.evaluateHandle(addScriptContent, content, type)).asElement(); - } - - throw new Error('Provide an object with a `url`, `path` or `content` property'); - - /** - */ - async function addScriptUrl(url: string, type: string): Promise { - const script = document.createElement('script'); - script.src = url; - if (type) - script.type = type; - const promise = new Promise((res, rej) => { - script.onload = res; - script.onerror = rej; - }); - document.head.appendChild(script); - await promise; - return script; - } - - /** - */ - function addScriptContent(content: string, type: string = 'text/javascript'): HTMLElement { - const script = document.createElement('script'); - script.type = type; - script.text = content; - let error = null; - script.onerror = e => error = e; - document.head.appendChild(script); - if (error) - throw error; - return script; - } - } - - async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { - const { - url = null, - path = null, - content = null - } = options; - if (url !== null) { - try { - const context = await this.executionContext(); - return (await context.evaluateHandle(addStyleUrl, url)).asElement(); - } catch (error) { - throw new Error(`Loading style from ${url} failed`); - } - } - - if (path !== null) { - let contents = await readFileAsync(path, 'utf8'); - contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/'; - const context = await this.executionContext(); - return (await context.evaluateHandle(addStyleContent, contents)).asElement(); - } - - if (content !== null) { - const context = await this.executionContext(); - return (await context.evaluateHandle(addStyleContent, content)).asElement(); - } - - throw new Error('Provide an object with a `url`, `path` or `content` property'); - - /** - */ - async function addStyleUrl(url: string): Promise { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = url; - const promise = new Promise((res, rej) => { - link.onload = res; - link.onerror = rej; - }); - document.head.appendChild(link); - await promise; - return link; - } - - /** - */ - async function addStyleContent(content: string): Promise { - const style = document.createElement('style'); - style.type = 'text/css'; - style.appendChild(document.createTextNode(content)); - const promise = new Promise((res, rej) => { - style.onload = res; - style.onerror = rej; - }); - document.head.appendChild(style); - await promise; - return style; - } - } - - name(): string { - return this._name || ''; - } - - url(): string { - return this._url; - } - - parentFrame(): Frame | null { - return this._parentFrame; - } - - childFrames(): Array { - return Array.from(this._childFrames); - } - - isDetached(): boolean { - return this._detached; - } - - async click(selector: string, options?: ClickOptions) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.click(options); - await handle.dispose(); - } - - async dblclick(selector: string, options?: MultiClickOptions) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.dblclick(options); - await handle.dispose(); - } - - async tripleclick(selector: string, options?: MultiClickOptions) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.tripleclick(options); - await handle.dispose(); - } - - async fill(selector: string, value: string) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.fill(value); - await handle.dispose(); - } - - async focus(selector: string) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.focus(); - await handle.dispose(); - } - - async hover(selector: string) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.hover(); - await handle.dispose(); - } - - async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - const result = await handle.select(...values); - await handle.dispose(); - return result; - } - - async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.type(text, options); - await handle.dispose(); - } - - waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: object | undefined = {}, ...args: Array): Promise { - const xPathPattern = '//'; - - if (helper.isString(selectorOrFunctionOrTimeout)) { - const string: string = /** @type {string} */ (selectorOrFunctionOrTimeout); - if (string.startsWith(xPathPattern)) - return this.waitForXPath(string, options); - return this.waitForSelector(string, options); - } - if (helper.isNumber(selectorOrFunctionOrTimeout)) - return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout))); - if (typeof selectorOrFunctionOrTimeout === 'function') - return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args); - return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout))); - } - - async title(): Promise { - return this.evaluate(() => document.title); - } - - _navigated(framePayload: Protocol.Page.Frame) { - this._name = framePayload.name; - // TODO(lushnikov): remove this once requestInterception has loaderId exposed. - this._navigationURL = framePayload.url; - this._url = framePayload.url; - // It may have been disposed by targetDestroyed. - if (this._executionContext) - this._setContext(null); - } - - _navigatedWithinDocument(url: string) { - this._url = url; - } - - _onLoadingStopped() { - this._lifecycleEvents.add('DOMContentLoaded'); - this._lifecycleEvents.add('load'); - } - - _detach() { - this._detached = true; - for (const waitTask of this._waitTasks) - waitTask.terminate(new Error('waitForFunction failed: frame got detached.')); - if (this._parentFrame) - this._parentFrame._childFrames.delete(this); - this._parentFrame = null; - } - - _setContext(context: ExecutionContext | null) { - if (this._executionContext) - this._executionContext._dispose(); - this._executionContext = context; - if (context) { - this._contextResolveCallback.call(null, context); - this._contextResolveCallback = null; - for (const waitTask of this._waitTasks) - waitTask.rerun(context); - } else { - this._documentPromise = null; - this._contextPromise = new Promise(fulfill => { - this._contextResolveCallback = fulfill; - }); - } - } - - private _scheduleWaitTask(params: WaitTaskParams): Promise { - const task = new WaitTask(params, () => this._waitTasks.delete(task)); - this._waitTasks.add(task); - if (this._executionContext) - task.rerun(this._executionContext); - return task.promise; - } } /** * @internal */ class NextNavigationWatchdog { - _frame: any; + _frameManager: FrameManager; + _frame: Frame; _newDocumentNavigationPromise: Promise; _newDocumentNavigationCallback: (value?: unknown) => void; _sameDocumentNavigationPromise: Promise; @@ -689,7 +303,8 @@ class NextNavigationWatchdog { _timeoutPromise: Promise; _timeoutId: NodeJS.Timer; - constructor(frame, timeout) { + constructor(frameManager: FrameManager, frame: Frame, timeout) { + this._frameManager = frameManager; this._frame = frame; this._newDocumentNavigationPromise = new Promise(fulfill => { this._newDocumentNavigationCallback = fulfill; @@ -700,10 +315,10 @@ class NextNavigationWatchdog { /** @type {?Request} */ this._navigationRequest = null; this._eventListeners = [ - helper.addEventListener(frame._frameManager._page, Events.Page.Load, event => this._newDocumentNavigationCallback()), - helper.addEventListener(frame._frameManager, FrameManagerEvents.FrameNavigatedWithinDocument, frame => this._onSameDocumentNavigation(frame)), - helper.addEventListener(frame._frameManager, FrameManagerEvents.TargetSwappedOnNavigation, event => this._onTargetReconnected()), - helper.addEventListener(frame._frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)), + helper.addEventListener(frameManager._page, Events.Page.Load, event => this._newDocumentNavigationCallback()), + helper.addEventListener(frameManager, FrameManagerEvents.FrameNavigatedWithinDocument, frame => this._onSameDocumentNavigation(frame)), + helper.addEventListener(frameManager, FrameManagerEvents.TargetSwappedOnNavigation, event => this._onTargetReconnected()), + helper.addEventListener(frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)), ]; const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms'); let timeoutCallback; @@ -731,7 +346,7 @@ class NextNavigationWatchdog { const context = await this._frame.executionContext(); const readyState = await context.evaluate(() => document.readyState); switch (readyState) { - case 'loaded': + case 'loading': case 'interactive': case 'complete': this._newDocumentNavigationCallback(); diff --git a/src/webkit/JSHandle.ts b/src/webkit/JSHandle.ts index 1cc4506b64..11aa1bec1d 100644 --- a/src/webkit/JSHandle.ts +++ b/src/webkit/JSHandle.ts @@ -34,7 +34,7 @@ const writeFileAsync = helper.promisify(fs.writeFile); export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) { const frame = context.frame(); if (remoteObject.subtype === 'node' && frame) { - const frameManager = frame._frameManager; + const frameManager = frame._delegate as FrameManager; return new ElementHandle(context, context._session, remoteObject, frameManager.page(), frameManager); } return new JSHandle(context, context._session, remoteObject);