From 6e78e12d9079aa77dd28b7dfe1622dd2340bd389 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 26 Nov 2019 08:57:53 -0800 Subject: [PATCH] chore: move meaningful methods with a single callsite from DOMWorld to Frame (#68) --- src/chromium/DOMWorld.ts | 219 +----------------------------- src/chromium/ExecutionContext.ts | 7 + src/chromium/Frame.ts | 225 ++++++++++++++++++++++++++----- src/firefox/DOMWorld.ts | 175 +----------------------- src/firefox/ExecutionContext.ts | 9 +- src/firefox/FrameManager.ts | 207 ++++++++++++++++++++++++---- 6 files changed, 391 insertions(+), 451 deletions(-) diff --git a/src/chromium/DOMWorld.ts b/src/chromium/DOMWorld.ts index f071390ef7..5d02b8b9f2 100644 --- a/src/chromium/DOMWorld.ts +++ b/src/chromium/DOMWorld.ts @@ -15,32 +15,22 @@ * limitations under the License. */ -import * as fs from 'fs'; -import * as types from '../types'; import { ExecutionContext } from './ExecutionContext'; import { Frame } from './Frame'; -import { FrameManager } from './FrameManager'; -import { helper } from '../helper'; import { ElementHandle, JSHandle } from './JSHandle'; -import { LifecycleWatcher } from './LifecycleWatcher'; import { TimeoutSettings } from '../TimeoutSettings'; import { WaitTask, WaitTaskParams, waitForSelectorOrXPath } from '../waitTask'; -const readFileAsync = helper.promisify(fs.readFile); - export class DOMWorld { - private _frameManager: FrameManager; private _frame: Frame; private _timeoutSettings: TimeoutSettings; - private _documentPromise: Promise | null = null; private _contextPromise: Promise; private _contextResolveCallback: ((c: ExecutionContext) => void) | null; private _context: ExecutionContext | null; _waitTasks = new Set>(); private _detached = false; - constructor(frameManager: FrameManager, frame: Frame, timeoutSettings: TimeoutSettings) { - this._frameManager = frameManager; + constructor(frame: Frame, timeoutSettings: TimeoutSettings) { this._frame = frame; this._timeoutSettings = timeoutSettings; this._contextPromise; @@ -59,7 +49,6 @@ export class DOMWorld { for (const waitTask of this._waitTasks) waitTask.rerun(context); } else { - this._documentPromise = null; this._contextPromise = new Promise(fulfill => { this._contextResolveCallback = fulfill; }); @@ -82,208 +71,6 @@ export class DOMWorld { 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; - } - - async _document(): Promise { - if (this._documentPromise) - return 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 | string[]; - } = {}) { - const { - waitUntil = ['load'], - timeout = this._timeoutSettings.navigationTimeout(), - } = options; - // We rely upon the fact that document.open() will reset frame lifecycle with "init" - // lifecycle event. @see https://crrev.com/608658 - await this.evaluate(html => { - document.open(); - document.write(html); - document.close(); - }, html); - const watcher = new LifecycleWatcher(this._frameManager, this._frame, waitUntil, timeout); - const error = await Promise.race([ - watcher.timeoutOrTerminationPromise(), - watcher.lifecyclePromise(), - ]); - watcher.dispose(); - if (error) - throw error; - } - - 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; - } - } - 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); @@ -326,9 +113,5 @@ export class DOMWorld { task.rerun(this._context); return task.promise; } - - async title(): Promise { - return this.evaluate(() => document.title); - } } diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index f45a185bd6..bb1dfaffc0 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -34,6 +34,7 @@ export class ExecutionContext implements types.EvaluationContext { _client: CDPSession; _world: DOMWorld; private _injectedPromise: Promise | null = null; + private _documentPromise: Promise | null = null; private _contextId: number; constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: DOMWorld | null) { @@ -176,4 +177,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/chromium/Frame.ts b/src/chromium/Frame.ts index 94524202cc..b028ac565f 100644 --- a/src/chromium/Frame.ts +++ b/src/chromium/Frame.ts @@ -16,6 +16,7 @@ */ import * as types from '../types'; +import * as fs from 'fs'; import { helper, assert } from '../helper'; import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input'; import { CDPSession } from './Connection'; @@ -25,6 +26,9 @@ import { FrameManager } from './FrameManager'; import { ElementHandle, JSHandle } from './JSHandle'; import { Response } from './NetworkManager'; import { Protocol } from './protocol'; +import { LifecycleWatcher } from './LifecycleWatcher'; + +const readFileAsync = helper.promisify(fs.readFile); export class Frame { _id: string; @@ -47,8 +51,8 @@ export class Frame { this._parentFrame = parentFrame; this._id = frameId; - this._mainWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings); - this._secondaryWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings); + this._mainWorld = new DOMWorld(this, frameManager._timeoutSettings); + this._secondaryWorld = new DOMWorld(this, frameManager._timeoutSettings); if (this._parentFrame) this._parentFrame._childFrames.add(this); @@ -69,43 +73,82 @@ export class Frame { return this._mainWorld.executionContext(); } - evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => { - return this._mainWorld.evaluateHandle(pageFunction, ...args as any); + evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { + const context = await this._mainWorld.executionContext(); + return context.evaluateHandle(pageFunction, ...args as any); } - evaluate: types.Evaluate = (pageFunction, ...args) => { - return this._mainWorld.evaluate(pageFunction, ...args as any); + evaluate: types.Evaluate = async (pageFunction, ...args) => { + const context = await this._mainWorld.executionContext(); + return context.evaluate(pageFunction, ...args as any); } async $(selector: string): Promise { - return this._mainWorld.$(selector); + const context = await this._mainWorld.executionContext(); + const document = await context._document(); + return document.$(selector); } async $x(expression: string): Promise { - return this._mainWorld.$x(expression); + const context = await this._mainWorld.executionContext(); + const document = await context._document(); + return document.$x(expression); } - $eval: types.$Eval = (selector, pageFunction, ...args) => { - return this._mainWorld.$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); } - $$eval: types.$$Eval = (selector, pageFunction, ...args) => { - return this._mainWorld.$$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 $$(selector: string): Promise { - return this._mainWorld.$$(selector); + const context = await this._mainWorld.executionContext(); + const document = await context._document(); + return document.$$(selector); } async content(): Promise { - return this._secondaryWorld.content(); + const context = await this._secondaryWorld.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, options: { timeout?: number; waitUntil?: string | string[]; } = {}) { - return this._secondaryWorld.setContent(html, options); + const { + waitUntil = ['load'], + timeout = this._frameManager._timeoutSettings.navigationTimeout(), + } = options; + const context = await this._secondaryWorld.executionContext(); + // We rely upon the fact that document.open() will reset frame lifecycle with "init" + // lifecycle event. @see https://crrev.com/608658 + await context.evaluate(html => { + document.open(); + document.write(html); + document.close(); + }, html); + const watcher = new LifecycleWatcher(this._frameManager, this, waitUntil, timeout); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise(), + watcher.lifecyclePromise(), + ]); + watcher.dispose(); + if (error) + throw error; } name(): string { @@ -131,61 +174,178 @@ export class Frame { async addScriptTag(options: { url?: string; path?: string; content?: string; - type?: string; }): Promise { - return this._mainWorld.addScriptTag(options); + 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 { - return this._mainWorld.addStyleTag(options); + 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 click(selector: string, options?: ClickOptions) { - const handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.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 handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.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 handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.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 fill(selector: string, value: string) { - const handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.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 focus(selector: string) { - const handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.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, options?: PointerActionOptions) { - const handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.executionContext(); + const document = await context._document(); + const handle = await document.$(selector); assert(handle, 'No node found for selector: ' + selector); await handle.hover(options); await handle.dispose(); } async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { - const handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.executionContext(); + const document = await context._document(); + const handle = await document.$(selector); assert(handle, 'No node found for selector: ' + selector); const secondaryExecutionContext = await this._secondaryWorld.executionContext(); const adoptedValues = await Promise.all(values.map(async value => value instanceof ElementHandle ? secondaryExecutionContext._adoptElementHandle(value) : value)); @@ -195,7 +355,9 @@ export class Frame { } async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { - const handle = await this._secondaryWorld.$(selector); + const context = await this._secondaryWorld.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(); @@ -251,7 +413,8 @@ export class Frame { } async title(): Promise { - return this._secondaryWorld.title(); + const context = await this._secondaryWorld.executionContext(); + return context.evaluate(() => document.title); } _navigated(framePayload: Protocol.Page.Frame) { diff --git a/src/firefox/DOMWorld.ts b/src/firefox/DOMWorld.ts index fdd120e295..694ca88569 100644 --- a/src/firefox/DOMWorld.ts +++ b/src/firefox/DOMWorld.ts @@ -15,19 +15,13 @@ * limitations under the License. */ -import * as fs from 'fs'; -import * as util from 'util'; -import * as types from '../types'; import {ElementHandle, JSHandle} from './JSHandle'; import { ExecutionContext } from './ExecutionContext'; import { WaitTaskParams, WaitTask, waitForSelectorOrXPath } from '../waitTask'; -const readFileAsync = util.promisify(fs.readFile); - export class DOMWorld { _frame: any; _timeoutSettings: any; - _documentPromise: any; _contextPromise: any; _contextResolveCallback: any; private _context: ExecutionContext | null; @@ -37,7 +31,6 @@ export class DOMWorld { this._frame = frame; this._timeoutSettings = timeoutSettings; - this._documentPromise = null; this._contextPromise; this._contextResolveCallback = null; this._setContext(null); @@ -58,7 +51,6 @@ export class DOMWorld { for (const waitTask of this._waitTasks) waitTask.rerun(context); } else { - this._documentPromise = null; this._contextPromise = new Promise(fulfill => { this._contextResolveCallback = fulfill; }); @@ -73,170 +65,9 @@ export class DOMWorld { async executionContext(): Promise { if (this._detached) - throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); + throw new Error(`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`); return this._contextPromise; } - url() { - throw new Error('Method not implemented.'); - } - - 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(); - return document.$(selector); - } - - _document() { - if (!this._documentPromise) - this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()); - return this._documentPromise; - } - - async $x(expression: string): Promise> { - const document = await this._document(); - return document.$x(expression); - } - - $eval: types.$Eval = async (selector, pageFunction, ...args) => { - const document = await this._document(); - return document.$eval(selector, pageFunction, ...args); - } - - $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { - const document = await this._document(); - return document.$$eval(selector, pageFunction, ...args); - } - - async $$(selector: string): Promise> { - const document = await this._document(); - return document.$$(selector); - } - - 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) { - await this.evaluate(html => { - document.open(); - document.write(html); - document.close(); - }, html); - } - - async addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise { - if (typeof options.url === 'string') { - const url = options.url; - try { - return (await this.evaluateHandle(addScriptUrl, url, options.type)).asElement(); - } catch (error) { - throw new Error(`Loading script from ${url} failed`); - } - } - - if (typeof options.path === 'string') { - let contents = await readFileAsync(options.path, 'utf8'); - contents += '//# sourceURL=' + options.path.replace(/\n/g, ''); - return (await this.evaluateHandle(addScriptContent, contents, options.type)).asElement(); - } - - if (typeof options.content === 'string') - return (await this.evaluateHandle(addScriptContent, options.content, options.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: { content?: string; path?: string; url?: string; }): Promise { - if (typeof options.url === 'string') { - const url = options.url; - try { - return (await this.evaluateHandle(addStyleUrl, url)).asElement(); - } catch (error) { - throw new Error(`Loading style from ${url} failed`); - } - } - - if (typeof options.path === 'string') { - let contents = await readFileAsync(options.path, 'utf8'); - contents += '/*# sourceURL=' + options.path.replace(/\n/g, '') + '*/'; - return (await this.evaluateHandle(addStyleContent, contents)).asElement(); - } - - if (typeof options.content === 'string') - return (await this.evaluateHandle(addStyleContent, options.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 waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise { const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options }); @@ -273,10 +104,6 @@ export class DOMWorld { return this._scheduleWaitTask(params); } - async title(): Promise { - return this.evaluate(() => document.title); - } - private _scheduleWaitTask(params: WaitTaskParams): Promise { const task = new WaitTask(params, () => this._waitTasks.delete(task)); this._waitTasks.add(task); diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index ecff55c952..ea6bd44455 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -16,7 +16,7 @@ */ import {helper} from '../helper'; -import {JSHandle, createHandle} from './JSHandle'; +import {JSHandle, createHandle, ElementHandle} from './JSHandle'; import { Frame } from './FrameManager'; import * as injectedSource from '../generated/injectedSource'; import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource'; @@ -28,6 +28,7 @@ export class ExecutionContext implements types.EvaluationContext { _frame: Frame; _executionContextId: string; private _injectedPromise: Promise | null = null; + private _documentPromise: Promise | null = null; constructor(session: any, frame: Frame | null, executionContextId: string) { this._session = session; @@ -132,4 +133,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/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 448c094744..8a7641fdfe 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -17,7 +17,7 @@ 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'; @@ -30,6 +30,8 @@ import { NetworkManager } from './NetworkManager'; import { MultiClickOptions, ClickOptions, SelectOption } from '../input'; import * as types from '../types'; +const readFileAsync = helper.promisify(fs.readFile); + export const FrameManagerEvents = { FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), FrameAttached: Symbol('FrameManagerEvents.FrameAttached'), @@ -271,28 +273,36 @@ export class Frame { } async click(selector: string, options?: ClickOptions) { - const handle = await this.$(selector); + 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 handle = await this.$(selector); + 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 handle = await this.$(selector); + 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 handle = await this.$(selector); + 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(); @@ -300,28 +310,36 @@ export class Frame { } async fill(selector: string, value: string) { - const handle = await this.$(selector); + 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 handle = await this.$(selector); + 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 handle = await this.$(selector); + 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 handle = await this.$(selector); + 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(); @@ -370,51 +388,186 @@ export class Frame { } async content(): Promise { - return this._mainWorld.content(); + 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) { - return this._mainWorld.setContent(html); + const context = await this._mainWorld.executionContext(); + await context.evaluate(html => { + document.open(); + document.write(html); + document.close(); + }, html); } - evaluate: types.Evaluate = (pageFunction, ...args) => { - return this._mainWorld.evaluate(pageFunction, ...args as any); + evaluate: types.Evaluate = async (pageFunction, ...args) => { + const context = await this._mainWorld.executionContext(); + return context.evaluate(pageFunction, ...args as any); } async $(selector: string): Promise { - return this._mainWorld.$(selector); + const context = await this._mainWorld.executionContext(); + const document = await context._document(); + return document.$(selector); } async $$(selector: string): Promise> { - return this._mainWorld.$$(selector); + const context = await this._mainWorld.executionContext(); + const document = await context._document(); + return document.$$(selector); } - $eval: types.$Eval = (selector, pageFunction, ...args) => { - return this._mainWorld.$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); } - $$eval: types.$$Eval = (selector, pageFunction, ...args) => { - return this._mainWorld.$$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> { - return this._mainWorld.$x(expression); + const context = await this._mainWorld.executionContext(); + const document = await context._document(); + return document.$x(expression); } - evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => { - return this._mainWorld.evaluateHandle(pageFunction, ...args as any); + evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { + const context = await this._mainWorld.executionContext(); + return context.evaluateHandle(pageFunction, ...args as any); } - async addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise { - return this._mainWorld.addScriptTag(options); + 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: { content?: string; path?: string; url?: string; }): Promise { - return this._mainWorld.addStyleTag(options); + 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 { - return this._mainWorld.title(); + const context = await this._mainWorld.executionContext(); + return context.evaluate(() => document.title); } name() {