chore: reuse ElementHandle between browsers (#108)

This commit is contained in:
Dmitry Gozman 2019-11-27 16:02:31 -08:00 committed by GitHub
parent b596f36bad
commit c3393039b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 552 additions and 874 deletions

View file

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

View file

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

View file

@ -15,31 +15,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert, debugError, helper } from '../helper'; import { assert, debugError } from '../helper';
import Injected from '../injected/injected';
import * as input from '../input';
import * as types from '../types';
import * as js from '../javascript'; import * as js from '../javascript';
import * as dom from '../dom';
import * as input from '../input';
import { CDPSession } from './Connection'; import { CDPSession } from './Connection';
import { Frame } from './FrameManager'; import { Frame } from './FrameManager';
import { FrameManager } from './FrameManager'; import { FrameManager } from './FrameManager';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext'; 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 { export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): JSHandle {
const frame = context.frame(); const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) { if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager; const frameManager = frame._delegate as FrameManager;
const page = frameManager.page(); const page = frameManager.page();
const delegate = new ElementHandleDelegate((context._delegate as ExecutionContextDelegate)._client, frameManager); const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._client, frameManager);
const handle = new ElementHandle(context, page.keyboard, page.mouse, delegate); const handle = new dom.ElementHandle(context, page.keyboard, page.mouse, delegate);
markJSHandle(handle, remoteObject); markJSHandle(handle, remoteObject);
return handle; return handle;
} }
@ -48,7 +40,7 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol
return handle; return handle;
} }
class ElementHandleDelegate { class DOMWorldDelegate implements dom.DOMWorldDelegate {
private _client: CDPSession; private _client: CDPSession;
private _frameManager: FrameManager; private _frameManager: FrameManager;
@ -57,7 +49,7 @@ class ElementHandleDelegate {
this._frameManager = frameManager; 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', { const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId objectId: toRemoteObject(handle).objectId
}); });
@ -70,13 +62,13 @@ class ElementHandleDelegate {
return this._frameManager.page()._javascriptEnabled; 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', { return this._client.send('DOM.getBoxModel', {
objectId: toRemoteObject(handle).objectId objectId: toRemoteObject(handle).objectId
}).catch(error => debugError(error)); }).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); const result = await this._getBoxModel(handle);
if (!result) if (!result)
return null; return null;
@ -88,7 +80,7 @@ class ElementHandleDelegate {
return {x, y, width, height}; 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 needsViewportReset = false;
let boundingBox = await this.boundingBox(handle); let boundingBox = await this.boundingBox(handle);
@ -129,7 +121,7 @@ class ElementHandleDelegate {
return imageData; return imageData;
} }
async ensurePointerActionPoint(handle: ElementHandle, relativePoint?: Point): Promise<Point> { async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
await handle._scrollIntoViewIfNeeded(); await handle._scrollIntoViewIfNeeded();
if (!relativePoint) if (!relativePoint)
return this._clickablePoint(handle); return this._clickablePoint(handle);
@ -150,8 +142,8 @@ class ElementHandleDelegate {
return r.point; return r.point;
} }
private async _clickablePoint(handle: ElementHandle): Promise<Point> { private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
const fromProtocolQuad = (quad: number[]): Point[] => { const fromProtocolQuad = (quad: number[]): dom.Point[] => {
return [ return [
{x: quad[0], y: quad[1]}, {x: quad[0], y: quad[1]},
{x: quad[2], y: quad[3]}, {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 => ({ return quad.map(point => ({
x: Math.min(Math.max(point.x, 0), width), x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height), 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 // Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons // https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0; let area = 0;
@ -177,7 +169,7 @@ class ElementHandleDelegate {
area += (p1.x * p2.y - p2.x * p1.y) / 2; area += (p1.x * p2.y - p2.x * p1.y) / 2;
} }
return Math.abs(area); return Math.abs(area);
} };
const [result, layoutMetrics] = await Promise.all([ const [result, layoutMetrics] = await Promise.all([
this._client.send('DOM.getContentQuads', { 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); const model = await this._getBoxModel(handle);
let point: Point; let point: dom.Point;
if (!model) { if (!model) {
point = relativePoint; point = relativePoint;
} else { } else {
@ -237,206 +229,8 @@ class ElementHandleDelegate {
scrollY = point.y - metrics.layoutViewport.clientHeight + 1; scrollY = point.y - metrics.layoutViewport.clientHeight + 1;
return { point, scrollX, scrollY }; return { point, scrollX, scrollY };
} }
}
export class ElementHandle extends js.JSHandle<ElementHandle> { async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
private _delegate: ElementHandleDelegate; await handle.evaluate(input.setFileInputFunction, files);
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;
});
} }
} }

View file

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

View file

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

View file

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

View file

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

231
src/dom.ts Normal file
View 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;
});
}
}

View file

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

View file

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

View file

@ -15,70 +15,59 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert, debugError, helper } from '../helper'; import { assert, debugError } from '../helper';
import Injected from '../injected/injected';
import * as input from '../input';
import * as types from '../types';
import * as js from '../javascript'; import * as js from '../javascript';
import * as dom from '../dom';
import * as input from '../input';
import { JugglerSession } from './Connection'; import { JugglerSession } from './Connection';
import { Frame, FrameManager } from './FrameManager'; import { Frame, FrameManager } from './FrameManager';
import { Page } from './Page'; import { ExecutionContext, markJSHandle, ExecutionContextDelegate, toPayload } from './ExecutionContext';
import { JSHandle, ExecutionContext, markJSHandle, ExecutionContextDelegate } 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> { constructor(session: JugglerSession, frameManager: FrameManager, frameId: string) {
_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;
this._session = session; this._session = session;
this._objectId = payload.objectId; this._frameManager = frameManager;
markJSHandle(this, payload); this._frameId = frameId;
} }
async contentFrame(): Promise<Frame | null> { async contentFrame(handle: dom.ElementHandle): Promise<Frame|null> {
const {frameId} = await this._session.send('Page.contentFrame', { const {frameId} = await this._session.send('Page.contentFrame', {
frameId: this._frameId, frameId: this._frameId,
objectId: this._objectId, objectId: toPayload(handle).objectId,
}); });
if (!frameId) if (!frameId)
return null; return null;
const frame = this._page._frameManager.frame(frameId); const frame = this._frameManager.frame(frameId);
return frame; return frame;
} }
asElement(): ElementHandle { isJavascriptEnabled(): boolean {
return this; 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', { return await this._session.send('Page.getBoundingBox', {
frameId: this._frameId, 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', { const clip = await this._session.send('Page.getBoundingBox', {
frameId: this._frameId, frameId: this._frameId,
objectId: this._objectId, objectId: toPayload(handle).objectId,
}); });
if (!clip) if (!clip)
throw new Error('Node is either not visible or not an HTMLElement'); throw new Error('Node is either not visible or not an HTMLElement');
assert(clip.width, 'Node has 0 width.'); assert(clip.width, 'Node has 0 width.');
assert(clip.height, 'Node has 0 height.'); 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: { clip: {
x: clip.x, x: clip.x,
y: clip.y, y: clip.y,
@ -88,182 +77,42 @@ export class ElementHandle extends js.JSHandle<ElementHandle> {
})); }));
} }
isIntersectingViewport(): Promise<boolean> { async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
return this._frame.evaluate(async (element: Element) => { await handle._scrollIntoViewIfNeeded();
const visibleRatio = await new Promise(resolve => { if (!relativePoint)
const observer = new IntersectionObserver(entries => { return this._clickablePoint(handle);
resolve(entries[0].intersectionRatio); const box = await this.boundingBox(handle);
observer.disconnect(); return { x: box.x + relativePoint.x, y: box.y + relativePoint.y };
});
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
});
return visibleRatio > 0;
}, this);
} }
async $(selector: string): Promise<ElementHandle | null> { private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
const handle = await this._frame.evaluateHandle( type Quad = {p1: dom.Point, p2: dom.Point, p3: dom.Point, p4: dom.Point};
(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;
}
async $$(selector: string): Promise<ElementHandle[]> { const computeQuadArea = (quad: Quad) => {
const arrayHandle = await this._frame.evaluateHandle( // Compute sum of all directed areas of adjacent triangles
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root), // https://en.wikipedia.org/wiki/Polygon#Simple_polygons
this, selector, await this._context._injected() let area = 0;
); const points = [quad.p1, quad.p2, quad.p3, quad.p4];
const properties = await arrayHandle.getProperties(); for (let i = 0; i < points.length; ++i) {
await arrayHandle.dispose(); const p1 = points[i];
const result = []; const p2 = points[(i + 1) % points.length];
for (const property of properties.values()) { area += (p1.x * p2.y - p2.x * p1.y) / 2;
const elementHandle = property.asElement(); }
if (elementHandle) return Math.abs(area);
result.push(elementHandle); };
}
return result;
}
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => { const computeQuadCenter = (quad: Quad) => {
const elementHandle = await this.$(selector); let x = 0, y = 0;
if (!elementHandle) for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
throw new Error(`Error: failed to find element matching selector "${selector}"`); x += point.x;
const result = await this._frame.evaluate(pageFunction, elementHandle, ...args); y += point.y;
await elementHandle.dispose(); }
return result; 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', { const result = await this._session.send('Page.getContentQuads', {
frameId: this._frameId, frameId: this._frameId,
objectId: this._objectId, objectId: toPayload(handle).objectId,
}).catch(debugError); }).catch(debugError);
if (!result || !result.quads.length) if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement'); 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 the middle point of the first quad.
return computeQuadCenter(quads[0]); 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) { 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 frame = context.frame();
const frameManager = frame._delegate as FrameManager; const frameManager = frame._delegate as FrameManager;
const frameId = frameManager._frameData(frame).frameId; const frameId = frameManager._frameData(frame).frameId;
const page = frameManager._page;
const session = (context._delegate as ExecutionContextDelegate)._session; 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); const handle = new js.JSHandle(context);
markJSHandle(handle, result); markJSHandle(handle, result);
return handle; 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
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,6 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT license. // 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 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 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>); 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 $$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 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 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>;
}

