chore: reuse Frame between browsers (#100)

This commit is contained in:
Dmitry Gozman 2019-11-27 12:38:26 -08:00 committed by GitHub
parent 49f8963bf1
commit dfc5592910
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 266 additions and 1037 deletions

View file

@ -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<JSHandle> {
export class ExecutionContext {
_client: CDPSession;
private _frame: Frame;
private _injectedPromise: Promise<JSHandle> | null = null;

View file

@ -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<string>,
};
export class FrameManager extends EventEmitter implements FrameDelegate {
export type Frame = frames.Frame<JSHandle, ElementHandle, ExecutionContext, Response>;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response> {
_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: '',

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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<WaitTask<JSHandle>>;
_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<ExecutionContext> {
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<JSHandle> {
const task = new WaitTask(params, () => this._waitTasks.delete(task));
this._waitTasks.add(task);
if (this._context)
task.rerun(this._context);
return task.promise;
}
}

View file

@ -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<JSHandle> {
export class ExecutionContext {
_session: any;
_frame: Frame;
_executionContextId: string;

View file

@ -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<string>,
};
export type Frame = frames.Frame<JSHandle, ElementHandle, ExecutionContext, Response>;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response> {
_session: JugglerSession;
_page: Page;
_networkManager: any;
@ -49,6 +52,7 @@ export class FrameManager extends EventEmitter {
_frames: Map<string, Frame>;
_contextIdToContext: Map<string, ExecutionContext>;
_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<Frame>;
private _detached: boolean;
_firedEvents: Set<string>;
_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<ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return elementHandle;
}
async waitForNavigation(options: { timeout?: number; waitUntil?: string | Array<string>; } = {}) {
async waitForFrameNavigation(frame: Frame, options: { timeout?: number; waitUntil?: string | Array<string>; } = {}) {
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<string>; referer?: string; } = {}) {
async navigateFrame(frame: Frame, url: string, options: { timeout?: number; waitUntil?: string | Array<string>; 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<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);
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<any>): Promise<JSHandle> {
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<JSHandle> {
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<ElementHandle | null> {
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<ElementHandle | null> {
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<string> {
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<JSHandle> = async (pageFunction, ...args) => {
const context = await this._mainWorld.executionContext();
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string): Promise<ElementHandle | null> {
const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$(selector);
}
async $$(selector: string): Promise<Array<ElementHandle>> {
const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$$(selector);
}
$eval: types.$Eval<JSHandle> = 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<JSHandle> = 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<Array<ElementHandle>> {
const context = await this._mainWorld.executionContext();
const document = await context._document();
return document.$x(expression);
}
evaluateHandle: types.EvaluateHandle<JSHandle> = 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<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: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
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> {
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) {

View file

@ -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<Frame | null> {
@ -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<void> {
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<void> {
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<string[]> {
@ -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) {

View file

@ -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<unknown>;
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));
}

View file

@ -31,7 +31,7 @@ export class Page extends EventEmitter {
private _closed: boolean;
private _pageBindings: Map<string, Function>;
private _networkManager: NetworkManager;
private _frameManager: FrameManager;
_frameManager: FrameManager;
private _eventListeners: RegisteredListener[];
private _viewport: Viewport;
private _disconnectPromise: Promise<Error>;
@ -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(),

View file

@ -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<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>, ExecutionContext> = {
contextPromise: Promise<ExecutionContext>;
contextResolveCallback: (c: ExecutionContext) => void;
context: ExecutionContext | null;
waitTasks: Set<WaitTask<JSHandle>>;
waitTasks: Set<WaitTask<JSHandle, ElementHandle>>;
};
export type NavigateOptions = {
@ -44,24 +41,24 @@ export type GotoOptions = NavigateOptions & {
referer?: string,
};
export interface FrameDelegate {
export interface FrameDelegate<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>, ExecutionContext extends types.ExecutionContext<JSHandle, ElementHandle>, Response> {
timeoutSettings(): TimeoutSettings;
navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise<Response | null>;
waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise<Response | null>;
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
navigateFrame(frame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>, url: string, options?: GotoOptions): Promise<Response | null>;
waitForFrameNavigation(frame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>, options?: NavigateOptions): Promise<Response | null>;
setFrameContent(frame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>, html: string, options?: NavigateOptions): Promise<void>;
adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle>;
}
export class Frame {
_delegate: FrameDelegate;
private _parentFrame: Frame;
export class Frame<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>, ExecutionContext extends types.ExecutionContext<JSHandle, ElementHandle>, Response> {
_delegate: FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response>;
private _parentFrame: Frame<JSHandle, ElementHandle, ExecutionContext, Response>;
private _url = '';
private _detached = false;
private _worlds = new Map<WorldType, World>();
private _childFrames = new Set<Frame>();
private _worlds = new Map<WorldType, World<JSHandle, ElementHandle, ExecutionContext>>();
private _childFrames = new Set<Frame<JSHandle, ElementHandle, ExecutionContext, Response>>();
private _name: string;
constructor(delegate: FrameDelegate, parentFrame: Frame | null) {
constructor(delegate: FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response>, parentFrame: Frame<JSHandle, ElementHandle, ExecutionContext, Response> | null) {
this._delegate = delegate;
this._parentFrame = parentFrame;
@ -162,11 +159,11 @@ export class Frame {
return this._url;
}
parentFrame(): Frame | null {
parentFrame(): Frame<JSHandle, ElementHandle, ExecutionContext, Response> | null {
return this._parentFrame;
}
childFrames(): Frame[] {
childFrames(): Frame<JSHandle, ElementHandle, ExecutionContext, Response>[] {
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<JSHandle> {
private _scheduleWaitTask(params: WaitTaskParams, world: World<JSHandle, ElementHandle, ExecutionContext>): Promise<JSHandle> {
const task = new WaitTask(params, () => world.waitTasks.delete(task));
world.waitTasks.add(task);
if (world.context)

View file

@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as input from './input';
type Boxed<Args extends any[], Handle> = { [Index in keyof Args]: Args[Index] | Handle };
type PageFunction<Args extends any[], R = any> = string | ((...args: Args) => R | Promise<R>);
type PageFunctionOn<On, Args extends any[], R = any> = string | ((on: On, ...args: Args) => R | Promise<R>);
@ -12,11 +14,30 @@ export type $$Eval<Handle> = <Args extends any[], R>(selector: string, pageFunct
export type EvaluateOn<Handle> = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args, Handle>) => Promise<R>;
export type EvaluateHandleOn<Handle> = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args, Handle>) => Promise<Handle>;
export interface EvaluationContext<Handle> {
export interface ExecutionContext<Handle extends JSHandle<Handle, EHandle>, EHandle extends ElementHandle<Handle, EHandle>> {
evaluate: Evaluate<Handle>;
evaluateHandle: EvaluateHandle<Handle>;
_document(): Promise<EHandle>;
}
export interface Handle {
export interface JSHandle<Handle extends JSHandle<Handle, EHandle>, EHandle extends ElementHandle<Handle, EHandle>> {
executionContext(): ExecutionContext<Handle, EHandle>;
dispose(): Promise<void>;
asElement(): EHandle | null;
}
export interface ElementHandle<Handle extends JSHandle<Handle, EHandle>, EHandle extends ElementHandle<Handle, EHandle>> extends JSHandle<Handle, EHandle> {
$(selector: string): Promise<EHandle | null>;
$x(expression: string): Promise<EHandle[]>;
$$(selector: string): Promise<EHandle[]>;
$eval: $Eval<Handle>;
$$eval: $$Eval<Handle>;
click(options?: input.ClickOptions): Promise<void>;
dblclick(options?: input.MultiClickOptions): Promise<void>;
tripleclick(options?: input.MultiClickOptions): Promise<void>;
fill(value: string): Promise<void>;
focus(): Promise<void>;
hover(options?: input.PointerActionOptions): Promise<void>;
select(...values: (string | EHandle | input.SelectOption)[]): Promise<string[]>;
type(text: string, options: { delay: (number | undefined); } | undefined): Promise<void>;
}

View file

@ -14,12 +14,12 @@ export type WaitTaskParams = {
args: any[];
};
export class WaitTask<Handle extends types.Handle> {
readonly promise: Promise<Handle>;
export class WaitTask<JSHandle extends types.JSHandle<JSHandle, ElementHandle>, ElementHandle extends types.ElementHandle<JSHandle, ElementHandle>> {
readonly promise: Promise<JSHandle>;
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<Handle extends types.Handle> {
};
this._cleanup = cleanup;
this._runCount = 0;
this.promise = new Promise<Handle>((resolve, reject) => {
this.promise = new Promise<JSHandle>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
@ -56,9 +56,9 @@ export class WaitTask<Handle extends types.Handle> {
this._doCleanup();
}
async rerun(context: types.EvaluationContext<Handle>) {
async rerun(context: types.ExecutionContext<JSHandle, ElementHandle>) {
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);

View file

@ -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<JSHandle> {
export class ExecutionContext {
_globalObjectId?: string;
_session: TargetSession;
_frame: Frame;
@ -37,6 +37,7 @@ export class ExecutionContext implements types.EvaluationContext<JSHandle> {
private _contextDestroyedCallback: any;
private _executionContextDestroyedPromise: Promise<unknown>;
private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<ElementHandle> | 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<JSHandle> {
}
return this._injectedPromise;
}
_document(): Promise<ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
}

View file

@ -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<JSHandle, ElementHandle, ExecutionContext, Response>;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<JSHandle, ElementHandle, ExecutionContext, Response> {
_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<string>;
_waitTasks: Set<WaitTask<JSHandle>>;
_executionContext: ExecutionContext | null;
_contextPromise: Promise<ExecutionContext>;
_contextResolveCallback: (arg: ExecutionContext) => void;
_childFrames: Set<Frame>;
_documentPromise: Promise<ElementHandle>;
_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<string>} */
this._lifecycleEvents = new Set();
/** @type {!Set<!WaitTask>} */
this._waitTasks = new Set();
this._executionContext = null;
this._contextPromise = null;
this._contextResolveCallback = null;
this._setContext(null);
/** @type {!Set<!Frame>} */
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<string>; } | undefined = {}): Promise<Response | null> {
async navigateFrame(frame: Frame, url: string, options: { referer?: string; timeout?: number; waitUntil?: string | Array<string>; } | undefined = {}): Promise<Response | null> {
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<Response | null> {
async waitForFrameNavigation(frame: Frame, options?: frames.NavigateOptions): Promise<Response | null> {
// 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<ElementHandle | null> {
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<ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return elementHandle;
}
async waitForXPath(xpath: string, options: { visible?: boolean, hidden?: boolean, timeout?: number } = {}): Promise<ElementHandle | null> {
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<JSHandle> {
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<ExecutionContext> {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame`);
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;
}
_document(): Promise<ElementHandle> {
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<Array<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<Array<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 | Array<string>; } | undefined = {}) {
async setFrameContent(frame: Frame, html: string, options: { timeout?: number; waitUntil?: string | Array<string>; } | 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<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;
}
}
name(): string {
return this._name || '';
}
url(): string {
return this._url;
}
parentFrame(): Frame | null {
return this._parentFrame;
}
childFrames(): Array<Frame> {
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<string[]> {
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<any>): Promise<JSHandle | null> {
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<string> {
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<JSHandle> {
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<unknown>;
_newDocumentNavigationCallback: (value?: unknown) => void;
_sameDocumentNavigationPromise: Promise<unknown>;
@ -689,7 +303,8 @@ class NextNavigationWatchdog {
_timeoutPromise: Promise<unknown>;
_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();

View file

@ -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);