chore: reuse ElementHandle between browsers (#108)

This commit is contained in:
Dmitry Gozman 2019-11-27 16:02:31 -08:00 коммит произвёл GitHub
Родитель b596f36bad
Коммит c3393039b0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 552 добавлений и 874 удалений

Просмотреть файл

@ -18,17 +18,18 @@
import { CDPSession } from './Connection';
import { helper } from '../helper';
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper';
import { createJSHandle, ElementHandle } from './JSHandle';
import { createJSHandle } from './JSHandle';
import { Protocol } from './protocol';
import * as js from '../javascript';
import * as dom from '../dom';
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
export type JSHandle = js.JSHandle<ElementHandle>;
export type ExecutionContext = js.ExecutionContext;
export type JSHandle = js.JSHandle;
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
_client: CDPSession;
_contextId: number;
@ -140,7 +141,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate<Ele
backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(context, object) as ElementHandle;
return createJSHandle(context, object) as dom.ElementHandle;
}
async getProperties(handle: JSHandle): Promise<Map<string, JSHandle>> {

Просмотреть файл

@ -19,10 +19,10 @@ import { EventEmitter } from 'events';
import * as frames from '../frames';
import { assert, debugError } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import { TimeoutSettings } from '../TimeoutSettings';
import { CDPSession } from './Connection';
import { EVALUATION_SCRIPT_URL, ExecutionContext, ExecutionContextDelegate, toRemoteObject } from './ExecutionContext';
import { ElementHandle } from './JSHandle';
import { LifecycleWatcher } from './LifecycleWatcher';
import { NetworkManager, Response } from './NetworkManager';
import { Page } from './Page';
@ -45,9 +45,9 @@ type FrameData = {
lifecycleEvents: Set<string>,
};
export type Frame = frames.Frame<ElementHandle>;
export type Frame = frames.Frame;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
export class FrameManager extends EventEmitter implements frames.FrameDelegate {
_client: CDPSession;
private _page: Page;
private _networkManager: NetworkManager;
@ -183,7 +183,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<E
return this._timeoutSettings;
}
async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle> {
async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise<dom.ElementHandle> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(elementHandle).objectId,
});

Просмотреть файл

