chore: move meaningful methods with a single callsite from DOMWorld to Frame (#68)

This commit is contained in:
Dmitry Gozman 2019-11-26 08:57:53 -08:00 committed by GitHub
parent 991f4a9072
commit 6e78e12d90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 391 additions and 451 deletions

View file

@ -15,32 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs';
import * as types from '../types';
import { ExecutionContext } from './ExecutionContext'; import { ExecutionContext } from './ExecutionContext';
import { Frame } from './Frame'; import { Frame } from './Frame';
import { FrameManager } from './FrameManager';
import { helper } from '../helper';
import { ElementHandle, JSHandle } from './JSHandle'; import { ElementHandle, JSHandle } from './JSHandle';
import { LifecycleWatcher } from './LifecycleWatcher';
import { TimeoutSettings } from '../TimeoutSettings'; import { TimeoutSettings } from '../TimeoutSettings';
import { WaitTask, WaitTaskParams, waitForSelectorOrXPath } from '../waitTask'; import { WaitTask, WaitTaskParams, waitForSelectorOrXPath } from '../waitTask';
const readFileAsync = helper.promisify(fs.readFile);
export class DOMWorld { export class DOMWorld {
private _frameManager: FrameManager;
private _frame: Frame; private _frame: Frame;
private _timeoutSettings: TimeoutSettings; private _timeoutSettings: TimeoutSettings;
private _documentPromise: Promise<ElementHandle> | null = null;
private _contextPromise: Promise<ExecutionContext>; private _contextPromise: Promise<ExecutionContext>;
private _contextResolveCallback: ((c: ExecutionContext) => void) | null; private _contextResolveCallback: ((c: ExecutionContext) => void) | null;
private _context: ExecutionContext | null; private _context: ExecutionContext | null;
_waitTasks = new Set<WaitTask<JSHandle>>(); _waitTasks = new Set<WaitTask<JSHandle>>();
private _detached = false; private _detached = false;
constructor(frameManager: FrameManager, frame: Frame, timeoutSettings: TimeoutSettings) { constructor(frame: Frame, timeoutSettings: TimeoutSettings) {
this._frameManager = frameManager;
this._frame = frame; this._frame = frame;
this._timeoutSettings = timeoutSettings; this._timeoutSettings = timeoutSettings;
this._contextPromise; this._contextPromise;
@ -59,7 +49,6 @@ export class DOMWorld {
for (const waitTask of this._waitTasks) for (const waitTask of this._waitTasks)
waitTask.rerun(context); waitTask.rerun(context);
} else { } else {
this._documentPromise = null;
this._contextPromise = new Promise(fulfill => { this._contextPromise = new Promise(fulfill => {
this._contextResolveCallback = fulfill; this._contextResolveCallback = fulfill;
}); });
@ -82,208 +71,6 @@ export class DOMWorld {
return this._contextPromise; return this._contextPromise;
} }
evaluateHandle: types.EvaluateHandle<JSHandle> = async (pageFunction, ...args) => {
const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args as any);
}
evaluate: types.Evaluate<JSHandle> = async (pageFunction, ...args) => {
const context = await this.executionContext();
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string): Promise<ElementHandle | null> {
const document = await this._document();
const value = await document.$(selector);
return value;
}
async _document(): Promise<ElementHandle> {
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<ElementHandle[]> {
const document = await this._document();
const value = await document.$x(expression);
return value;
}
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const document = await this._document();
return document.$eval(selector, pageFunction, ...args as any);
}
$$eval: types.$$Eval<JSHandle> = 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<ElementHandle[]> {
const document = await this._document();
const value = await document.$$(selector);
return value;
}
async content(): Promise<string> {
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<ElementHandle> {
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<HTMLElement> {
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<ElementHandle> {
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<HTMLElement> {
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<HTMLElement> {
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<ElementHandle | null> { async waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise<ElementHandle | null> {
const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options }); const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options });
const handle = await this._scheduleWaitTask(params); const handle = await this._scheduleWaitTask(params);
@ -326,9 +113,5 @@ export class DOMWorld {
task.rerun(this._context); task.rerun(this._context);
return task.promise; return task.promise;
} }
async title(): Promise<string> {
return this.evaluate(() => document.title);
}
} }

View file

@ -34,6 +34,7 @@ export class ExecutionContext implements types.EvaluationContext<JSHandle> {
_client: CDPSession; _client: CDPSession;
_world: DOMWorld; _world: DOMWorld;
private _injectedPromise: Promise<JSHandle> | null = null; private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<ElementHandle> | null = null;
private _contextId: number; private _contextId: number;
constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: DOMWorld | null) { constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: DOMWorld | null) {
@ -176,4 +177,10 @@ export class ExecutionContext implements types.EvaluationContext<JSHandle> {
} }
return this._injectedPromise; return this._injectedPromise;
} }
_document(): Promise<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
} }

View file

@ -16,6 +16,7 @@
*/ */
import * as types from '../types'; import * as types from '../types';
import * as fs from 'fs';
import { helper, assert } from '../helper'; import { helper, assert } from '../helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input'; import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input';
import { CDPSession } from './Connection'; import { CDPSession } from './Connection';
@ -25,6 +26,9 @@ import { FrameManager } from './FrameManager';
import { ElementHandle, JSHandle } from './JSHandle'; import { ElementHandle, JSHandle } from './JSHandle';
import { Response } from './NetworkManager'; import { Response } from './NetworkManager';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { LifecycleWatcher } from './LifecycleWatcher';
const readFileAsync = helper.promisify(fs.readFile);
export class Frame { export class Frame {
_id: string; _id: string;
@ -47,8 +51,8 @@ export class Frame {
this._parentFrame = parentFrame; this._parentFrame = parentFrame;
this._id = frameId; this._id = frameId;
this._mainWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings); this._mainWorld = new DOMWorld(this, frameManager._timeoutSettings);
this._secondaryWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings); this._secondaryWorld = new DOMWorld(this, frameManager._timeoutSettings);
if (this._parentFrame) if (this._parentFrame)
this._parentFrame._childFrames.add(this); this._parentFrame._childFrames.add(this);
@ -69,43 +73,82 @@ export class Frame {
return this._mainWorld.executionContext(); return this._mainWorld.executionContext();
} }
evaluateHandle: types.EvaluateHandle<JSHandle> = (pageFunction, ...args) => { evaluateHandle: types.EvaluateHandle<JSHandle> = async (pageFunction, ...args) => {
return this._mainWorld.evaluateHandle(pageFunction, ...args as any); const context = await this._mainWorld.executionContext();
return context.evaluateHandle(pageFunction, ...args as any);
} }
evaluate: types.Evaluate<JSHandle> = (pageFunction, ...args) => { evaluate: types.Evaluate<JSHandle> = async (pageFunction, ...args) => {
return this._mainWorld.evaluate(pageFunction, ...args as any); const context = await this._mainWorld.executionContext();
return context.evaluate(pageFunction, ...args as any);
} }
async $(selector: string): Promise<ElementHandle | null> { async $(selector: string): Promise<ElementHandle | null> {
return this._mainWorld.$(selector); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$(selector);
} }
async $x(expression: string): Promise<ElementHandle[]> { async $x(expression: string): Promise<ElementHandle[]> {
return this._mainWorld.$x(expression); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$x(expression);
} }
$eval: types.$Eval<JSHandle> = (selector, pageFunction, ...args) => { $eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
return this._mainWorld.$eval(selector, pageFunction, ...args as any); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$eval(selector, pageFunction, ...args as any);
} }
$$eval: types.$$Eval<JSHandle> = (selector, pageFunction, ...args) => { $$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
return this._mainWorld.$$eval(selector, pageFunction, ...args as any); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$$eval(selector, pageFunction, ...args as any);
} }
async $$(selector: string): Promise<ElementHandle[]> { async $$(selector: string): Promise<ElementHandle[]> {
return this._mainWorld.$$(selector); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$$(selector);
} }
async content(): Promise<string> { async content(): Promise<string> {
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: { async setContent(html: string, options: {
timeout?: number; timeout?: number;
waitUntil?: string | string[]; 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 { name(): string {
@ -131,61 +174,178 @@ export class Frame {
async addScriptTag(options: { async addScriptTag(options: {
url?: string; path?: string; url?: string; path?: string;
content?: string; content?: string;
type?: string; }): Promise<ElementHandle> { type?: string;
return this._mainWorld.addScriptTag(options); }): Promise<ElementHandle> {
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<HTMLElement> {
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: { async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
url?: string; const {
path?: string; url = null,
content?: string; }): Promise<ElementHandle> { path = null,
return this._mainWorld.addStyleTag(options); 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<HTMLElement> {
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<HTMLElement> {
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) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.click(options); await handle.click(options);
await handle.dispose(); await handle.dispose();
} }
async dblclick(selector: string, options?: MultiClickOptions) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.dblclick(options); await handle.dblclick(options);
await handle.dispose(); await handle.dispose();
} }
async tripleclick(selector: string, options?: MultiClickOptions) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.tripleclick(options); await handle.tripleclick(options);
await handle.dispose(); await handle.dispose();
} }
async fill(selector: string, value: string) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.fill(value); await handle.fill(value);
await handle.dispose(); await handle.dispose();
} }
async focus(selector: string) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.focus(); await handle.focus();
await handle.dispose(); await handle.dispose();
} }
async hover(selector: string, options?: PointerActionOptions) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.hover(options); await handle.hover(options);
await handle.dispose(); await handle.dispose();
} }
async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> { async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<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); assert(handle, 'No node found for selector: ' + selector);
const secondaryExecutionContext = await this._secondaryWorld.executionContext(); const secondaryExecutionContext = await this._secondaryWorld.executionContext();
const adoptedValues = await Promise.all(values.map(async value => value instanceof ElementHandle ? secondaryExecutionContext._adoptElementHandle(value) : value)); 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) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.type(text, options); await handle.type(text, options);
await handle.dispose(); await handle.dispose();
@ -251,7 +413,8 @@ export class Frame {
} }
async title(): Promise<string> { async title(): Promise<string> {
return this._secondaryWorld.title(); const context = await this._secondaryWorld.executionContext();
return context.evaluate(() => document.title);
} }
_navigated(framePayload: Protocol.Page.Frame) { _navigated(framePayload: Protocol.Page.Frame) {

View file

@ -15,19 +15,13 @@
* limitations under the License. * 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 {ElementHandle, JSHandle} from './JSHandle';
import { ExecutionContext } from './ExecutionContext'; import { ExecutionContext } from './ExecutionContext';
import { WaitTaskParams, WaitTask, waitForSelectorOrXPath } from '../waitTask'; import { WaitTaskParams, WaitTask, waitForSelectorOrXPath } from '../waitTask';
const readFileAsync = util.promisify(fs.readFile);
export class DOMWorld { export class DOMWorld {
_frame: any; _frame: any;
_timeoutSettings: any; _timeoutSettings: any;
_documentPromise: any;
_contextPromise: any; _contextPromise: any;
_contextResolveCallback: any; _contextResolveCallback: any;
private _context: ExecutionContext | null; private _context: ExecutionContext | null;
@ -37,7 +31,6 @@ export class DOMWorld {
this._frame = frame; this._frame = frame;
this._timeoutSettings = timeoutSettings; this._timeoutSettings = timeoutSettings;
this._documentPromise = null;
this._contextPromise; this._contextPromise;
this._contextResolveCallback = null; this._contextResolveCallback = null;
this._setContext(null); this._setContext(null);
@ -58,7 +51,6 @@ export class DOMWorld {
for (const waitTask of this._waitTasks) for (const waitTask of this._waitTasks)
waitTask.rerun(context); waitTask.rerun(context);
} else { } else {
this._documentPromise = null;
this._contextPromise = new Promise(fulfill => { this._contextPromise = new Promise(fulfill => {
this._contextResolveCallback = fulfill; this._contextResolveCallback = fulfill;
}); });
@ -73,170 +65,9 @@ export class DOMWorld {
async executionContext(): Promise<ExecutionContext> { async executionContext(): Promise<ExecutionContext> {
if (this._detached) 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; return this._contextPromise;
} }
url() {
throw new Error('Method not implemented.');
}
evaluateHandle: types.EvaluateHandle<JSHandle> = async (pageFunction, ...args) => {
const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args as any);
}
evaluate: types.Evaluate<JSHandle> = async (pageFunction, ...args) => {
const context = await this.executionContext();
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string): Promise<ElementHandle | null> {
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<Array<ElementHandle>> {
const document = await this._document();
return document.$x(expression);
}
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const document = await this._document();
return document.$eval(selector, pageFunction, ...args);
}
$$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const document = await this._document();
return document.$$eval(selector, pageFunction, ...args);
}
async $$(selector: string): Promise<Array<ElementHandle>> {
const document = await this._document();
return document.$$(selector);
}
async content(): Promise<string> {
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<ElementHandle> {
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<HTMLElement> {
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<ElementHandle> {
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<HTMLElement> {
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<HTMLElement> {
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<ElementHandle | null> { async waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise<ElementHandle | null> {
const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options }); const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._timeoutSettings.timeout(), ...options });
@ -273,10 +104,6 @@ export class DOMWorld {
return this._scheduleWaitTask(params); return this._scheduleWaitTask(params);
} }
async title(): Promise<string> {
return this.evaluate(() => document.title);
}
private _scheduleWaitTask(params: WaitTaskParams): Promise<JSHandle> { private _scheduleWaitTask(params: WaitTaskParams): Promise<JSHandle> {
const task = new WaitTask(params, () => this._waitTasks.delete(task)); const task = new WaitTask(params, () => this._waitTasks.delete(task));
this._waitTasks.add(task); this._waitTasks.add(task);

View file

@ -16,7 +16,7 @@
*/ */
import {helper} from '../helper'; import {helper} from '../helper';
import {JSHandle, createHandle} from './JSHandle'; import {JSHandle, createHandle, ElementHandle} from './JSHandle';
import { Frame } from './FrameManager'; import { Frame } from './FrameManager';
import * as injectedSource from '../generated/injectedSource'; import * as injectedSource from '../generated/injectedSource';
import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource'; import * as cssSelectorEngineSource from '../generated/cssSelectorEngineSource';
@ -28,6 +28,7 @@ export class ExecutionContext implements types.EvaluationContext<JSHandle> {
_frame: Frame; _frame: Frame;
_executionContextId: string; _executionContextId: string;
private _injectedPromise: Promise<JSHandle> | null = null; private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<ElementHandle> | null = null;
constructor(session: any, frame: Frame | null, executionContextId: string) { constructor(session: any, frame: Frame | null, executionContextId: string) {
this._session = session; this._session = session;
@ -132,4 +133,10 @@ export class ExecutionContext implements types.EvaluationContext<JSHandle> {
} }
return this._injectedPromise; return this._injectedPromise;
} }
_document(): Promise<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
} }

View file

@ -17,7 +17,7 @@
import { JugglerSession } from './Connection'; import { JugglerSession } from './Connection';
import { Page } from './Page'; import { Page } from './Page';
import * as fs from 'fs';
import {RegisteredListener, helper, assert} from '../helper'; import {RegisteredListener, helper, assert} from '../helper';
import {TimeoutError} from '../Errors'; import {TimeoutError} from '../Errors';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
@ -30,6 +30,8 @@ import { NetworkManager } from './NetworkManager';
import { MultiClickOptions, ClickOptions, SelectOption } from '../input'; import { MultiClickOptions, ClickOptions, SelectOption } from '../input';
import * as types from '../types'; import * as types from '../types';
const readFileAsync = helper.promisify(fs.readFile);
export const FrameManagerEvents = { export const FrameManagerEvents = {
FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'),
FrameAttached: Symbol('FrameManagerEvents.FrameAttached'), FrameAttached: Symbol('FrameManagerEvents.FrameAttached'),
@ -271,28 +273,36 @@ export class Frame {
} }
async click(selector: string, options?: ClickOptions) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.click(options); await handle.click(options);
await handle.dispose(); await handle.dispose();
} }
async dblclick(selector: string, options?: MultiClickOptions) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.dblclick(options); await handle.dblclick(options);
await handle.dispose(); await handle.dispose();
} }
async tripleclick(selector: string, options?: MultiClickOptions) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.tripleclick(options); await handle.tripleclick(options);
await handle.dispose(); await handle.dispose();
} }
async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> { async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<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); assert(handle, 'No node found for selector: ' + selector);
const result = await handle.select(...values); const result = await handle.select(...values);
await handle.dispose(); await handle.dispose();
@ -300,28 +310,36 @@ export class Frame {
} }
async fill(selector: string, value: string) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.fill(value); await handle.fill(value);
await handle.dispose(); await handle.dispose();
} }
async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.type(text, options); await handle.type(text, options);
await handle.dispose(); await handle.dispose();
} }
async focus(selector: string) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.focus(); await handle.focus();
await handle.dispose(); await handle.dispose();
} }
async hover(selector: string) { 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); assert(handle, 'No node found for selector: ' + selector);
await handle.hover(); await handle.hover();
await handle.dispose(); await handle.dispose();
@ -370,51 +388,186 @@ export class Frame {
} }
async content(): Promise<string> { async content(): Promise<string> {
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) { 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<JSHandle> = (pageFunction, ...args) => { evaluate: types.Evaluate<JSHandle> = async (pageFunction, ...args) => {
return this._mainWorld.evaluate(pageFunction, ...args as any); const context = await this._mainWorld.executionContext();
return context.evaluate(pageFunction, ...args as any);
} }
async $(selector: string): Promise<ElementHandle | null> { async $(selector: string): Promise<ElementHandle | null> {
return this._mainWorld.$(selector); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$(selector);
} }
async $$(selector: string): Promise<Array<ElementHandle>> { async $$(selector: string): Promise<Array<ElementHandle>> {
return this._mainWorld.$$(selector); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$$(selector);
} }
$eval: types.$Eval<JSHandle> = (selector, pageFunction, ...args) => { $eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
return this._mainWorld.$eval(selector, pageFunction, ...args as any); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$eval(selector, pageFunction, ...args as any);
} }
$$eval: types.$$Eval<JSHandle> = (selector, pageFunction, ...args) => { $$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
return this._mainWorld.$$eval(selector, pageFunction, ...args as any); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$$eval(selector, pageFunction, ...args as any);
} }
async $x(expression: string): Promise<Array<ElementHandle>> { async $x(expression: string): Promise<Array<ElementHandle>> {
return this._mainWorld.$x(expression); const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$x(expression);
} }
evaluateHandle: types.EvaluateHandle<JSHandle> = (pageFunction, ...args) => { evaluateHandle: types.EvaluateHandle<JSHandle> = async (pageFunction, ...args) => {
return this._mainWorld.evaluateHandle(pageFunction, ...args as any); 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<ElementHandle> { async addScriptTag(options: {
return this._mainWorld.addScriptTag(options); url?: string; path?: string;
content?: string;
type?: string;
}): Promise<ElementHandle> {
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<HTMLElement> {
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<ElementHandle> { async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
return this._mainWorld.addStyleTag(options); 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<HTMLElement> {
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<HTMLElement> {
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<string> { async title(): Promise<string> {
return this._mainWorld.title(); const context = await this._mainWorld.executionContext();
return context.evaluate(() => document.title);
} }
name() { name() {