View file

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

View file

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

View file

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

View file

@ -16,90 +16,113 @@
*/ */
import * as fs from 'fs'; import * as fs from 'fs';
import { assert, debugError, helper } from '../helper'; import { debugError, helper } from '../helper';
import * as input from '../input'; import * as input from '../input';
import * as dom from '../dom';
import * as frames from '../frames';
import { TargetSession } from './Connection'; import { TargetSession } from './Connection';
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext'; import { ExecutionContext, ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
import { FrameManager } from './FrameManager'; import { FrameManager } from './FrameManager';
import { Page } from './Page';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import Injected from '../injected/injected';
import * as types from '../types';
import * as js from '../javascript'; import * as js from '../javascript';
type SelectorRoot = Element | ShadowRoot | Document;
const writeFileAsync = helper.promisify(fs.writeFile); const writeFileAsync = helper.promisify(fs.writeFile);
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) { export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
const delegate = context._delegate as ExecutionContextDelegate;
const frame = context.frame(); const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) { if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager; 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); const handle = new js.JSHandle(context);
markJSHandle(handle, remoteObject); markJSHandle(handle, remoteObject);
return handle; return handle;
} }
export class ElementHandle extends js.JSHandle<ElementHandle> { class DOMWorldDelegate implements dom.DOMWorldDelegate {
private _client: TargetSession; private _client: TargetSession;
private _remoteObject: Protocol.Runtime.RemoteObject;
private _page: Page;
private _frameManager: FrameManager; private _frameManager: FrameManager;
constructor(context: ExecutionContext, client: TargetSession, remoteObject: Protocol.Runtime.RemoteObject, page: Page, frameManager: FrameManager) { constructor(client: TargetSession, frameManager: FrameManager) {
super(context);
this._client = client; this._client = client;
this._remoteObject = remoteObject;
this._page = page;
this._frameManager = frameManager; this._frameManager = frameManager;
markJSHandle(this, remoteObject);
} }
asElement(): ElementHandle | null { async contentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
return this; throw new Error('contentFrame() is not implemented');
} }
async _scrollIntoViewIfNeeded() { isJavascriptEnabled(): boolean {
const error = await this.evaluate(async (element, pageJavascriptEnabled) => { return this._frameManager.page()._javascriptEnabled;
if (!element.isConnected) }
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE) async boundingBox(handle: dom.ElementHandle): Promise<dom.Rect | null> {
return 'Node is not of type HTMLElement'; throw new Error('boundingBox() is not implemented');
// force-scroll if page's javascript is disabled. }
if (!pageJavascriptEnabled) {
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
return false; 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 => { return Math.abs(area);
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);
}
async _clickablePoint() {
const [result, viewport] = await Promise.all([ const [result, viewport] = await Promise.all([
this._client.send('DOM.getContentQuads', { this._client.send('DOM.getContentQuads', {
objectId: this._remoteObject.objectId objectId: toRemoteObject(handle).objectId
}).catch(debugError), }).catch(debugError),
this._page.evaluate(() => ({ clientWidth: innerWidth, clientHeight: innerHeight })), handle.evaluate(() => ({ clientWidth: innerWidth, clientHeight: innerHeight })),
]); ]);
if (!result || !result.quads.length) if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement'); throw new Error('Node is either not visible or not an HTMLElement');
// Filter out quads that have too small area to click into. // Filter out quads that have too small area to click into.
const {clientWidth, clientHeight} = viewport; 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) if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement'); throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad. // 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; }> { async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
return [ const objectId = toRemoteObject(handle);
{x: quad[0], y: quad[1]}, await this._client.send('DOM.setInputFiles', { objectId, files });
{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;
});
} }
} }
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);
}

View file

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

View file

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

View file

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