@ -15,31 +15,23 @@
* limitations under the License.
*/
import { assert, debugError, helper } from '../helper';
import Injected from '../injected/injected';
import * as input from '../input';
import * as types from '../types';
import { assert, debugError } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import * as input from '../input';
import { CDPSession } from './Connection';
import { Frame } from './FrameManager';
import { FrameManager } from './FrameManager';
import { Protocol } from './protocol';
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
type SelectorRoot = Element | ShadowRoot | Document;
type Point = {
x: number;
y: number;
};
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): JSHandle {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager;
const page = frameManager.page();
const delegate = new ElementHandleDelegate((context._delegate as ExecutionContextDelegate)._client, frameManager);
const handle = new ElementHandle(context, page.keyboard, page.mouse, delegate);
const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._client, frameManager);
const handle = new dom.ElementHandle(context, page.keyboard, page.mouse, delegate);
markJSHandle(handle, remoteObject);
return handle;
}
@ -48,7 +40,7 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol
return handle;
}
class ElementHandleDelegate {
class DOMWorldDelegate implements dom.DOMWorldDelegate {
private _client: CDPSession;
private _frameManager: FrameManager;
@ -57,7 +49,7 @@ class ElementHandleDelegate {
this._frameManager = frameManager;
}
async contentFrame(handle: ElementHandle): Promise<Frame|null> {
async contentFrame(handle: dom.ElementHandle): Promise<Frame|null> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId
});
@ -70,13 +62,13 @@ class ElementHandleDelegate {
return this._frameManager.page()._javascriptEnabled;
}
private _getBoxModel(handle: ElementHandle): Promise<void | Protocol.DOM.getBoxModelReturnValue> {
private _getBoxModel(handle: dom.ElementHandle): Promise<void | Protocol.DOM.getBoxModelReturnValue> {
return this._client.send('DOM.getBoxModel', {
objectId: toRemoteObject(handle).objectId
}).catch(error => debugError(error));
}
async boundingBox(handle: ElementHandle): Promise<{ x: number; y: number; width: number; height: number; } | null> {
async boundingBox(handle: dom.ElementHandle): Promise<dom.Rect | null> {
const result = await this._getBoxModel(handle);
if (!result)
return null;
@ -88,7 +80,7 @@ class ElementHandleDelegate {
return {x, y, width, height};
}
async screenshot(handle: ElementHandle, options: any = {}): Promise<string | Buffer> {
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
let needsViewportReset = false;
let boundingBox = await this.boundingBox(handle);
@ -129,7 +121,7 @@ class ElementHandleDelegate {
return imageData;
}
async ensurePointerActionPoint(handle: ElementHandle, relativePoint?: Point): Promise<Point> {
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
await handle._scrollIntoViewIfNeeded();
if (!relativePoint)
return this._clickablePoint(handle);
@ -150,8 +142,8 @@ class ElementHandleDelegate {
return r.point;
}
private async _clickablePoint(handle: ElementHandle): Promise<Point> {
const fromProtocolQuad = (quad: number[]): Point[] => {
private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
const fromProtocolQuad = (quad: number[]): dom.Point[] => {
return [
{x: quad[0], y: quad[1]},
{x: quad[2], y: quad[3]},
@ -160,14 +152,14 @@ class ElementHandleDelegate {
];
};
const intersectQuadWithViewport = (quad: Point[], width: number, height: number): Point[] => {
const intersectQuadWithViewport = (quad: dom.Point[], width: number, height: number): dom.Point[] => {
return quad.map(point => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
}
};
const computeQuadArea = (quad: Point[]) => {
const computeQuadArea = (quad: dom.Point[]) => {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;
@ -177,7 +169,7 @@ class ElementHandleDelegate {
area += (p1.x * p2.y - p2.x * p1.y) / 2;
}
return Math.abs(area);
}
};
const [result, layoutMetrics] = await Promise.all([
this._client.send('DOM.getContentQuads', {
@ -208,9 +200,9 @@ class ElementHandleDelegate {
};
}
async _viewportPointAndScroll(handle: ElementHandle, relativePoint: Point): Promise<{point: Point, scrollX: number, scrollY: number}> {
async _viewportPointAndScroll(handle: dom.ElementHandle, relativePoint: dom.Point): Promise<{point: dom.Point, scrollX: number, scrollY: number}> {
const model = await this._getBoxModel(handle);
let point: Point;
let point: dom.Point;
if (!model) {
point = relativePoint;
} else {
@ -237,206 +229,8 @@ class ElementHandleDelegate {
scrollY = point.y - metrics.layoutViewport.clientHeight + 1;
return { point, scrollX, scrollY };
}
}
export class ElementHandle extends js.JSHandle<ElementHandle> {
private _delegate: ElementHandleDelegate;
private _keyboard: input.Keyboard;
private _mouse: input.Mouse;
constructor(context: ExecutionContext, keyboard: input.Keyboard, mouse: input.Mouse, delegate: ElementHandleDelegate) {
super(context);
this._delegate = delegate;
this._keyboard = keyboard;
this._mouse = mouse;
}
asElement(): ElementHandle | null {
return this;
}
async contentFrame(): Promise<Frame | null> {
return this._delegate.contentFrame(this);
}
async _scrollIntoViewIfNeeded() {
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}, this._delegate.isJavascriptEnabled());
if (error)
throw new Error(error);
}
async _performPointerAction(action: (point: Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> {
const point = await this._delegate.ensurePointerActionPoint(this, options ? options.relativePoint : undefined);
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._keyboard._ensureModifiers(options.modifiers);
await action(point);
if (restoreModifiers)
await this._keyboard._ensureModifiers(restoreModifiers);
}
hover(options?: input.PointerActionOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.move(point.x, point.y), options);
}
click(options?: input.ClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.click(point.x, point.y, options), options);
}
dblclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.dblclick(point.x, point.y, options), options);
}
tripleclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.tripleclick(point.x, point.y, options), options);
}
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
const options = values.map(value => typeof value === 'object' ? value : { value });
for (const option of options) {
if (option instanceof ElementHandle)
continue;
if (option.value !== undefined)
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
if (option.label !== undefined)
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this.evaluate(input.selectFunction, ...options);
}
async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this.focus();
await this._keyboard.sendCharacters(value);
}
async setInputFiles(...files: (string|input.FilePayload)[]) {
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
await this.evaluate(input.setFileInputFunction, await input.loadFiles(files));
}
async focus() {
await this.evaluate(element => element.focus());
}
async type(text: string, options: { delay: (number | undefined); } | undefined) {
await this.focus();
await this._keyboard.type(text, options);
}
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
await this.focus();
await this._keyboard.press(key, options);
}
async boundingBox(): Promise<{ x: number; y: number; width: number; height: number; } | null> {
return this._delegate.boundingBox(this);
}
async screenshot(options: any = {}): Promise<string | Buffer> {
return this._delegate.screenshot(this, options);
}
async $(selector: string): Promise<ElementHandle | null> {
const handle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
selector, await this._context._injected()
);
const element = handle.asElement();
if (element)
return element;
await handle.dispose();
return null;
}
async $$(selector: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
selector, await this._context._injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await elementHandle.evaluate(pageFunction, ...args as any);
await elementHandle.dispose();
return result;
}
$$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
selector, await this._context._injected()
);
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
await arrayHandle.dispose();
return result;
}
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
expression, await this._context._injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
isIntersectingViewport(): Promise<boolean> {
return this.evaluate(async element => {
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
return visibleRatio > 0;
});
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
await handle.evaluate(input.setFileInputFunction, files);
}
}

Просмотреть файл

@ -22,7 +22,6 @@ import { FrameManager } from './FrameManager';
import { assert, debugError, helper } from '../helper';
import { Protocol } from './protocol';
import * as network from '../network';
import { ElementHandle } from './JSHandle';
export const NetworkManagerEvents = {
Request: Symbol('Events.NetworkManager.Request'),
@ -31,8 +30,8 @@ export const NetworkManagerEvents = {
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
};
export type Request = network.Request<ElementHandle>;
export type Response = network.Response<ElementHandle>;
export type Request = network.Request;
export type Response = network.Response;
export class NetworkManager extends EventEmitter {
private _client: CDPSession;
@ -269,7 +268,7 @@ export class NetworkManager extends EventEmitter {
const interceptableRequestSymbol = Symbol('interceptableRequest');
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
return (request as any)[interceptableRequestSymbol];
}

Просмотреть файл

@ -36,7 +36,7 @@ import { Workers } from './features/workers';
import { Frame } from './FrameManager';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { createJSHandle, ElementHandle } from './JSHandle';
import { createJSHandle } from './JSHandle';
import { JSHandle, toRemoteObject } from './ExecutionContext';
import { NetworkManagerEvents, Response } from './NetworkManager';
import { Protocol } from './protocol';
@ -45,6 +45,7 @@ import { Target } from './Target';
import { TaskQueue } from './TaskQueue';
import * as input from '../input';
import * as types from '../types';
import * as dom from '../dom';
import { ExecutionContextDelegate } from './ExecutionContext';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -224,7 +225,7 @@ export class Page extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector: string): Promise<ElementHandle | null> {
async $(selector: string): Promise<dom.ElementHandle | null> {
return this.mainFrame().$(selector);
}
@ -241,19 +242,19 @@ export class Page extends EventEmitter {
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string): Promise<ElementHandle[]> {
async $$(selector: string): Promise<dom.ElementHandle[]> {
return this.mainFrame().$$(selector);
}
async $x(expression: string): Promise<ElementHandle[]> {
async $x(expression: string): Promise<dom.ElementHandle[]> {
return this.mainFrame().$x(expression);
}
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<ElementHandle> {
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addScriptTag(options);
}
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addStyleTag(options);
}
@ -651,7 +652,7 @@ export class Page extends EventEmitter {
return this.mainFrame().hover(selector, options);
}
select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> {
select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
return this.mainFrame().select(selector, ...values);
}
@ -663,11 +664,11 @@ export class Page extends EventEmitter {
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
}
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForXPath(xpath, options);
}
@ -731,6 +732,6 @@ export class ConsoleMessage {
}
type FileChooser = {
element: ElementHandle,
element: dom.ElementHandle,
multiple: boolean
};

Просмотреть файл

@ -9,6 +9,7 @@ export { Chromium } from './features/chromium';
export { CDPSession } from './Connection';
export { Dialog } from './Dialog';
export { ExecutionContext, JSHandle } from '../javascript';
export { ElementHandle } from '../dom';
export { Accessibility } from './features/accessibility';
export { Coverage } from './features/coverage';
export { Overrides } from './features/overrides';
@ -18,7 +19,6 @@ export { Permissions } from './features/permissions';
export { Worker, Workers } from './features/workers';
export { Frame } from '../frames';
export { Keyboard, Mouse } from '../input';
export { ElementHandle } from './JSHandle';
export { Request, Response } from '../network';
export { ConsoleMessage, Page } from './Page';
export { Playwright } from './Playwright';

Просмотреть файл

@ -16,9 +16,9 @@
*/
import { CDPSession } from '../Connection';
import { ElementHandle } from '../JSHandle';
import { Protocol } from '../protocol';
import { toRemoteObject } from '../ExecutionContext';
import * as dom from '../../dom';
type SerializedAXNode = {
role: string,
@ -64,7 +64,7 @@ export class Accessibility {
async snapshot(options: {
interestingOnly?: boolean;
root?: ElementHandle | null;
root?: dom.ElementHandle | null;
} = {}): Promise<SerializedAXNode> {
const {
interestingOnly = true,

231
src/dom.ts Normal file
Просмотреть файл

@ -0,0 +1,231 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as frames from './frames';
import * as types from './types';
import * as js from './javascript';
import * as input from './input';
import { assert, helper } from './helper';
import Injected from './injected/injected';
export type Rect = { x: number, y: number, width: number, height: number };
export type Point = { x: number, y: number };
type SelectorRoot = Element | ShadowRoot | Document;
export interface DOMWorldDelegate {
isJavascriptEnabled(): boolean;
contentFrame(handle: ElementHandle): Promise<frames.Frame | null>;
boundingBox(handle: ElementHandle): Promise<Rect | null>;
screenshot(handle: ElementHandle, options?: any): Promise<string | Buffer>;
ensurePointerActionPoint(handle: ElementHandle, relativePoint?: Point): Promise<Point>;
setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise<void>;
// await this.evaluate(input.setFileInputFunction, );
}
export class ElementHandle extends js.JSHandle {
private _delegate: DOMWorldDelegate;
private _keyboard: input.Keyboard;
private _mouse: input.Mouse;
constructor(context: js.ExecutionContext, keyboard: input.Keyboard, mouse: input.Mouse, delegate: DOMWorldDelegate) {
super(context);
this._delegate = delegate;
this._keyboard = keyboard;
this._mouse = mouse;
}
asElement(): ElementHandle | null {
return this;
}
async contentFrame(): Promise<frames.Frame | null> {
return this._delegate.contentFrame(this);
}
async _scrollIntoViewIfNeeded() {
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
});
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}, this._delegate.isJavascriptEnabled());
if (error)
throw new Error(error);
}
async _performPointerAction(action: (point: Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> {
const point = await this._delegate.ensurePointerActionPoint(this, options ? options.relativePoint : undefined);
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._keyboard._ensureModifiers(options.modifiers);
await action(point);
if (restoreModifiers)
await this._keyboard._ensureModifiers(restoreModifiers);
}
hover(options?: input.PointerActionOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.move(point.x, point.y), options);
}
click(options?: input.ClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.click(point.x, point.y, options), options);
}
dblclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.dblclick(point.x, point.y, options), options);
}
tripleclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._mouse.tripleclick(point.x, point.y, options), options);
}
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
const options = values.map(value => typeof value === 'object' ? value : { value });
for (const option of options) {
if (option instanceof ElementHandle)
continue;
if (option.value !== undefined)
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
if (option.label !== undefined)
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this.evaluate(input.selectFunction, ...options);
}
async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this.focus();
await this._keyboard.sendCharacters(value);
}
async setInputFiles(...files: (string|input.FilePayload)[]) {
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
await this._delegate.setInputFiles(this, await input.loadFiles(files));
}
async focus() {
await this.evaluate(element => element.focus());
}
async type(text: string, options: { delay: (number | undefined); } | undefined) {
await this.focus();
await this._keyboard.type(text, options);
}
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
await this.focus();
await this._keyboard.press(key, options);
}
async boundingBox(): Promise<Rect | null> {
return this._delegate.boundingBox(this);
}
async screenshot(options: any = {}): Promise<string | Buffer> {
return this._delegate.screenshot(this, options);
}
async $(selector: string): Promise<ElementHandle | null> {
const handle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
selector, await this._context._injected()
);
const element = handle.asElement();
if (element)
return element;
await handle.dispose();
return null;
}
async $$(selector: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
selector, await this._context._injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
$eval: types.$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await elementHandle.evaluate(pageFunction, ...args as any);
await elementHandle.dispose();
return result;
}
$$eval: types.$$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
selector, await this._context._injected()
);
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
await arrayHandle.dispose();
return result;
}
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
expression, await this._context._injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
isIntersectingViewport(): Promise<boolean> {
return this.evaluate(async element => {
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
});
return visibleRatio > 0;
});
}
}

Просмотреть файл

@ -16,14 +16,14 @@
*/
import {helper, debugError} from '../helper';
import { createHandle, ElementHandle } from './JSHandle';
import { createHandle } from './JSHandle';
import * as js from '../javascript';
import { JugglerSession } from './Connection';
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
export type JSHandle = js.JSHandle<ElementHandle>;
export type ExecutionContext = js.ExecutionContext;
export type JSHandle = js.JSHandle;
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
_session: JugglerSession;
_executionContextId: string;

Просмотреть файл

@ -20,10 +20,10 @@ import { TimeoutError } from '../Errors';
import * as frames from '../frames';
import { assert, helper, RegisteredListener } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import { TimeoutSettings } from '../TimeoutSettings';
import { JugglerSession } from './Connection';
import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext';
import { ElementHandle } from './JSHandle';
import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog';
import { Page } from './Page';
@ -42,9 +42,9 @@ type FrameData = {
firedEvents: Set<string>,
};
export type Frame = frames.Frame<ElementHandle>;
export type Frame = frames.Frame;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
export class FrameManager extends EventEmitter implements frames.FrameDelegate {
_session: JugglerSession;
_page: Page;
_networkManager: any;
@ -180,7 +180,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<E
return this._timeoutSettings;
}
async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle> {
async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise<dom.ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return elementHandle;
}

Просмотреть файл

@ -15,70 +15,59 @@
* limitations under the License.
*/
import { assert, debugError, helper } from '../helper';
import Injected from '../injected/injected';
import * as input from '../input';
import * as types from '../types';
import { assert, debugError } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import * as input from '../input';
import { JugglerSession } from './Connection';
import { Frame, FrameManager } from './FrameManager';
import { Page } from './Page';
import { JSHandle, ExecutionContext, markJSHandle, ExecutionContextDelegate } from './ExecutionContext';
import { ExecutionContext, markJSHandle, ExecutionContextDelegate, toPayload } from './ExecutionContext';
type SelectorRoot = Element | ShadowRoot | Document;
class DOMWorldDelegate implements dom.DOMWorldDelegate {
private _session: JugglerSession;
private _frameManager: FrameManager;
private _frameId: string;
export class ElementHandle extends js.JSHandle<ElementHandle> {
_frame: Frame;
_frameId: string;
_page: Page;
_context: ExecutionContext;
protected _session: JugglerSession;
protected _objectId: string;
constructor(frame: Frame, frameId: string, page: Page, session: JugglerSession, context: ExecutionContext, payload: any) {
super(context);
this._frame = frame;
this._frameId = frameId;
this._page = page;
constructor(session: JugglerSession, frameManager: FrameManager, frameId: string) {
this._session = session;
this._objectId = payload.objectId;
markJSHandle(this, payload);
this._frameManager = frameManager;
this._frameId = frameId;
}
async contentFrame(): Promise<Frame | null> {
async contentFrame(handle: dom.ElementHandle): Promise<Frame|null> {
const {frameId} = await this._session.send('Page.contentFrame', {
frameId: this._frameId,
objectId: this._objectId,
objectId: toPayload(handle).objectId,
});
if (!frameId)
return null;
const frame = this._page._frameManager.frame(frameId);
const frame = this._frameManager.frame(frameId);
return frame;
}
asElement(): ElementHandle {
return this;
isJavascriptEnabled(): boolean {
return this._frameManager._page._javascriptEnabled;
}
async boundingBox(): Promise<{ width: number; height: number; x: number; y: number; }> {
async boundingBox(handle: dom.ElementHandle): Promise<dom.Rect | null> {
return await this._session.send('Page.getBoundingBox', {
frameId: this._frameId,
objectId: this._objectId,
objectId: toPayload(handle).objectId,
});
}
async screenshot(options: { encoding?: string; path?: string; } = {}) {
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
const clip = await this._session.send('Page.getBoundingBox', {
frameId: this._frameId,
objectId: this._objectId,
objectId: toPayload(handle).objectId,
});
if (!clip)
throw new Error('Node is either not visible or not an HTMLElement');
assert(clip.width, 'Node has 0 width.');
assert(clip.height, 'Node has 0 height.');
await this._scrollIntoViewIfNeeded();
await handle._scrollIntoViewIfNeeded();
return await this._page.screenshot(Object.assign({}, options, {
return await this._frameManager._page.screenshot(Object.assign({}, options, {
clip: {
x: clip.x,
y: clip.y,
@ -88,182 +77,42 @@ export class ElementHandle extends js.JSHandle<ElementHandle> {
}));
}
isIntersectingViewport(): Promise<boolean> {
return this._frame.evaluate(async (element: Element) => {
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
});
return visibleRatio > 0;
}, this);
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
await handle._scrollIntoViewIfNeeded();
if (!relativePoint)
return this._clickablePoint(handle);
const box = await this.boundingBox(handle);
return { x: box.x + relativePoint.x, y: box.y + relativePoint.y };
}
async $(selector: string): Promise<ElementHandle | null> {
const handle = await this._frame.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
this, selector, await this._context._injected()
);
const element = handle.asElement();
if (element)
return element;
await handle.dispose();
return null;
}
private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
type Quad = {p1: dom.Point, p2: dom.Point, p3: dom.Point, p4: dom.Point};
async $$(selector: string): Promise<ElementHandle[]> {
const arrayHandle = await this._frame.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
this, selector, await this._context._injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
const computeQuadArea = (quad: Quad) => {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;
const points = [quad.p1, quad.p2, quad.p3, quad.p4];
for (let i = 0; i < points.length; ++i) {
const p1 = points[i];
const p2 = points[(i + 1) % points.length];
area += (p1.x * p2.y - p2.x * p1.y) / 2;
}
return Math.abs(area);
};
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await this._frame.evaluate(pageFunction, elementHandle, ...args);
await elementHandle.dispose();
return result;
}
const computeQuadCenter = (quad: Quad) => {
let x = 0, y = 0;
for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
x += point.x;
y += point.y;
}
return {x: x / 4, y: y / 4};
};
$$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const arrayHandle = await this._frame.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
this, selector, await this._context._injected()
);
const result = await this._frame.evaluate(pageFunction, arrayHandle, ...args);
await arrayHandle.dispose();
return result;
}
async $x(expression: string): Promise<Array<ElementHandle>> {
const arrayHandle = await this._frame.evaluateHandle(
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
this, expression, await this._context._injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
async _scrollIntoViewIfNeeded() {
const error = await this._frame.evaluate(async (element: Element) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
});
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: ('instant' as ScrollBehavior)});
return false;
}, this);
if (error)
throw new Error(error);
}
async click(options?: input.ClickOptions) {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
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._page.mouse.dblclick(x, y, options);
}
async tripleclick(options?: input.MultiClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._page.mouse.tripleclick(x, y, options);
}
async setInputFiles(...files: (string|input.FilePayload)[]) {
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
await this.evaluate(input.setFileInputFunction, await input.loadFiles(files));
}
async hover() {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._page.mouse.move(x, y);
}
async focus() {
await this._frame.evaluate(element => element.focus(), this);
}
async type(text: string, options: { delay: (number | undefined); } | undefined) {
await this.focus();
await this._page.keyboard.type(text, options);
}
async press(key: string, options: { delay?: number; } | undefined) {
await this.focus();
await this._page.keyboard.press(key, options);
}
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
const options = values.map(value => typeof value === 'object' ? value : { value });
for (const option of options) {
if (option instanceof ElementHandle)
continue;
if (option.value !== undefined)
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
if (option.label !== undefined)
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this.evaluate(input.selectFunction, ...options);
}
async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this.focus();
await this._page.keyboard.sendCharacters(value);
}
async _clickablePoint(): Promise<{ x: number; y: number; }> {
const result = await this._session.send('Page.getContentQuads', {
frameId: this._frameId,
objectId: this._objectId,
objectId: toPayload(handle).objectId,
}).catch(debugError);
if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
@ -274,6 +123,10 @@ export class ElementHandle extends js.JSHandle<ElementHandle> {
// Return the middle point of the first quad.
return computeQuadCenter(quads[0]);
}
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
await handle.evaluate(input.setFileInputFunction, files);
}
}
export function createHandle(context: ExecutionContext, result: any, exceptionDetails?: any) {
@ -287,39 +140,13 @@ export function createHandle(context: ExecutionContext, result: any, exceptionDe
const frame = context.frame();
const frameManager = frame._delegate as FrameManager;
const frameId = frameManager._frameData(frame).frameId;
const page = frameManager._page;
const session = (context._delegate as ExecutionContextDelegate)._session;
return new ElementHandle(frame, frameId, page, session, context, result);
const delegate = new DOMWorldDelegate(session, frameManager, frameId);
const handle = new dom.ElementHandle(context, frameManager._page.keyboard, frameManager._page.mouse, delegate);
markJSHandle(handle, result);
return handle;
}
const handle = new js.JSHandle(context);
markJSHandle(handle, result);
return handle;
}
function computeQuadArea(quad) {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;
const points = [quad.p1, quad.p2, quad.p3, quad.p4];
for (let i = 0; i < points.length; ++i) {
const p1 = points[i];
const p2 = points[(i + 1) % points.length];
area += (p1.x * p2.y - p2.x * p1.y) / 2;
}
return Math.abs(area);
}
function computeQuadCenter(quad) {
let x = 0, y = 0;
for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
x += point.x;
y += point.y;
}
return {x: x / 4, y: y / 4};
}
type FilePayload = {
name: string,
mimeType: string,
data: string
};

Просмотреть файл

@ -20,10 +20,9 @@ import { assert, debugError, helper, RegisteredListener } from '../helper';
import { JugglerSession } from './Connection';
import { FrameManager, Frame } from './FrameManager';
import * as network from '../network';
import { ElementHandle } from './JSHandle';
export type Request = network.Request<ElementHandle>;
export type Response = network.Response<ElementHandle>;
export type Request = network.Request;
export type Response = network.Response;
export const NetworkManagerEvents = {
RequestFailed: Symbol('NetworkManagerEvents.RequestFailed'),
@ -166,7 +165,7 @@ const causeToResourceType = {
const interceptableRequestSymbol = Symbol('interceptableRequest');
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
return (request as any)[interceptableRequestSymbol];
}

Просмотреть файл

@ -12,11 +12,12 @@ import { Accessibility } from './features/accessibility';
import { Interception } from './features/interception';
import { FrameManager, FrameManagerEvents, normalizeWaitUntil, Frame } from './FrameManager';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { createHandle, ElementHandle } from './JSHandle';
import { createHandle } from './JSHandle';
import { NavigationWatchdog } from './NavigationWatchdog';
import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager';
import * as input from '../input';
import * as types from '../types';
import * as dom from '../dom';
import { JSHandle, toPayload, deserializeValue } from './ExecutionContext';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -33,6 +34,7 @@ export class Page extends EventEmitter {
private _pageBindings: Map<string, Function>;
private _networkManager: NetworkManager;
_frameManager: FrameManager;
_javascriptEnabled = true;
private _eventListeners: RegisteredListener[];
private _viewport: Viewport;
private _disconnectPromise: Promise<Error>;
@ -209,6 +211,7 @@ export class Page extends EventEmitter {
}
async setJavaScriptEnabled(enabled) {
this._javascriptEnabled = enabled;
await this._session.send('Page.setJavascriptEnabled', {enabled});
}
@ -421,11 +424,11 @@ export class Page extends EventEmitter {
return this.mainFrame().evaluate(pageFunction, ...args as any);
}
addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise<ElementHandle> {
addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addScriptTag(options);
}
addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise<ElementHandle> {
addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addStyleTag(options);
}
@ -469,11 +472,11 @@ export class Page extends EventEmitter {
return this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args);
}
waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> {
waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<dom.ElementHandle> {
return this._frameManager.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> {
waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<dom.ElementHandle> {
return this._frameManager.mainFrame().waitForXPath(xpath, options);
}
@ -481,11 +484,11 @@ export class Page extends EventEmitter {
return this._frameManager.mainFrame().title();
}
$(selector: string): Promise<ElementHandle | null> {
$(selector: string): Promise<dom.ElementHandle | null> {
return this._frameManager.mainFrame().$(selector);
}
$$(selector: string): Promise<Array<ElementHandle>> {
$$(selector: string): Promise<Array<dom.ElementHandle>> {
return this._frameManager.mainFrame().$$(selector);
}
@ -497,7 +500,7 @@ export class Page extends EventEmitter {
return this._frameManager.mainFrame().$$eval(selector, pageFunction, ...args as any);
}
$x(expression: string): Promise<Array<ElementHandle>> {
$x(expression: string): Promise<Array<dom.ElementHandle>> {
return this._frameManager.mainFrame().$x(expression);
}
@ -548,7 +551,7 @@ export class Page extends EventEmitter {
if (!this._fileChooserInterceptors.size)
return;
const context = this._frameManager.executionContextById(executionContextId);
const handle = createHandle(context, element) as ElementHandle;
const handle = createHandle(context, element) as dom.ElementHandle;
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
@ -621,6 +624,6 @@ export type Viewport = {
}
type FileChooser = {
element: ElementHandle,
element: dom.ElementHandle,
multiple: boolean
};

Просмотреть файл

@ -10,8 +10,8 @@ export { ExecutionContext, JSHandle } from '../javascript';
export { Accessibility } from './features/accessibility';
export { Interception } from './features/interception';
export { Permissions } from './features/permissions';
export { Frame } from './FrameManager';
export { ElementHandle } from './JSHandle';
export { Frame } from '../frames';
export { ElementHandle } from '../dom';
export { Request, Response } from '../network';
export { ConsoleMessage, Page } from './Page';
export { Playwright } from './Playwright';

Просмотреть файл

@ -18,6 +18,7 @@
import * as types from './types';
import * as fs from 'fs';
import * as js from './javascript';
import * as dom from './dom';
import * as network from './network';
import { helper, assert } from './helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
@ -27,11 +28,11 @@ import { TimeoutSettings } from './TimeoutSettings';
const readFileAsync = helper.promisify(fs.readFile);
type WorldType = 'main' | 'utility';
type World<ElementHandle extends types.ElementHandle<ElementHandle>> = {
contextPromise: Promise<js.ExecutionContext<ElementHandle>>;
contextResolveCallback: (c: js.ExecutionContext<ElementHandle>) => void;
context: js.ExecutionContext<ElementHandle> | null;
waitTasks: Set<WaitTask<ElementHandle>>;
type World = {
contextPromise: Promise<js.ExecutionContext>;
contextResolveCallback: (c: js.ExecutionContext) => void;
context: js.ExecutionContext | null;
waitTasks: Set<WaitTask>;
};
export type NavigateOptions = {
@ -43,24 +44,24 @@ export type GotoOptions = NavigateOptions & {
referer?: string,
};
export interface FrameDelegate<ElementHandle extends types.ElementHandle<ElementHandle>> {
export interface FrameDelegate {
timeoutSettings(): TimeoutSettings;
navigateFrame(frame: Frame<ElementHandle>, url: string, options?: GotoOptions): Promise<network.Response<ElementHandle> | null>;
waitForFrameNavigation(frame: Frame<ElementHandle>, options?: NavigateOptions): Promise<network.Response<ElementHandle> | null>;
setFrameContent(frame: Frame<ElementHandle>, html: string, options?: NavigateOptions): Promise<void>;
adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle>): Promise<ElementHandle>;
navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise<network.Response | null>;
waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise<network.Response | null>;
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise<dom.ElementHandle>;
}
export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
_delegate: FrameDelegate<ElementHandle>;
private _parentFrame: Frame<ElementHandle>;
export class Frame {
_delegate: FrameDelegate;
private _parentFrame: Frame;
private _url = '';
private _detached = false;
private _worlds = new Map<WorldType, World<ElementHandle>>();
private _childFrames = new Set<Frame<ElementHandle>>();
private _worlds = new Map<WorldType, World>();
private _childFrames = new Set<Frame>();
private _name: string;
constructor(delegate: FrameDelegate<ElementHandle>, parentFrame: Frame<ElementHandle> | null) {
constructor(delegate: FrameDelegate, parentFrame: Frame | null) {
this._delegate = delegate;
this._parentFrame = parentFrame;
@ -73,65 +74,65 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
this._parentFrame._childFrames.add(this);
}
goto(url: string, options?: GotoOptions): Promise<network.Response<ElementHandle> | null> {
goto(url: string, options?: GotoOptions): Promise<network.Response | null> {
return this._delegate.navigateFrame(this, url, options);
}
waitForNavigation(options?: NavigateOptions): Promise<network.Response<ElementHandle> | null> {
waitForNavigation(options?: NavigateOptions): Promise<network.Response | null> {
return this._delegate.waitForFrameNavigation(this, options);
}
_mainContext(): Promise<js.ExecutionContext<ElementHandle>> {
_mainContext(): Promise<js.ExecutionContext> {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
return this._worlds.get('main').contextPromise;
}
_utilityContext(): Promise<js.ExecutionContext<ElementHandle>> {
_utilityContext(): Promise<js.ExecutionContext> {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
return this._worlds.get('utility').contextPromise;
}
executionContext(): Promise<js.ExecutionContext<ElementHandle>> {
executionContext(): Promise<js.ExecutionContext> {
return this._mainContext();
}
evaluateHandle: types.EvaluateHandle<js.JSHandle<ElementHandle>> = async (pageFunction, ...args) => {
evaluateHandle: types.EvaluateHandle<js.JSHandle> = async (pageFunction, ...args) => {
const context = await this._mainContext();
return context.evaluateHandle(pageFunction, ...args as any);
}
evaluate: types.Evaluate<js.JSHandle<ElementHandle>> = async (pageFunction, ...args) => {
evaluate: types.Evaluate<js.JSHandle> = async (pageFunction, ...args) => {
const context = await this._mainContext();
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string): Promise<ElementHandle | null> {
async $(selector: string): Promise<dom.ElementHandle | null> {
const context = await this._mainContext();
const document = await context._document();
return document.$(selector);
}
async $x(expression: string): Promise<ElementHandle[]> {
async $x(expression: string): Promise<dom.ElementHandle[]> {
const context = await this._mainContext();
const document = await context._document();
return document.$x(expression);
}
$eval: types.$Eval<js.JSHandle<ElementHandle>> = async (selector, pageFunction, ...args) => {
$eval: types.$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
const context = await this._mainContext();
const document = await context._document();
return document.$eval(selector, pageFunction, ...args as any);
}
$$eval: types.$$Eval<js.JSHandle<ElementHandle>> = async (selector, pageFunction, ...args) => {
$$eval: types.$$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
const context = await this._mainContext();
const document = await context._document();
return document.$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string): Promise<ElementHandle[]> {
async $$(selector: string): Promise<dom.ElementHandle[]> {
const context = await this._mainContext();
const document = await context._document();
return document.$$(selector);
@ -161,11 +162,11 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
return this._url;
}
parentFrame(): Frame<ElementHandle> | null {
parentFrame(): Frame | null {
return this._parentFrame;
}
childFrames(): Frame<ElementHandle>[] {
childFrames(): Frame[] {
return Array.from(this._childFrames);
}
@ -177,7 +178,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
url?: string; path?: string;
content?: string;
type?: string;
}): Promise<ElementHandle> {
}): Promise<dom.ElementHandle> {
const {
url = null,
path = null,
@ -234,7 +235,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
}
}
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
const {
url = null,
path = null,
@ -344,15 +345,15 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
await handle.dispose();
}
async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> {
async select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
const context = await this._utilityContext();
const document = await context._document();
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 => {
if (typeof value === 'object' && (value as any).asElement && (value as any).asElement() === value)
return this._adoptElementHandle(value as ElementHandle, utilityContext, false /* dispose */);
if (value instanceof dom.ElementHandle)
return this._adoptElementHandle(value, utilityContext, false /* dispose */);
return value;
}));
const result = await handle.select(...adoptedValues);
@ -369,7 +370,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
await handle.dispose();
}
waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise<js.JSHandle<ElementHandle> | null> {
waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise<js.JSHandle | null> {
const xPathPattern = '//';
if (helper.isString(selectorOrFunctionOrTimeout)) {
@ -388,7 +389,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
async waitForSelector(selector: string, options: {
visible?: boolean;
hidden?: boolean;
timeout?: number; } | undefined): Promise<ElementHandle | null> {
timeout?: number; } | undefined): Promise<dom.ElementHandle | null> {
const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._delegate.timeoutSettings().timeout(), ...options });
const handle = await this._scheduleWaitTask(params, this._worlds.get('utility'));
if (!handle.asElement()) {
@ -402,7 +403,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
async waitForXPath(xpath: string, options: {
visible?: boolean;
hidden?: boolean;
timeout?: number; } | undefined): Promise<ElementHandle | null> {
timeout?: number; } | undefined): Promise<dom.ElementHandle | null> {
const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._delegate.timeoutSettings().timeout(), ...options });
const handle = await this._scheduleWaitTask(params, this._worlds.get('utility'));
if (!handle.asElement()) {
@ -416,7 +417,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
waitForFunction(
pageFunction: Function | string,
options: { polling?: string | number; timeout?: number; } = {},
...args): Promise<js.JSHandle<ElementHandle>> {
...args): Promise<js.JSHandle> {
const {
polling = 'raf',
timeout = this._delegate.timeoutSettings().timeout(),
@ -452,7 +453,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
this._parentFrame = null;
}
private _scheduleWaitTask(params: WaitTaskParams, world: World<ElementHandle>): Promise<js.JSHandle<ElementHandle>> {
private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise<js.JSHandle> {
const task = new WaitTask(params, () => world.waitTasks.delete(task));
world.waitTasks.add(task);
if (world.context)
@ -460,7 +461,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
return task.promise;
}
private _setContext(worldType: WorldType, context: js.ExecutionContext<ElementHandle> | null) {
private _setContext(worldType: WorldType, context: js.ExecutionContext | null) {
const world = this._worlds.get(worldType);
world.context = context;
if (context) {
@ -474,7 +475,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
}
}
_contextCreated(worldType: WorldType, context: js.ExecutionContext<ElementHandle>) {
_contextCreated(worldType: WorldType, context: js.ExecutionContext) {
const world = this._worlds.get(worldType);
// In case of multiple sessions to the same target, there's a race between
// connections so we might end up creating multiple isolated worlds.
@ -483,14 +484,14 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
this._setContext(worldType, context);
}
_contextDestroyed(context: js.ExecutionContext<ElementHandle>) {
_contextDestroyed(context: js.ExecutionContext) {
for (const [worldType, world] of this._worlds) {
if (world.context === context)
this._setContext(worldType, null);
}
}
private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle>, dispose: boolean): Promise<ElementHandle> {
private async _adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise<dom.ElementHandle> {
if (elementHandle.executionContext() === context)
return elementHandle;
const handle = this._delegate.adoptElementHandle(elementHandle, context);

Просмотреть файл

@ -3,42 +3,43 @@
import * as frames from './frames';
import * as types from './types';
import * as dom from './dom';
import * as injectedSource from './generated/injectedSource';
import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
export interface ExecutionContextDelegate<ElementHandle extends types.ElementHandle<ElementHandle>> {
evaluate(context: ExecutionContext<ElementHandle>, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
getProperties(handle: JSHandle<ElementHandle>): Promise<Map<string, JSHandle<ElementHandle>>>;
releaseHandle(handle: JSHandle<ElementHandle>): Promise<void>;
handleToString(handle: JSHandle<ElementHandle>): string;
handleJSONValue(handle: JSHandle<ElementHandle>): Promise<any>;
export interface ExecutionContextDelegate {
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
releaseHandle(handle: JSHandle): Promise<void>;
handleToString(handle: JSHandle): string;
handleJSONValue(handle: JSHandle): Promise<any>;
}
export class ExecutionContext<ElementHandle extends types.ElementHandle<ElementHandle>> {
_delegate: ExecutionContextDelegate<ElementHandle>;
private _frame: frames.Frame<ElementHandle>;
private _injectedPromise: Promise<JSHandle<ElementHandle>> | null = null;
private _documentPromise: Promise<ElementHandle> | null = null;
export class ExecutionContext {
_delegate: ExecutionContextDelegate;
private _frame: frames.Frame;
private _injectedPromise: Promise<JSHandle> | null = null;
private _documentPromise: Promise<dom.ElementHandle> | null = null;
constructor(delegate: ExecutionContextDelegate<ElementHandle>, frame: frames.Frame<ElementHandle> | null) {
constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) {
this._delegate = delegate;
this._frame = frame;
}
frame(): frames.Frame<ElementHandle> | null {
frame(): frames.Frame | null {
return this._frame;
}
evaluate: types.Evaluate<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
evaluate: types.Evaluate<JSHandle> = (pageFunction, ...args) => {
return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args);
}
evaluateHandle: types.EvaluateHandle<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
evaluateHandle: types.EvaluateHandle<JSHandle> = (pageFunction, ...args) => {
return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args);
}
_injected(): Promise<JSHandle<ElementHandle>> {
_injected(): Promise<JSHandle> {
if (!this._injectedPromise) {
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source];
const source = `
@ -51,34 +52,34 @@ export class ExecutionContext<ElementHandle extends types.ElementHandle<ElementH
return this._injectedPromise;
}
_document(): Promise<ElementHandle> {
_document(): Promise<dom.ElementHandle> {
if (!this._documentPromise)
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
return this._documentPromise;
}
}
export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle>> {
_context: ExecutionContext<ElementHandle>;
export class JSHandle {
_context: ExecutionContext;
_disposed = false;
constructor(context: ExecutionContext<ElementHandle>) {
constructor(context: ExecutionContext) {
this._context = context;
}
executionContext(): ExecutionContext<ElementHandle> {
executionContext(): ExecutionContext {
return this._context;
}
evaluate: types.EvaluateOn<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
evaluate: types.EvaluateOn<JSHandle> = (pageFunction, ...args) => {
return this._context.evaluate(pageFunction, this, ...args);
}
evaluateHandle: types.EvaluateHandleOn<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
evaluateHandle: types.EvaluateHandleOn<JSHandle> = (pageFunction, ...args) => {
return this._context.evaluateHandle(pageFunction, this, ...args);
}
async getProperty(propertyName: string): Promise<JSHandle<ElementHandle> | null> {
async getProperty(propertyName: string): Promise<JSHandle | null> {
const objectHandle = await this.evaluateHandle((object, propertyName) => {
const result = {__proto__: null};
result[propertyName] = object[propertyName];
@ -90,7 +91,7 @@ export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle>>
return result;
}
getProperties(): Promise<Map<string, JSHandle<ElementHandle>>> {
getProperties(): Promise<Map<string, JSHandle>> {
return this._context._delegate.getProperties(this);
}
@ -98,7 +99,7 @@ export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle>>
return this._context._delegate.handleJSONValue(this);
}
asElement(): ElementHandle | null {
asElement(): dom.ElementHandle | null {
return null;
}

Просмотреть файл

@ -50,9 +50,9 @@ export function filterCookies(cookies: NetworkCookie[], urls: string[]) {
export type Headers = { [key: string]: string };
export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
_response: Response<ElementHandle> | null = null;
_redirectChain: Request<ElementHandle>[];
export class Request {
_response: Response | null = null;
_redirectChain: Request[];
private _isNavigationRequest: boolean;
private _failureText: string | null = null;
private _url: string;
@ -60,9 +60,9 @@ export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
private _method: string;
private _postData: string;
private _headers: Headers;
private _frame: frames.Frame<ElementHandle>;
private _frame: frames.Frame;
constructor(frame: frames.Frame<ElementHandle> | null, redirectChain: Request<ElementHandle>[], isNavigationRequest: boolean,
constructor(frame: frames.Frame | null, redirectChain: Request[], isNavigationRequest: boolean,
url: string, resourceType: string, method: string, postData: string, headers: Headers) {
this._frame = frame;
this._redirectChain = redirectChain;
@ -98,11 +98,11 @@ export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
return this._headers;
}
response(): Response<ElementHandle> | null {
response(): Response | null {
return this._response;
}
frame(): frames.Frame<ElementHandle> | null {
frame(): frames.Frame | null {
return this._frame;
}
@ -110,7 +110,7 @@ export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
return this._isNavigationRequest;
}
redirectChain(): Request<ElementHandle>[] {
redirectChain(): Request[] {
return this._redirectChain.slice();
}
@ -130,8 +130,8 @@ export type RemoteAddress = {
type GetResponseBodyCallback = () => Promise<Buffer>;
export class Response<ElementHandle extends types.ElementHandle<ElementHandle>> {
private _request: Request<ElementHandle>;
export class Response {
private _request: Request;
private _contentPromise: Promise<Buffer> | null = null;
private _bodyLoadedPromise: Promise<Error | null>;
private _bodyLoadedPromiseFulfill: any;
@ -142,7 +142,7 @@ export class Response<ElementHandle extends types.ElementHandle<ElementHandle>>
private _headers: Headers;
private _getResponseBodyCallback: GetResponseBodyCallback;
constructor(request: Request<ElementHandle>, status: number, statusText: string, headers: Headers, remoteAddress: RemoteAddress, getResponseBodyCallback: GetResponseBodyCallback) {
constructor(request: Request, status: number, statusText: string, headers: Headers, remoteAddress: RemoteAddress, getResponseBodyCallback: GetResponseBodyCallback) {
this._request = request;
this._request._response = this;
this._status = status;
@ -205,11 +205,11 @@ export class Response<ElementHandle extends types.ElementHandle<ElementHandle>>
return JSON.parse(content);
}
request(): Request<ElementHandle> {
request(): Request {
return this._request;
}
frame(): frames.Frame<ElementHandle> | null {
frame(): frames.Frame | null {
return this._request.frame();
}
}

Просмотреть файл

@ -1,9 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as input from './input';
import * as js from './javascript';
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>);
@ -14,19 +11,3 @@ export type $Eval<Handle> = <Args extends any[], R>(selector: string, pageFuncti
export type $$Eval<Handle> = <Args extends any[], R>(selector: string, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args, Handle>) => Promise<R>;
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 ElementHandle<EHandle extends ElementHandle<EHandle>> extends js.JSHandle<EHandle> {
$(selector: string): Promise<EHandle | null>;
$x(expression: string): Promise<EHandle[]>;
$$(selector: string): Promise<EHandle[]>;
$eval: $Eval<js.JSHandle<EHandle>>;
$$eval: $$Eval<js.JSHandle<EHandle>>;
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>;
}

Просмотреть файл

@ -2,7 +2,6 @@
// Licensed under the MIT license.
import { assert, helper } from './helper';
import * as types from './types';
import * as js from './javascript';
import { TimeoutError } from './Errors';
@ -15,12 +14,12 @@ export type WaitTaskParams = {
args: any[];
};
export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle>> {
readonly promise: Promise<js.JSHandle<ElementHandle>>;
export class WaitTask {
readonly promise: Promise<js.JSHandle>;
private _cleanup: () => void;
private _params: WaitTaskParams & { predicateBody: string };
private _runCount: number;
private _resolve: (result: js.JSHandle<ElementHandle>) => void;
private _resolve: (result: js.JSHandle) => void;
private _reject: (reason: Error) => void;
private _timeoutTimer: NodeJS.Timer;
private _terminated: boolean;
@ -39,7 +38,7 @@ export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle>>
};
this._cleanup = cleanup;
this._runCount = 0;
this.promise = new Promise<js.JSHandle<ElementHandle>>((resolve, reject) => {
this.promise = new Promise<js.JSHandle>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
@ -57,9 +56,9 @@ export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle>>
this._doCleanup();
}
async rerun(context: js.ExecutionContext<ElementHandle>) {
async rerun(context: js.ExecutionContext) {
const runCount = ++this._runCount;
let success: js.JSHandle<ElementHandle> | null = null;
let success: js.JSHandle | null = null;
let error = null;
try {
success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args);

Просмотреть файл

@ -18,17 +18,17 @@
import { TargetSession } from './Connection';
import { helper } from '../helper';
import { valueFromRemoteObject, releaseObject } from './protocolHelper';
import { createJSHandle, ElementHandle } from './JSHandle';
import { createJSHandle } from './JSHandle';
import { Protocol } from './protocol';
import * as js from '../javascript';
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
export type JSHandle = js.JSHandle<ElementHandle>;
export type ExecutionContext = js.ExecutionContext;
export type JSHandle = js.JSHandle;
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
private _globalObjectId?: string;
_session: TargetSession;
_contextId: number;

Просмотреть файл

@ -20,11 +20,11 @@ import { TimeoutError } from '../Errors';
import * as frames from '../frames';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import { TimeoutSettings } from '../TimeoutSettings';
import { TargetSession } from './Connection';
import { Events } from './events';
import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext';
import { ElementHandle } from './JSHandle';
import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager';
import { Page } from './Page';
import { Protocol } from './protocol';
@ -42,9 +42,9 @@ type FrameData = {
id: string,
};
export type Frame = frames.Frame<ElementHandle>;
export type Frame = frames.Frame;
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
export class FrameManager extends EventEmitter implements frames.FrameDelegate {
_session: TargetSession;
_page: Page;
_networkManager: NetworkManager;
@ -277,7 +277,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<E
return watchDog.waitForNavigation();
}
async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle> {
async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise<dom.ElementHandle> {
assert(false, 'Multiple isolated worlds are not implemented');
return elementHandle;
}

Просмотреть файл

@ -16,90 +16,113 @@
*/
import * as fs from 'fs';
import { assert, debugError, helper } from '../helper';
import { debugError, helper } from '../helper';
import * as input from '../input';
import * as dom from '../dom';
import * as frames from '../frames';
import { TargetSession } from './Connection';
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext';
import { ExecutionContext, ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
import { FrameManager } from './FrameManager';
import { Page } from './Page';
import { Protocol } from './protocol';
import Injected from '../injected/injected';
import * as types from '../types';
import * as js from '../javascript';
type SelectorRoot = Element | ShadowRoot | Document;
const writeFileAsync = helper.promisify(fs.writeFile);
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
const delegate = context._delegate as ExecutionContextDelegate;
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager;
return new ElementHandle(context, delegate._session, remoteObject, frameManager.page(), frameManager);
const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._session, frameManager);
return new dom.ElementHandle(context, frameManager.page().keyboard, frameManager.page().mouse, delegate);
}
const handle = new js.JSHandle(context);
markJSHandle(handle, remoteObject);
return handle;
}
export class ElementHandle extends js.JSHandle<ElementHandle> {
class DOMWorldDelegate implements dom.DOMWorldDelegate {
private _client: TargetSession;
private _remoteObject: Protocol.Runtime.RemoteObject;
private _page: Page;
private _frameManager: FrameManager;
constructor(context: ExecutionContext, client: TargetSession, remoteObject: Protocol.Runtime.RemoteObject, page: Page, frameManager: FrameManager) {
super(context);
constructor(client: TargetSession, frameManager: FrameManager) {
this._client = client;
this._remoteObject = remoteObject;
this._page = page;
this._frameManager = frameManager;
markJSHandle(this, remoteObject);
}
asElement(): ElementHandle | null {
return this;
async contentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
throw new Error('contentFrame() is not implemented');
}
async _scrollIntoViewIfNeeded() {
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
isJavascriptEnabled(): boolean {
return this._frameManager.page()._javascriptEnabled;
}
async boundingBox(handle: dom.ElementHandle): Promise<dom.Rect | null> {
throw new Error('boundingBox() is not implemented');
}
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
const objectId = toRemoteObject(handle).objectId;
this._client.send('DOM.getDocument');
const {nodeId} = await this._client.send('DOM.requestNode', {objectId});
const result = await this._client.send('Page.snapshotNode', {nodeId});
const prefix = 'data:image/png;base64,';
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
if (options.path)
await writeFileAsync(options.path, buffer);
return buffer;
}
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
await handle._scrollIntoViewIfNeeded();
if (!relativePoint)
return this._clickablePoint(handle);
const box = await this.boundingBox(handle);
return { x: box.x + relativePoint.x, y: box.y + relativePoint.y };
}
private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
const fromProtocolQuad = (quad: number[]): dom.Point[] => {
return [
{x: quad[0], y: quad[1]},
{x: quad[2], y: quad[3]},
{x: quad[4], y: quad[5]},
{x: quad[6], y: quad[7]}
];
};
const intersectQuadWithViewport = (quad: dom.Point[], width: number, height: number): dom.Point[] => {
return quad.map(point => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
};
const computeQuadArea = (quad: dom.Point[]) => {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;
for (let i = 0; i < quad.length; ++i) {
const p1 = quad[i];
const p2 = quad[(i + 1) % quad.length];
area += (p1.x * p2.y - p2.x * p1.y) / 2;
}
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}, this._page._javascriptEnabled);
if (error)
throw new Error(error);
}
return Math.abs(area);
};
async _clickablePoint() {
const [result, viewport] = await Promise.all([
this._client.send('DOM.getContentQuads', {
objectId: this._remoteObject.objectId
objectId: toRemoteObject(handle).objectId
}).catch(debugError),
this._page.evaluate(() => ({ clientWidth: innerWidth, clientHeight: innerHeight })),
handle.evaluate(() => ({ clientWidth: innerWidth, clientHeight: innerHeight })),
]);
if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Filter out quads that have too small area to click into.
const {clientWidth, clientHeight} = viewport;
const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).map(quad => this._intersectQuadWithViewport(quad, clientWidth, clientHeight)).filter(quad => computeQuadArea(quad) > 1);
const quads = result.quads.map(fromProtocolQuad)
.map(quad => intersectQuadWithViewport(quad, clientWidth, clientHeight))
.filter(quad => computeQuadArea(quad) > 1);
if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad.
@ -116,190 +139,8 @@ export class ElementHandle extends js.JSHandle<ElementHandle> {
};
}
_fromProtocolQuad(quad: number[]): Array<{ x: number; y: number; }> {
return [
{x: quad[0], y: quad[1]},
{x: quad[2], y: quad[3]},
{x: quad[4], y: quad[5]},
{x: quad[6], y: quad[7]}
];
}
_intersectQuadWithViewport(quad: Array<{ x: number; y: number; }>, width: number, height: number): Array<{ x: number; y: number; }> {
return quad.map(point => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
}
async hover(): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._page.mouse.move(x, y);
}
async click(options?: input.ClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
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._page.mouse.dblclick(x, y, options);
}
async tripleclick(options?: input.MultiClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._page.mouse.tripleclick(x, y, options);
}
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
const options = values.map(value => typeof value === 'object' ? value : { value });
for (const option of options) {
if (option instanceof ElementHandle)
continue;
if (option.value !== undefined)
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
if (option.label !== undefined)
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this.evaluate(input.selectFunction, ...options);
}
async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this.focus();
await this._page.keyboard.sendCharacters(value);
}
async setInputFiles(...files: (string|input.FilePayload)[]) {
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
const filePayloads = await input.loadFiles(files);
const objectId = this._remoteObject.objectId;
await this._client.send('DOM.setInputFiles', { objectId, files: filePayloads });
}
async focus() {
await this.evaluate(element => element.focus());
}
async type(text: string, options: { delay: (number | undefined); } | undefined) {
await this.focus();
await this._page.keyboard.type(text, options);
}
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
await this.focus();
await this._page.keyboard.press(key, options);
}
async screenshot(options: {path?: string} = {}): Promise<string | Buffer> {
const objectId = this._remoteObject.objectId;
this._client.send('DOM.getDocument');
const {nodeId} = await this._client.send('DOM.requestNode', {objectId});
const result = await this._client.send('Page.snapshotNode', {nodeId});
const prefix = 'data:image/png;base64,';
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
if (options.path)
await writeFileAsync(options.path, buffer);
return buffer;
}
async $(selector: string): Promise<ElementHandle | null> {
const handle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
selector, await this._context._injected()
);
const element = handle.asElement();
if (element)
return element;
await handle.dispose();
return null;
}
async $$(selector: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(element, selector) => element.querySelectorAll(selector),
selector
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await elementHandle.evaluate(pageFunction, ...args as any);
await elementHandle.dispose();
return result;
}
$$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
selector, await this._context._injected()
);
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
await arrayHandle.dispose();
return result;
}
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
expression, await this._context._injected()
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
isIntersectingViewport(): Promise<boolean> {
return this.evaluate(async element => {
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
return visibleRatio > 0;
});
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
const objectId = toRemoteObject(handle);
await this._client.send('DOM.setInputFiles', { objectId, files });
}
}
function computeQuadArea(quad) {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;
for (let i = 0; i < quad.length; ++i) {
const p1 = quad[i];
const p2 = quad[(i + 1) % quad.length];
area += (p1.x * p2.y - p2.x * p1.y) / 2;
}
return Math.abs(area);
}

Просмотреть файл

@ -21,7 +21,6 @@ import { Frame, FrameManager } from './FrameManager';
import { assert, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol';
import * as network from '../network';
import { ElementHandle } from './JSHandle';
export const NetworkManagerEvents = {
Request: Symbol('Events.NetworkManager.Request'),
@ -30,8 +29,8 @@ export const NetworkManagerEvents = {
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
};
export type Request = network.Request<ElementHandle>;
export type Response = network.Response<ElementHandle>;
export type Request = network.Request;
export type Response = network.Response;
export class NetworkManager extends EventEmitter {
private _sesssion: TargetSession;
@ -171,7 +170,7 @@ export class NetworkManager extends EventEmitter {
const interceptableRequestSymbol = Symbol('interceptableRequest');
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
return (request as any)[interceptableRequestSymbol];
}

Просмотреть файл

@ -26,7 +26,7 @@ import { TargetSession, TargetSessionEvents } from './Connection';
import { Events } from './events';
import { Frame, FrameManager, FrameManagerEvents } from './FrameManager';
import { RawKeyboardImpl, RawMouseImpl } from './Input';
import { createJSHandle, ElementHandle } from './JSHandle';
import { createJSHandle } from './JSHandle';
import { JSHandle, toRemoteObject } from './ExecutionContext';
import { NetworkManagerEvents, Response } from './NetworkManager';
import { Protocol } from './protocol';
@ -35,6 +35,7 @@ import { Target } from './Target';
import { TaskQueue } from './TaskQueue';
import * as input from '../input';
import * as types from '../types';
import * as dom from '../dom';
import { Dialog, DialogType } from './Dialog';
const writeFileAsync = helper.promisify(fs.writeFile);
@ -219,7 +220,7 @@ export class Page extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector: string): Promise<ElementHandle | null> {
async $(selector: string): Promise<dom.ElementHandle | null> {
return this.mainFrame().$(selector);
}
@ -236,19 +237,19 @@ export class Page extends EventEmitter {
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string): Promise<ElementHandle[]> {
async $$(selector: string): Promise<dom.ElementHandle[]> {
return this.mainFrame().$$(selector);
}
async $x(expression: string): Promise<ElementHandle[]> {
async $x(expression: string): Promise<dom.ElementHandle[]> {
return this.mainFrame().$x(expression);
}
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<ElementHandle> {
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addScriptTag(options);
}
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addStyleTag(options);
}
@ -458,7 +459,7 @@ export class Page extends EventEmitter {
if (!this._fileChooserInterceptors.size)
return;
const context = await this._frameManager.frame(event.frameId)._utilityContext();
const handle = createJSHandle(context, event.element) as ElementHandle;
const handle = createJSHandle(context, event.element) as dom.ElementHandle;
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
@ -508,11 +509,11 @@ export class Page extends EventEmitter {
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
}
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForXPath(xpath, options);
}
@ -588,6 +589,6 @@ export class ConsoleMessage {
}
type FileChooser = {
element: ElementHandle,
element: dom.ElementHandle,
multiple: boolean
};

Просмотреть файл

@ -5,9 +5,9 @@ export { TimeoutError } from '../Errors';
export { Browser, BrowserContext } from './Browser';
export { BrowserFetcher } from './BrowserFetcher';
export { ExecutionContext, JSHandle } from '../javascript';
export { Frame } from './FrameManager';
export { Frame } from '../frames';
export { Mouse, Keyboard } from '../input';
export { ElementHandle } from './JSHandle';
export { ElementHandle } from '../dom';
export { Request, Response } from '../network';
export { ConsoleMessage, Page } from './Page';
export { Playwright } from './Playwright';