chore: reuse ElementHandle between browsers (#108)
This commit is contained in:
Родитель
b596f36bad
Коммит
c3393039b0
|
@ -18,17 +18,18 @@
|
|||
import { CDPSession } from './Connection';
|
||||
import { helper } from '../helper';
|
||||
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper';
|
||||
import { createJSHandle, ElementHandle } from './JSHandle';
|
||||
import { createJSHandle } from './JSHandle';
|
||||
import { Protocol } from './protocol';
|
||||
import * as js from '../javascript';
|
||||
import * as dom from '../dom';
|
||||
|
||||
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
||||
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
||||
|
||||
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
|
||||
export type JSHandle = js.JSHandle<ElementHandle>;
|
||||
export type ExecutionContext = js.ExecutionContext;
|
||||
export type JSHandle = js.JSHandle;
|
||||
|
||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
|
||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
|
||||
_client: CDPSession;
|
||||
_contextId: number;
|
||||
|
||||
|
@ -140,7 +141,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate<Ele
|
|||
backendNodeId,
|
||||
executionContextId: this._contextId,
|
||||
});
|
||||
return createJSHandle(context, object) as ElementHandle;
|
||||
return createJSHandle(context, object) as dom.ElementHandle;
|
||||
}
|
||||
|
||||
async getProperties(handle: JSHandle): Promise<Map<string, JSHandle>> {
|
||||
|
|
|
@ -19,10 +19,10 @@ import { EventEmitter } from 'events';
|
|||
import * as frames from '../frames';
|
||||
import { assert, debugError } from '../helper';
|
||||
import * as js from '../javascript';
|
||||
import * as dom from '../dom';
|
||||
import { TimeoutSettings } from '../TimeoutSettings';
|
||||
import { CDPSession } from './Connection';
|
||||
import { EVALUATION_SCRIPT_URL, ExecutionContext, ExecutionContextDelegate, toRemoteObject } from './ExecutionContext';
|
||||
import { ElementHandle } from './JSHandle';
|
||||
import { LifecycleWatcher } from './LifecycleWatcher';
|
||||
import { NetworkManager, Response } from './NetworkManager';
|
||||
import { Page } from './Page';
|
||||
|
@ -45,9 +45,9 @@ type FrameData = {
|
|||
lifecycleEvents: Set<string>,
|
||||
};
|
||||
|
||||
export type Frame = frames.Frame<ElementHandle>;
|
||||
export type Frame = frames.Frame;
|
||||
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate {
|
||||
_client: CDPSession;
|
||||
private _page: Page;
|
||||
private _networkManager: NetworkManager;
|
||||
|
@ -183,7 +183,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<E
|
|||
return this._timeoutSettings;
|
||||
}
|
||||
|
||||
async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle> {
|
||||
async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise<dom.ElementHandle> {
|
||||
const nodeInfo = await this._client.send('DOM.describeNode', {
|
||||
objectId: toRemoteObject(elementHandle).objectId,
|
||||
});
|
||||
|
|
|
@ -15,31 +15,23 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assert, debugError, helper } from '../helper';
|
||||
import Injected from '../injected/injected';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
import { assert, debugError } from '../helper';
|
||||
import * as js from '../javascript';
|
||||
import * as dom from '../dom';
|
||||
import * as input from '../input';
|
||||
import { CDPSession } from './Connection';
|
||||
import { Frame } from './FrameManager';
|
||||
import { FrameManager } from './FrameManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
|
||||
|
||||
type SelectorRoot = Element | ShadowRoot | Document;
|
||||
|
||||
type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): JSHandle {
|
||||
const frame = context.frame();
|
||||
if (remoteObject.subtype === 'node' && frame) {
|
||||
const frameManager = frame._delegate as FrameManager;
|
||||
const page = frameManager.page();
|
||||
const delegate = new ElementHandleDelegate((context._delegate as ExecutionContextDelegate)._client, frameManager);
|
||||
const handle = new ElementHandle(context, page.keyboard, page.mouse, delegate);
|
||||
const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._client, frameManager);
|
||||
const handle = new dom.ElementHandle(context, page.keyboard, page.mouse, delegate);
|
||||
markJSHandle(handle, remoteObject);
|
||||
return handle;
|
||||
}
|
||||
|
@ -48,7 +40,7 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol
|
|||
return handle;
|
||||
}
|
||||
|
||||
class ElementHandleDelegate {
|
||||
class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
private _client: CDPSession;
|
||||
private _frameManager: FrameManager;
|
||||
|
||||
|
@ -57,7 +49,7 @@ class ElementHandleDelegate {
|
|||
this._frameManager = frameManager;
|
||||
}
|
||||
|
||||
async contentFrame(handle: ElementHandle): Promise<Frame|null> {
|
||||
async contentFrame(handle: dom.ElementHandle): Promise<Frame|null> {
|
||||
const nodeInfo = await this._client.send('DOM.describeNode', {
|
||||
objectId: toRemoteObject(handle).objectId
|
||||
});
|
||||
|
@ -70,13 +62,13 @@ class ElementHandleDelegate {
|
|||
return this._frameManager.page()._javascriptEnabled;
|
||||
}
|
||||
|
||||
private _getBoxModel(handle: ElementHandle): Promise<void | Protocol.DOM.getBoxModelReturnValue> {
|
||||
private _getBoxModel(handle: dom.ElementHandle): Promise<void | Protocol.DOM.getBoxModelReturnValue> {
|
||||
return this._client.send('DOM.getBoxModel', {
|
||||
objectId: toRemoteObject(handle).objectId
|
||||
}).catch(error => debugError(error));
|
||||
}
|
||||
|
||||
async boundingBox(handle: ElementHandle): Promise<{ x: number; y: number; width: number; height: number; } | null> {
|
||||
async boundingBox(handle: dom.ElementHandle): Promise<dom.Rect | null> {
|
||||
const result = await this._getBoxModel(handle);
|
||||
if (!result)
|
||||
return null;
|
||||
|
@ -88,7 +80,7 @@ class ElementHandleDelegate {
|
|||
return {x, y, width, height};
|
||||
}
|
||||
|
||||
async screenshot(handle: ElementHandle, options: any = {}): Promise<string | Buffer> {
|
||||
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
|
||||
let needsViewportReset = false;
|
||||
|
||||
let boundingBox = await this.boundingBox(handle);
|
||||
|
@ -129,7 +121,7 @@ class ElementHandleDelegate {
|
|||
return imageData;
|
||||
}
|
||||
|
||||
async ensurePointerActionPoint(handle: ElementHandle, relativePoint?: Point): Promise<Point> {
|
||||
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
if (!relativePoint)
|
||||
return this._clickablePoint(handle);
|
||||
|
@ -150,8 +142,8 @@ class ElementHandleDelegate {
|
|||
return r.point;
|
||||
}
|
||||
|
||||
private async _clickablePoint(handle: ElementHandle): Promise<Point> {
|
||||
const fromProtocolQuad = (quad: number[]): Point[] => {
|
||||
private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
|
||||
const fromProtocolQuad = (quad: number[]): dom.Point[] => {
|
||||
return [
|
||||
{x: quad[0], y: quad[1]},
|
||||
{x: quad[2], y: quad[3]},
|
||||
|
@ -160,14 +152,14 @@ class ElementHandleDelegate {
|
|||
];
|
||||
};
|
||||
|
||||
const intersectQuadWithViewport = (quad: Point[], width: number, height: number): Point[] => {
|
||||
const intersectQuadWithViewport = (quad: dom.Point[], width: number, height: number): dom.Point[] => {
|
||||
return quad.map(point => ({
|
||||
x: Math.min(Math.max(point.x, 0), width),
|
||||
y: Math.min(Math.max(point.y, 0), height),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const computeQuadArea = (quad: Point[]) => {
|
||||
const computeQuadArea = (quad: dom.Point[]) => {
|
||||
// Compute sum of all directed areas of adjacent triangles
|
||||
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
||||
let area = 0;
|
||||
|
@ -177,7 +169,7 @@ class ElementHandleDelegate {
|
|||
area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
||||
}
|
||||
return Math.abs(area);
|
||||
}
|
||||
};
|
||||
|
||||
const [result, layoutMetrics] = await Promise.all([
|
||||
this._client.send('DOM.getContentQuads', {
|
||||
|
@ -208,9 +200,9 @@ class ElementHandleDelegate {
|
|||
};
|
||||
}
|
||||
|
||||
async _viewportPointAndScroll(handle: ElementHandle, relativePoint: Point): Promise<{point: Point, scrollX: number, scrollY: number}> {
|
||||
async _viewportPointAndScroll(handle: dom.ElementHandle, relativePoint: dom.Point): Promise<{point: dom.Point, scrollX: number, scrollY: number}> {
|
||||
const model = await this._getBoxModel(handle);
|
||||
let point: Point;
|
||||
let point: dom.Point;
|
||||
if (!model) {
|
||||
point = relativePoint;
|
||||
} else {
|
||||
|
@ -237,206 +229,8 @@ class ElementHandleDelegate {
|
|||
scrollY = point.y - metrics.layoutViewport.clientHeight + 1;
|
||||
return { point, scrollX, scrollY };
|
||||
}
|
||||
}
|
||||
|
||||
export class ElementHandle extends js.JSHandle<ElementHandle> {
|
||||
private _delegate: ElementHandleDelegate;
|
||||
private _keyboard: input.Keyboard;
|
||||
private _mouse: input.Mouse;
|
||||
|
||||
constructor(context: ExecutionContext, keyboard: input.Keyboard, mouse: input.Mouse, delegate: ElementHandleDelegate) {
|
||||
super(context);
|
||||
this._delegate = delegate;
|
||||
this._keyboard = keyboard;
|
||||
this._mouse = mouse;
|
||||
}
|
||||
|
||||
asElement(): ElementHandle | null {
|
||||
return this;
|
||||
}
|
||||
|
||||
async contentFrame(): Promise<Frame | null> {
|
||||
return this._delegate.contentFrame(this);
|
||||
}
|
||||
|
||||
async _scrollIntoViewIfNeeded() {
|
||||
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
|
||||
if (!element.isConnected)
|
||||
return 'Node is detached from document';
|
||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'Node is not of type HTMLElement';
|
||||
// force-scroll if page's javascript is disabled.
|
||||
if (!pageJavascriptEnabled) {
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
}
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
});
|
||||
if (visibleRatio !== 1.0)
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
}, this._delegate.isJavascriptEnabled());
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
async _performPointerAction(action: (point: Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> {
|
||||
const point = await this._delegate.ensurePointerActionPoint(this, options ? options.relativePoint : undefined);
|
||||
let restoreModifiers: input.Modifier[] | undefined;
|
||||
if (options && options.modifiers)
|
||||
restoreModifiers = await this._keyboard._ensureModifiers(options.modifiers);
|
||||
await action(point);
|
||||
if (restoreModifiers)
|
||||
await this._keyboard._ensureModifiers(restoreModifiers);
|
||||
}
|
||||
|
||||
hover(options?: input.PointerActionOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.move(point.x, point.y), options);
|
||||
}
|
||||
|
||||
click(options?: input.ClickOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.click(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
dblclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.dblclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
tripleclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.tripleclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
|
||||
const options = values.map(value => typeof value === 'object' ? value : { value });
|
||||
for (const option of options) {
|
||||
if (option instanceof ElementHandle)
|
||||
continue;
|
||||
if (option.value !== undefined)
|
||||
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
|
||||
if (option.label !== undefined)
|
||||
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
|
||||
if (option.index !== undefined)
|
||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
||||
}
|
||||
return this.evaluate(input.selectFunction, ...options);
|
||||
}
|
||||
|
||||
async fill(value: string): Promise<void> {
|
||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
const error = await this.evaluate(input.fillFunction);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
await this.focus();
|
||||
await this._keyboard.sendCharacters(value);
|
||||
}
|
||||
|
||||
async setInputFiles(...files: (string|input.FilePayload)[]) {
|
||||
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
|
||||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||
await this.evaluate(input.setFileInputFunction, await input.loadFiles(files));
|
||||
}
|
||||
|
||||
async focus() {
|
||||
await this.evaluate(element => element.focus());
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
||||
await this.focus();
|
||||
await this._keyboard.type(text, options);
|
||||
}
|
||||
|
||||
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
|
||||
await this.focus();
|
||||
await this._keyboard.press(key, options);
|
||||
}
|
||||
|
||||
async boundingBox(): Promise<{ x: number; y: number; width: number; height: number; } | null> {
|
||||
return this._delegate.boundingBox(this);
|
||||
}
|
||||
|
||||
async screenshot(options: any = {}): Promise<string | Buffer> {
|
||||
return this._delegate.screenshot(this, options);
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
const handle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await elementHandle.evaluate(pageFunction, ...args as any);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
$$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
|
||||
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
|
||||
expression, await this._context._injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isIntersectingViewport(): Promise<boolean> {
|
||||
return this.evaluate(async element => {
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
});
|
||||
return visibleRatio > 0;
|
||||
});
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
await handle.evaluate(input.setFileInputFunction, files);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import { FrameManager } from './FrameManager';
|
|||
import { assert, debugError, helper } from '../helper';
|
||||
import { Protocol } from './protocol';
|
||||
import * as network from '../network';
|
||||
import { ElementHandle } from './JSHandle';
|
||||
|
||||
export const NetworkManagerEvents = {
|
||||
Request: Symbol('Events.NetworkManager.Request'),
|
||||
|
@ -31,8 +30,8 @@ export const NetworkManagerEvents = {
|
|||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
||||
};
|
||||
|
||||
export type Request = network.Request<ElementHandle>;
|
||||
export type Response = network.Response<ElementHandle>;
|
||||
export type Request = network.Request;
|
||||
export type Response = network.Response;
|
||||
|
||||
export class NetworkManager extends EventEmitter {
|
||||
private _client: CDPSession;
|
||||
|
@ -269,7 +268,7 @@ export class NetworkManager extends EventEmitter {
|
|||
|
||||
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||
|
||||
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
|
||||
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
|
||||
return (request as any)[interceptableRequestSymbol];
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import { Workers } from './features/workers';
|
|||
import { Frame } from './FrameManager';
|
||||
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||
import { RawMouseImpl, RawKeyboardImpl } from './Input';
|
||||
import { createJSHandle, ElementHandle } from './JSHandle';
|
||||
import { createJSHandle } from './JSHandle';
|
||||
import { JSHandle, toRemoteObject } from './ExecutionContext';
|
||||
import { NetworkManagerEvents, Response } from './NetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
|
@ -45,6 +45,7 @@ import { Target } from './Target';
|
|||
import { TaskQueue } from './TaskQueue';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
import * as dom from '../dom';
|
||||
import { ExecutionContextDelegate } from './ExecutionContext';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
@ -224,7 +225,7 @@ export class Page extends EventEmitter {
|
|||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
async $(selector: string): Promise<dom.ElementHandle | null> {
|
||||
return this.mainFrame().$(selector);
|
||||
}
|
||||
|
||||
|
@ -241,19 +242,19 @@ export class Page extends EventEmitter {
|
|||
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
async $$(selector: string): Promise<dom.ElementHandle[]> {
|
||||
return this.mainFrame().$$(selector);
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
async $x(expression: string): Promise<dom.ElementHandle[]> {
|
||||
return this.mainFrame().$x(expression);
|
||||
}
|
||||
|
||||
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<ElementHandle> {
|
||||
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addScriptTag(options);
|
||||
}
|
||||
|
||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
|
||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addStyleTag(options);
|
||||
}
|
||||
|
||||
|
@ -651,7 +652,7 @@ export class Page extends EventEmitter {
|
|||
return this.mainFrame().hover(selector, options);
|
||||
}
|
||||
|
||||
select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> {
|
||||
select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
|
||||
return this.mainFrame().select(selector, ...values);
|
||||
}
|
||||
|
||||
|
@ -663,11 +664,11 @@ export class Page extends EventEmitter {
|
|||
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
||||
}
|
||||
|
||||
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
|
||||
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
|
||||
return this.mainFrame().waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
|
||||
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
|
||||
return this.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
|
@ -731,6 +732,6 @@ export class ConsoleMessage {
|
|||
}
|
||||
|
||||
type FileChooser = {
|
||||
element: ElementHandle,
|
||||
element: dom.ElementHandle,
|
||||
multiple: boolean
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ export { Chromium } from './features/chromium';
|
|||
export { CDPSession } from './Connection';
|
||||
export { Dialog } from './Dialog';
|
||||
export { ExecutionContext, JSHandle } from '../javascript';
|
||||
export { ElementHandle } from '../dom';
|
||||
export { Accessibility } from './features/accessibility';
|
||||
export { Coverage } from './features/coverage';
|
||||
export { Overrides } from './features/overrides';
|
||||
|
@ -18,7 +19,6 @@ export { Permissions } from './features/permissions';
|
|||
export { Worker, Workers } from './features/workers';
|
||||
export { Frame } from '../frames';
|
||||
export { Keyboard, Mouse } from '../input';
|
||||
export { ElementHandle } from './JSHandle';
|
||||
export { Request, Response } from '../network';
|
||||
export { ConsoleMessage, Page } from './Page';
|
||||
export { Playwright } from './Playwright';
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
*/
|
||||
|
||||
import { CDPSession } from '../Connection';
|
||||
import { ElementHandle } from '../JSHandle';
|
||||
import { Protocol } from '../protocol';
|
||||
import { toRemoteObject } from '../ExecutionContext';
|
||||
import * as dom from '../../dom';
|
||||
|
||||
type SerializedAXNode = {
|
||||
role: string,
|
||||
|
@ -64,7 +64,7 @@ export class Accessibility {
|
|||
|
||||
async snapshot(options: {
|
||||
interestingOnly?: boolean;
|
||||
root?: ElementHandle | null;
|
||||
root?: dom.ElementHandle | null;
|
||||
} = {}): Promise<SerializedAXNode> {
|
||||
const {
|
||||
interestingOnly = true,
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as frames from './frames';
|
||||
import * as types from './types';
|
||||
import * as js from './javascript';
|
||||
import * as input from './input';
|
||||
import { assert, helper } from './helper';
|
||||
import Injected from './injected/injected';
|
||||
|
||||
export type Rect = { x: number, y: number, width: number, height: number };
|
||||
export type Point = { x: number, y: number };
|
||||
type SelectorRoot = Element | ShadowRoot | Document;
|
||||
|
||||
export interface DOMWorldDelegate {
|
||||
isJavascriptEnabled(): boolean;
|
||||
contentFrame(handle: ElementHandle): Promise<frames.Frame | null>;
|
||||
boundingBox(handle: ElementHandle): Promise<Rect | null>;
|
||||
screenshot(handle: ElementHandle, options?: any): Promise<string | Buffer>;
|
||||
ensurePointerActionPoint(handle: ElementHandle, relativePoint?: Point): Promise<Point>;
|
||||
setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise<void>;
|
||||
// await this.evaluate(input.setFileInputFunction, );
|
||||
}
|
||||
|
||||
export class ElementHandle extends js.JSHandle {
|
||||
private _delegate: DOMWorldDelegate;
|
||||
private _keyboard: input.Keyboard;
|
||||
private _mouse: input.Mouse;
|
||||
|
||||
constructor(context: js.ExecutionContext, keyboard: input.Keyboard, mouse: input.Mouse, delegate: DOMWorldDelegate) {
|
||||
super(context);
|
||||
this._delegate = delegate;
|
||||
this._keyboard = keyboard;
|
||||
this._mouse = mouse;
|
||||
}
|
||||
|
||||
asElement(): ElementHandle | null {
|
||||
return this;
|
||||
}
|
||||
|
||||
async contentFrame(): Promise<frames.Frame | null> {
|
||||
return this._delegate.contentFrame(this);
|
||||
}
|
||||
|
||||
async _scrollIntoViewIfNeeded() {
|
||||
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
|
||||
if (!element.isConnected)
|
||||
return 'Node is detached from document';
|
||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'Node is not of type HTMLElement';
|
||||
// force-scroll if page's javascript is disabled.
|
||||
if (!pageJavascriptEnabled) {
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
}
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
if (visibleRatio !== 1.0)
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
}, this._delegate.isJavascriptEnabled());
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
async _performPointerAction(action: (point: Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> {
|
||||
const point = await this._delegate.ensurePointerActionPoint(this, options ? options.relativePoint : undefined);
|
||||
let restoreModifiers: input.Modifier[] | undefined;
|
||||
if (options && options.modifiers)
|
||||
restoreModifiers = await this._keyboard._ensureModifiers(options.modifiers);
|
||||
await action(point);
|
||||
if (restoreModifiers)
|
||||
await this._keyboard._ensureModifiers(restoreModifiers);
|
||||
}
|
||||
|
||||
hover(options?: input.PointerActionOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.move(point.x, point.y), options);
|
||||
}
|
||||
|
||||
click(options?: input.ClickOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.click(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
dblclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.dblclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
tripleclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
return this._performPointerAction(point => this._mouse.tripleclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
|
||||
const options = values.map(value => typeof value === 'object' ? value : { value });
|
||||
for (const option of options) {
|
||||
if (option instanceof ElementHandle)
|
||||
continue;
|
||||
if (option.value !== undefined)
|
||||
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
|
||||
if (option.label !== undefined)
|
||||
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
|
||||
if (option.index !== undefined)
|
||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
||||
}
|
||||
return this.evaluate(input.selectFunction, ...options);
|
||||
}
|
||||
|
||||
async fill(value: string): Promise<void> {
|
||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
const error = await this.evaluate(input.fillFunction);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
await this.focus();
|
||||
await this._keyboard.sendCharacters(value);
|
||||
}
|
||||
|
||||
async setInputFiles(...files: (string|input.FilePayload)[]) {
|
||||
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
|
||||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||
await this._delegate.setInputFiles(this, await input.loadFiles(files));
|
||||
}
|
||||
|
||||
async focus() {
|
||||
await this.evaluate(element => element.focus());
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
||||
await this.focus();
|
||||
await this._keyboard.type(text, options);
|
||||
}
|
||||
|
||||
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
|
||||
await this.focus();
|
||||
await this._keyboard.press(key, options);
|
||||
}
|
||||
|
||||
async boundingBox(): Promise<Rect | null> {
|
||||
return this._delegate.boundingBox(this);
|
||||
}
|
||||
|
||||
async screenshot(options: any = {}): Promise<string | Buffer> {
|
||||
return this._delegate.screenshot(this, options);
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
const handle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
$eval: types.$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await elementHandle.evaluate(pageFunction, ...args as any);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
$$eval: types.$$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
|
||||
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
|
||||
expression, await this._context._injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isIntersectingViewport(): Promise<boolean> {
|
||||
return this.evaluate(async element => {
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
return visibleRatio > 0;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -16,14 +16,14 @@
|
|||
*/
|
||||
|
||||
import {helper, debugError} from '../helper';
|
||||
import { createHandle, ElementHandle } from './JSHandle';
|
||||
import { createHandle } from './JSHandle';
|
||||
import * as js from '../javascript';
|
||||
import { JugglerSession } from './Connection';
|
||||
|
||||
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
|
||||
export type JSHandle = js.JSHandle<ElementHandle>;
|
||||
export type ExecutionContext = js.ExecutionContext;
|
||||
export type JSHandle = js.JSHandle;
|
||||
|
||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
|
||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
|
||||
_session: JugglerSession;
|
||||
_executionContextId: string;
|
||||
|
||||
|
|
|
@ -20,10 +20,10 @@ import { TimeoutError } from '../Errors';
|
|||
import * as frames from '../frames';
|
||||
import { assert, helper, RegisteredListener } from '../helper';
|
||||
import * as js from '../javascript';
|
||||
import * as dom from '../dom';
|
||||
import { TimeoutSettings } from '../TimeoutSettings';
|
||||
import { JugglerSession } from './Connection';
|
||||
import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext';
|
||||
import { ElementHandle } from './JSHandle';
|
||||
import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog';
|
||||
import { Page } from './Page';
|
||||
|
||||
|
@ -42,9 +42,9 @@ type FrameData = {
|
|||
firedEvents: Set<string>,
|
||||
};
|
||||
|
||||
export type Frame = frames.Frame<ElementHandle>;
|
||||
export type Frame = frames.Frame;
|
||||
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate {
|
||||
_session: JugglerSession;
|
||||
_page: Page;
|
||||
_networkManager: any;
|
||||
|
@ -180,7 +180,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<E
|
|||
return this._timeoutSettings;
|
||||
}
|
||||
|
||||
async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle> {
|
||||
async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise<dom.ElementHandle> {
|
||||
assert(false, 'Multiple isolated worlds are not implemented');
|
||||
return elementHandle;
|
||||
}
|
||||
|
|
|
@ -15,70 +15,59 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assert, debugError, helper } from '../helper';
|
||||
import Injected from '../injected/injected';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
import { assert, debugError } from '../helper';
|
||||
import * as js from '../javascript';
|
||||
import * as dom from '../dom';
|
||||
import * as input from '../input';
|
||||
import { JugglerSession } from './Connection';
|
||||
import { Frame, FrameManager } from './FrameManager';
|
||||
import { Page } from './Page';
|
||||
import { JSHandle, ExecutionContext, markJSHandle, ExecutionContextDelegate } from './ExecutionContext';
|
||||
import { ExecutionContext, markJSHandle, ExecutionContextDelegate, toPayload } from './ExecutionContext';
|
||||
|
||||
type SelectorRoot = Element | ShadowRoot | Document;
|
||||
class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
private _session: JugglerSession;
|
||||
private _frameManager: FrameManager;
|
||||
private _frameId: string;
|
||||
|
||||
export class ElementHandle extends js.JSHandle<ElementHandle> {
|
||||
_frame: Frame;
|
||||
_frameId: string;
|
||||
_page: Page;
|
||||
_context: ExecutionContext;
|
||||
protected _session: JugglerSession;
|
||||
protected _objectId: string;
|
||||
|
||||
constructor(frame: Frame, frameId: string, page: Page, session: JugglerSession, context: ExecutionContext, payload: any) {
|
||||
super(context);
|
||||
this._frame = frame;
|
||||
this._frameId = frameId;
|
||||
this._page = page;
|
||||
constructor(session: JugglerSession, frameManager: FrameManager, frameId: string) {
|
||||
this._session = session;
|
||||
this._objectId = payload.objectId;
|
||||
markJSHandle(this, payload);
|
||||
this._frameManager = frameManager;
|
||||
this._frameId = frameId;
|
||||
}
|
||||
|
||||
async contentFrame(): Promise<Frame | null> {
|
||||
async contentFrame(handle: dom.ElementHandle): Promise<Frame|null> {
|
||||
const {frameId} = await this._session.send('Page.contentFrame', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
objectId: toPayload(handle).objectId,
|
||||
});
|
||||
if (!frameId)
|
||||
return null;
|
||||
const frame = this._page._frameManager.frame(frameId);
|
||||
const frame = this._frameManager.frame(frameId);
|
||||
return frame;
|
||||
}
|
||||
|
||||
asElement(): ElementHandle {
|
||||
return this;
|
||||
isJavascriptEnabled(): boolean {
|
||||
return this._frameManager._page._javascriptEnabled;
|
||||
}
|
||||
|
||||
async boundingBox(): Promise<{ width: number; height: number; x: number; y: number; }> {
|
||||
async boundingBox(handle: dom.ElementHandle): Promise<dom.Rect | null> {
|
||||
return await this._session.send('Page.getBoundingBox', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
objectId: toPayload(handle).objectId,
|
||||
});
|
||||
}
|
||||
|
||||
async screenshot(options: { encoding?: string; path?: string; } = {}) {
|
||||
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
|
||||
const clip = await this._session.send('Page.getBoundingBox', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
objectId: toPayload(handle).objectId,
|
||||
});
|
||||
if (!clip)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
assert(clip.width, 'Node has 0 width.');
|
||||
assert(clip.height, 'Node has 0 height.');
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
|
||||
return await this._page.screenshot(Object.assign({}, options, {
|
||||
return await this._frameManager._page.screenshot(Object.assign({}, options, {
|
||||
clip: {
|
||||
x: clip.x,
|
||||
y: clip.y,
|
||||
|
@ -88,182 +77,42 @@ export class ElementHandle extends js.JSHandle<ElementHandle> {
|
|||
}));
|
||||
}
|
||||
|
||||
isIntersectingViewport(): Promise<boolean> {
|
||||
return this._frame.evaluate(async (element: Element) => {
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
return visibleRatio > 0;
|
||||
}, this);
|
||||
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
if (!relativePoint)
|
||||
return this._clickablePoint(handle);
|
||||
const box = await this.boundingBox(handle);
|
||||
return { x: box.x + relativePoint.x, y: box.y + relativePoint.y };
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
const handle = await this._frame.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
|
||||
this, selector, await this._context._injected()
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
|
||||
type Quad = {p1: dom.Point, p2: dom.Point, p3: dom.Point, p4: dom.Point};
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this._frame.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
this, selector, await this._context._injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const computeQuadArea = (quad: Quad) => {
|
||||
// Compute sum of all directed areas of adjacent triangles
|
||||
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
||||
let area = 0;
|
||||
const points = [quad.p1, quad.p2, quad.p3, quad.p4];
|
||||
for (let i = 0; i < points.length; ++i) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % points.length];
|
||||
area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
||||
}
|
||||
return Math.abs(area);
|
||||
};
|
||||
|
||||
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await this._frame.evaluate(pageFunction, elementHandle, ...args);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
const computeQuadCenter = (quad: Quad) => {
|
||||
let x = 0, y = 0;
|
||||
for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
|
||||
x += point.x;
|
||||
y += point.y;
|
||||
}
|
||||
return {x: x / 4, y: y / 4};
|
||||
};
|
||||
|
||||
$$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const arrayHandle = await this._frame.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
this, selector, await this._context._injected()
|
||||
);
|
||||
|
||||
const result = await this._frame.evaluate(pageFunction, arrayHandle, ...args);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<Array<ElementHandle>> {
|
||||
const arrayHandle = await this._frame.evaluateHandle(
|
||||
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
|
||||
this, expression, await this._context._injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async _scrollIntoViewIfNeeded() {
|
||||
const error = await this._frame.evaluate(async (element: Element) => {
|
||||
if (!element.isConnected)
|
||||
return 'Node is detached from document';
|
||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'Node is not of type HTMLElement';
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
if (visibleRatio !== 1.0)
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: ('instant' as ScrollBehavior)});
|
||||
return false;
|
||||
}, this);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
async click(options?: input.ClickOptions) {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.click(x, y, options);
|
||||
}
|
||||
|
||||
async dblclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.dblclick(x, y, options);
|
||||
}
|
||||
|
||||
async tripleclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.tripleclick(x, y, options);
|
||||
}
|
||||
|
||||
async setInputFiles(...files: (string|input.FilePayload)[]) {
|
||||
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
|
||||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||
await this.evaluate(input.setFileInputFunction, await input.loadFiles(files));
|
||||
}
|
||||
|
||||
async hover() {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.move(x, y);
|
||||
}
|
||||
|
||||
async focus() {
|
||||
await this._frame.evaluate(element => element.focus(), this);
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
||||
await this.focus();
|
||||
await this._page.keyboard.type(text, options);
|
||||
}
|
||||
|
||||
async press(key: string, options: { delay?: number; } | undefined) {
|
||||
await this.focus();
|
||||
await this._page.keyboard.press(key, options);
|
||||
}
|
||||
|
||||
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
|
||||
const options = values.map(value => typeof value === 'object' ? value : { value });
|
||||
for (const option of options) {
|
||||
if (option instanceof ElementHandle)
|
||||
continue;
|
||||
if (option.value !== undefined)
|
||||
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
|
||||
if (option.label !== undefined)
|
||||
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
|
||||
if (option.index !== undefined)
|
||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
||||
}
|
||||
return this.evaluate(input.selectFunction, ...options);
|
||||
}
|
||||
|
||||
async fill(value: string): Promise<void> {
|
||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
const error = await this.evaluate(input.fillFunction);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
await this.focus();
|
||||
await this._page.keyboard.sendCharacters(value);
|
||||
}
|
||||
|
||||
async _clickablePoint(): Promise<{ x: number; y: number; }> {
|
||||
const result = await this._session.send('Page.getContentQuads', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
objectId: toPayload(handle).objectId,
|
||||
}).catch(debugError);
|
||||
if (!result || !result.quads.length)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
|
@ -274,6 +123,10 @@ export class ElementHandle extends js.JSHandle<ElementHandle> {
|
|||
// Return the middle point of the first quad.
|
||||
return computeQuadCenter(quads[0]);
|
||||
}
|
||||
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
await handle.evaluate(input.setFileInputFunction, files);
|
||||
}
|
||||
}
|
||||
|
||||
export function createHandle(context: ExecutionContext, result: any, exceptionDetails?: any) {
|
||||
|
@ -287,39 +140,13 @@ export function createHandle(context: ExecutionContext, result: any, exceptionDe
|
|||
const frame = context.frame();
|
||||
const frameManager = frame._delegate as FrameManager;
|
||||
const frameId = frameManager._frameData(frame).frameId;
|
||||
const page = frameManager._page;
|
||||
const session = (context._delegate as ExecutionContextDelegate)._session;
|
||||
return new ElementHandle(frame, frameId, page, session, context, result);
|
||||
const delegate = new DOMWorldDelegate(session, frameManager, frameId);
|
||||
const handle = new dom.ElementHandle(context, frameManager._page.keyboard, frameManager._page.mouse, delegate);
|
||||
markJSHandle(handle, result);
|
||||
return handle;
|
||||
}
|
||||
const handle = new js.JSHandle(context);
|
||||
markJSHandle(handle, result);
|
||||
return handle;
|
||||
}
|
||||
|
||||
function computeQuadArea(quad) {
|
||||
// Compute sum of all directed areas of adjacent triangles
|
||||
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
||||
let area = 0;
|
||||
const points = [quad.p1, quad.p2, quad.p3, quad.p4];
|
||||
for (let i = 0; i < points.length; ++i) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % points.length];
|
||||
area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
||||
}
|
||||
return Math.abs(area);
|
||||
}
|
||||
|
||||
function computeQuadCenter(quad) {
|
||||
let x = 0, y = 0;
|
||||
for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
|
||||
x += point.x;
|
||||
y += point.y;
|
||||
}
|
||||
return {x: x / 4, y: y / 4};
|
||||
}
|
||||
|
||||
type FilePayload = {
|
||||
name: string,
|
||||
mimeType: string,
|
||||
data: string
|
||||
};
|
||||
|
|
|
@ -20,10 +20,9 @@ import { assert, debugError, helper, RegisteredListener } from '../helper';
|
|||
import { JugglerSession } from './Connection';
|
||||
import { FrameManager, Frame } from './FrameManager';
|
||||
import * as network from '../network';
|
||||
import { ElementHandle } from './JSHandle';
|
||||
|
||||
export type Request = network.Request<ElementHandle>;
|
||||
export type Response = network.Response<ElementHandle>;
|
||||
export type Request = network.Request;
|
||||
export type Response = network.Response;
|
||||
|
||||
export const NetworkManagerEvents = {
|
||||
RequestFailed: Symbol('NetworkManagerEvents.RequestFailed'),
|
||||
|
@ -166,7 +165,7 @@ const causeToResourceType = {
|
|||
|
||||
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||
|
||||
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
|
||||
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
|
||||
return (request as any)[interceptableRequestSymbol];
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,12 @@ import { Accessibility } from './features/accessibility';
|
|||
import { Interception } from './features/interception';
|
||||
import { FrameManager, FrameManagerEvents, normalizeWaitUntil, Frame } from './FrameManager';
|
||||
import { RawMouseImpl, RawKeyboardImpl } from './Input';
|
||||
import { createHandle, ElementHandle } from './JSHandle';
|
||||
import { createHandle } from './JSHandle';
|
||||
import { NavigationWatchdog } from './NavigationWatchdog';
|
||||
import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
import * as dom from '../dom';
|
||||
import { JSHandle, toPayload, deserializeValue } from './ExecutionContext';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
@ -33,6 +34,7 @@ export class Page extends EventEmitter {
|
|||
private _pageBindings: Map<string, Function>;
|
||||
private _networkManager: NetworkManager;
|
||||
_frameManager: FrameManager;
|
||||
_javascriptEnabled = true;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
private _viewport: Viewport;
|
||||
private _disconnectPromise: Promise<Error>;
|
||||
|
@ -209,6 +211,7 @@ export class Page extends EventEmitter {
|
|||
}
|
||||
|
||||
async setJavaScriptEnabled(enabled) {
|
||||
this._javascriptEnabled = enabled;
|
||||
await this._session.send('Page.setJavascriptEnabled', {enabled});
|
||||
}
|
||||
|
||||
|
@ -421,11 +424,11 @@ export class Page extends EventEmitter {
|
|||
return this.mainFrame().evaluate(pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise<ElementHandle> {
|
||||
addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addScriptTag(options);
|
||||
}
|
||||
|
||||
addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise<ElementHandle> {
|
||||
addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addStyleTag(options);
|
||||
}
|
||||
|
||||
|
@ -469,11 +472,11 @@ export class Page extends EventEmitter {
|
|||
return this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
|
||||
waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> {
|
||||
waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<dom.ElementHandle> {
|
||||
return this._frameManager.mainFrame().waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> {
|
||||
waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<dom.ElementHandle> {
|
||||
return this._frameManager.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
|
@ -481,11 +484,11 @@ export class Page extends EventEmitter {
|
|||
return this._frameManager.mainFrame().title();
|
||||
}
|
||||
|
||||
$(selector: string): Promise<ElementHandle | null> {
|
||||
$(selector: string): Promise<dom.ElementHandle | null> {
|
||||
return this._frameManager.mainFrame().$(selector);
|
||||
}
|
||||
|
||||
$$(selector: string): Promise<Array<ElementHandle>> {
|
||||
$$(selector: string): Promise<Array<dom.ElementHandle>> {
|
||||
return this._frameManager.mainFrame().$$(selector);
|
||||
}
|
||||
|
||||
|
@ -497,7 +500,7 @@ export class Page extends EventEmitter {
|
|||
return this._frameManager.mainFrame().$$eval(selector, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
$x(expression: string): Promise<Array<ElementHandle>> {
|
||||
$x(expression: string): Promise<Array<dom.ElementHandle>> {
|
||||
return this._frameManager.mainFrame().$x(expression);
|
||||
}
|
||||
|
||||
|
@ -548,7 +551,7 @@ export class Page extends EventEmitter {
|
|||
if (!this._fileChooserInterceptors.size)
|
||||
return;
|
||||
const context = this._frameManager.executionContextById(executionContextId);
|
||||
const handle = createHandle(context, element) as ElementHandle;
|
||||
const handle = createHandle(context, element) as dom.ElementHandle;
|
||||
const interceptors = Array.from(this._fileChooserInterceptors);
|
||||
this._fileChooserInterceptors.clear();
|
||||
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
|
||||
|
@ -621,6 +624,6 @@ export type Viewport = {
|
|||
}
|
||||
|
||||
type FileChooser = {
|
||||
element: ElementHandle,
|
||||
element: dom.ElementHandle,
|
||||
multiple: boolean
|
||||
};
|
||||
|
|
|
@ -10,8 +10,8 @@ export { ExecutionContext, JSHandle } from '../javascript';
|
|||
export { Accessibility } from './features/accessibility';
|
||||
export { Interception } from './features/interception';
|
||||
export { Permissions } from './features/permissions';
|
||||
export { Frame } from './FrameManager';
|
||||
export { ElementHandle } from './JSHandle';
|
||||
export { Frame } from '../frames';
|
||||
export { ElementHandle } from '../dom';
|
||||
export { Request, Response } from '../network';
|
||||
export { ConsoleMessage, Page } from './Page';
|
||||
export { Playwright } from './Playwright';
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import * as types from './types';
|
||||
import * as fs from 'fs';
|
||||
import * as js from './javascript';
|
||||
import * as dom from './dom';
|
||||
import * as network from './network';
|
||||
import { helper, assert } from './helper';
|
||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
||||
|
@ -27,11 +28,11 @@ import { TimeoutSettings } from './TimeoutSettings';
|
|||
const readFileAsync = helper.promisify(fs.readFile);
|
||||
|
||||
type WorldType = 'main' | 'utility';
|
||||
type World<ElementHandle extends types.ElementHandle<ElementHandle>> = {
|
||||
contextPromise: Promise<js.ExecutionContext<ElementHandle>>;
|
||||
contextResolveCallback: (c: js.ExecutionContext<ElementHandle>) => void;
|
||||
context: js.ExecutionContext<ElementHandle> | null;
|
||||
waitTasks: Set<WaitTask<ElementHandle>>;
|
||||
type World = {
|
||||
contextPromise: Promise<js.ExecutionContext>;
|
||||
contextResolveCallback: (c: js.ExecutionContext) => void;
|
||||
context: js.ExecutionContext | null;
|
||||
waitTasks: Set<WaitTask>;
|
||||
};
|
||||
|
||||
export type NavigateOptions = {
|
||||
|
@ -43,24 +44,24 @@ export type GotoOptions = NavigateOptions & {
|
|||
referer?: string,
|
||||
};
|
||||
|
||||
export interface FrameDelegate<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
export interface FrameDelegate {
|
||||
timeoutSettings(): TimeoutSettings;
|
||||
navigateFrame(frame: Frame<ElementHandle>, url: string, options?: GotoOptions): Promise<network.Response<ElementHandle> | null>;
|
||||
waitForFrameNavigation(frame: Frame<ElementHandle>, options?: NavigateOptions): Promise<network.Response<ElementHandle> | null>;
|
||||
setFrameContent(frame: Frame<ElementHandle>, html: string, options?: NavigateOptions): Promise<void>;
|
||||
adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle>): Promise<ElementHandle>;
|
||||
navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise<network.Response | null>;
|
||||
waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise<network.Response | null>;
|
||||
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
|
||||
adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise<dom.ElementHandle>;
|
||||
}
|
||||
|
||||
export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
_delegate: FrameDelegate<ElementHandle>;
|
||||
private _parentFrame: Frame<ElementHandle>;
|
||||
export class Frame {
|
||||
_delegate: FrameDelegate;
|
||||
private _parentFrame: Frame;
|
||||
private _url = '';
|
||||
private _detached = false;
|
||||
private _worlds = new Map<WorldType, World<ElementHandle>>();
|
||||
private _childFrames = new Set<Frame<ElementHandle>>();
|
||||
private _worlds = new Map<WorldType, World>();
|
||||
private _childFrames = new Set<Frame>();
|
||||
private _name: string;
|
||||
|
||||
constructor(delegate: FrameDelegate<ElementHandle>, parentFrame: Frame<ElementHandle> | null) {
|
||||
constructor(delegate: FrameDelegate, parentFrame: Frame | null) {
|
||||
this._delegate = delegate;
|
||||
this._parentFrame = parentFrame;
|
||||
|
||||
|
@ -73,65 +74,65 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
this._parentFrame._childFrames.add(this);
|
||||
}
|
||||
|
||||
goto(url: string, options?: GotoOptions): Promise<network.Response<ElementHandle> | null> {
|
||||
goto(url: string, options?: GotoOptions): Promise<network.Response | null> {
|
||||
return this._delegate.navigateFrame(this, url, options);
|
||||
}
|
||||
|
||||
waitForNavigation(options?: NavigateOptions): Promise<network.Response<ElementHandle> | null> {
|
||||
waitForNavigation(options?: NavigateOptions): Promise<network.Response | null> {
|
||||
return this._delegate.waitForFrameNavigation(this, options);
|
||||
}
|
||||
|
||||
_mainContext(): Promise<js.ExecutionContext<ElementHandle>> {
|
||||
_mainContext(): Promise<js.ExecutionContext> {
|
||||
if (this._detached)
|
||||
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
||||
return this._worlds.get('main').contextPromise;
|
||||
}
|
||||
|
||||
_utilityContext(): Promise<js.ExecutionContext<ElementHandle>> {
|
||||
_utilityContext(): Promise<js.ExecutionContext> {
|
||||
if (this._detached)
|
||||
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
||||
return this._worlds.get('utility').contextPromise;
|
||||
}
|
||||
|
||||
executionContext(): Promise<js.ExecutionContext<ElementHandle>> {
|
||||
executionContext(): Promise<js.ExecutionContext> {
|
||||
return this._mainContext();
|
||||
}
|
||||
|
||||
evaluateHandle: types.EvaluateHandle<js.JSHandle<ElementHandle>> = async (pageFunction, ...args) => {
|
||||
evaluateHandle: types.EvaluateHandle<js.JSHandle> = async (pageFunction, ...args) => {
|
||||
const context = await this._mainContext();
|
||||
return context.evaluateHandle(pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
evaluate: types.Evaluate<js.JSHandle<ElementHandle>> = async (pageFunction, ...args) => {
|
||||
evaluate: types.Evaluate<js.JSHandle> = async (pageFunction, ...args) => {
|
||||
const context = await this._mainContext();
|
||||
return context.evaluate(pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
async $(selector: string): Promise<dom.ElementHandle | null> {
|
||||
const context = await this._mainContext();
|
||||
const document = await context._document();
|
||||
return document.$(selector);
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
async $x(expression: string): Promise<dom.ElementHandle[]> {
|
||||
const context = await this._mainContext();
|
||||
const document = await context._document();
|
||||
return document.$x(expression);
|
||||
}
|
||||
|
||||
$eval: types.$Eval<js.JSHandle<ElementHandle>> = async (selector, pageFunction, ...args) => {
|
||||
$eval: types.$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const context = await this._mainContext();
|
||||
const document = await context._document();
|
||||
return document.$eval(selector, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
$$eval: types.$$Eval<js.JSHandle<ElementHandle>> = async (selector, pageFunction, ...args) => {
|
||||
$$eval: types.$$Eval<js.JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const context = await this._mainContext();
|
||||
const document = await context._document();
|
||||
return document.$$eval(selector, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
async $$(selector: string): Promise<dom.ElementHandle[]> {
|
||||
const context = await this._mainContext();
|
||||
const document = await context._document();
|
||||
return document.$$(selector);
|
||||
|
@ -161,11 +162,11 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
return this._url;
|
||||
}
|
||||
|
||||
parentFrame(): Frame<ElementHandle> | null {
|
||||
parentFrame(): Frame | null {
|
||||
return this._parentFrame;
|
||||
}
|
||||
|
||||
childFrames(): Frame<ElementHandle>[] {
|
||||
childFrames(): Frame[] {
|
||||
return Array.from(this._childFrames);
|
||||
}
|
||||
|
||||
|
@ -177,7 +178,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
url?: string; path?: string;
|
||||
content?: string;
|
||||
type?: string;
|
||||
}): Promise<ElementHandle> {
|
||||
}): Promise<dom.ElementHandle> {
|
||||
const {
|
||||
url = null,
|
||||
path = null,
|
||||
|
@ -234,7 +235,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
}
|
||||
}
|
||||
|
||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
|
||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
|
||||
const {
|
||||
url = null,
|
||||
path = null,
|
||||
|
@ -344,15 +345,15 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
await handle.dispose();
|
||||
}
|
||||
|
||||
async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> {
|
||||
async select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
|
||||
const context = await this._utilityContext();
|
||||
const document = await context._document();
|
||||
const handle = await document.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
const utilityContext = await this._utilityContext();
|
||||
const adoptedValues = await Promise.all(values.map(async value => {
|
||||
if (typeof value === 'object' && (value as any).asElement && (value as any).asElement() === value)
|
||||
return this._adoptElementHandle(value as ElementHandle, utilityContext, false /* dispose */);
|
||||
if (value instanceof dom.ElementHandle)
|
||||
return this._adoptElementHandle(value, utilityContext, false /* dispose */);
|
||||
return value;
|
||||
}));
|
||||
const result = await handle.select(...adoptedValues);
|
||||
|
@ -369,7 +370,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
await handle.dispose();
|
||||
}
|
||||
|
||||
waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise<js.JSHandle<ElementHandle> | null> {
|
||||
waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise<js.JSHandle | null> {
|
||||
const xPathPattern = '//';
|
||||
|
||||
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
||||
|
@ -388,7 +389,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
async waitForSelector(selector: string, options: {
|
||||
visible?: boolean;
|
||||
hidden?: boolean;
|
||||
timeout?: number; } | undefined): Promise<ElementHandle | null> {
|
||||
timeout?: number; } | undefined): Promise<dom.ElementHandle | null> {
|
||||
const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._delegate.timeoutSettings().timeout(), ...options });
|
||||
const handle = await this._scheduleWaitTask(params, this._worlds.get('utility'));
|
||||
if (!handle.asElement()) {
|
||||
|
@ -402,7 +403,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
async waitForXPath(xpath: string, options: {
|
||||
visible?: boolean;
|
||||
hidden?: boolean;
|
||||
timeout?: number; } | undefined): Promise<ElementHandle | null> {
|
||||
timeout?: number; } | undefined): Promise<dom.ElementHandle | null> {
|
||||
const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._delegate.timeoutSettings().timeout(), ...options });
|
||||
const handle = await this._scheduleWaitTask(params, this._worlds.get('utility'));
|
||||
if (!handle.asElement()) {
|
||||
|
@ -416,7 +417,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
waitForFunction(
|
||||
pageFunction: Function | string,
|
||||
options: { polling?: string | number; timeout?: number; } = {},
|
||||
...args): Promise<js.JSHandle<ElementHandle>> {
|
||||
...args): Promise<js.JSHandle> {
|
||||
const {
|
||||
polling = 'raf',
|
||||
timeout = this._delegate.timeoutSettings().timeout(),
|
||||
|
@ -452,7 +453,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
this._parentFrame = null;
|
||||
}
|
||||
|
||||
private _scheduleWaitTask(params: WaitTaskParams, world: World<ElementHandle>): Promise<js.JSHandle<ElementHandle>> {
|
||||
private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise<js.JSHandle> {
|
||||
const task = new WaitTask(params, () => world.waitTasks.delete(task));
|
||||
world.waitTasks.add(task);
|
||||
if (world.context)
|
||||
|
@ -460,7 +461,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
return task.promise;
|
||||
}
|
||||
|
||||
private _setContext(worldType: WorldType, context: js.ExecutionContext<ElementHandle> | null) {
|
||||
private _setContext(worldType: WorldType, context: js.ExecutionContext | null) {
|
||||
const world = this._worlds.get(worldType);
|
||||
world.context = context;
|
||||
if (context) {
|
||||
|
@ -474,7 +475,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
}
|
||||
}
|
||||
|
||||
_contextCreated(worldType: WorldType, context: js.ExecutionContext<ElementHandle>) {
|
||||
_contextCreated(worldType: WorldType, context: js.ExecutionContext) {
|
||||
const world = this._worlds.get(worldType);
|
||||
// In case of multiple sessions to the same target, there's a race between
|
||||
// connections so we might end up creating multiple isolated worlds.
|
||||
|
@ -483,14 +484,14 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
this._setContext(worldType, context);
|
||||
}
|
||||
|
||||
_contextDestroyed(context: js.ExecutionContext<ElementHandle>) {
|
||||
_contextDestroyed(context: js.ExecutionContext) {
|
||||
for (const [worldType, world] of this._worlds) {
|
||||
if (world.context === context)
|
||||
this._setContext(worldType, null);
|
||||
}
|
||||
}
|
||||
|
||||
private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle>, dispose: boolean): Promise<ElementHandle> {
|
||||
private async _adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise<dom.ElementHandle> {
|
||||
if (elementHandle.executionContext() === context)
|
||||
return elementHandle;
|
||||
const handle = this._delegate.adoptElementHandle(elementHandle, context);
|
||||
|
|
|
@ -3,42 +3,43 @@
|
|||
|
||||
import * as frames from './frames';
|
||||
import * as types from './types';
|
||||
import * as dom from './dom';
|
||||
import * as injectedSource from './generated/injectedSource';
|
||||
import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
|
||||
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
||||
|
||||
export interface ExecutionContextDelegate<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
evaluate(context: ExecutionContext<ElementHandle>, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
||||
getProperties(handle: JSHandle<ElementHandle>): Promise<Map<string, JSHandle<ElementHandle>>>;
|
||||
releaseHandle(handle: JSHandle<ElementHandle>): Promise<void>;
|
||||
handleToString(handle: JSHandle<ElementHandle>): string;
|
||||
handleJSONValue(handle: JSHandle<ElementHandle>): Promise<any>;
|
||||
export interface ExecutionContextDelegate {
|
||||
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
||||
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
|
||||
releaseHandle(handle: JSHandle): Promise<void>;
|
||||
handleToString(handle: JSHandle): string;
|
||||
handleJSONValue(handle: JSHandle): Promise<any>;
|
||||
}
|
||||
|
||||
export class ExecutionContext<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
_delegate: ExecutionContextDelegate<ElementHandle>;
|
||||
private _frame: frames.Frame<ElementHandle>;
|
||||
private _injectedPromise: Promise<JSHandle<ElementHandle>> | null = null;
|
||||
private _documentPromise: Promise<ElementHandle> | null = null;
|
||||
export class ExecutionContext {
|
||||
_delegate: ExecutionContextDelegate;
|
||||
private _frame: frames.Frame;
|
||||
private _injectedPromise: Promise<JSHandle> | null = null;
|
||||
private _documentPromise: Promise<dom.ElementHandle> | null = null;
|
||||
|
||||
constructor(delegate: ExecutionContextDelegate<ElementHandle>, frame: frames.Frame<ElementHandle> | null) {
|
||||
constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) {
|
||||
this._delegate = delegate;
|
||||
this._frame = frame;
|
||||
}
|
||||
|
||||
frame(): frames.Frame<ElementHandle> | null {
|
||||
frame(): frames.Frame | null {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
evaluate: types.Evaluate<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||
evaluate: types.Evaluate<JSHandle> = (pageFunction, ...args) => {
|
||||
return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args);
|
||||
}
|
||||
|
||||
evaluateHandle: types.EvaluateHandle<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||
evaluateHandle: types.EvaluateHandle<JSHandle> = (pageFunction, ...args) => {
|
||||
return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args);
|
||||
}
|
||||
|
||||
_injected(): Promise<JSHandle<ElementHandle>> {
|
||||
_injected(): Promise<JSHandle> {
|
||||
if (!this._injectedPromise) {
|
||||
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source];
|
||||
const source = `
|
||||
|
@ -51,34 +52,34 @@ export class ExecutionContext<ElementHandle extends types.ElementHandle<ElementH
|
|||
return this._injectedPromise;
|
||||
}
|
||||
|
||||
_document(): Promise<ElementHandle> {
|
||||
_document(): Promise<dom.ElementHandle> {
|
||||
if (!this._documentPromise)
|
||||
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!);
|
||||
return this._documentPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
_context: ExecutionContext<ElementHandle>;
|
||||
export class JSHandle {
|
||||
_context: ExecutionContext;
|
||||
_disposed = false;
|
||||
|
||||
constructor(context: ExecutionContext<ElementHandle>) {
|
||||
constructor(context: ExecutionContext) {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
executionContext(): ExecutionContext<ElementHandle> {
|
||||
executionContext(): ExecutionContext {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
evaluate: types.EvaluateOn<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||
evaluate: types.EvaluateOn<JSHandle> = (pageFunction, ...args) => {
|
||||
return this._context.evaluate(pageFunction, this, ...args);
|
||||
}
|
||||
|
||||
evaluateHandle: types.EvaluateHandleOn<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||
evaluateHandle: types.EvaluateHandleOn<JSHandle> = (pageFunction, ...args) => {
|
||||
return this._context.evaluateHandle(pageFunction, this, ...args);
|
||||
}
|
||||
|
||||
async getProperty(propertyName: string): Promise<JSHandle<ElementHandle> | null> {
|
||||
async getProperty(propertyName: string): Promise<JSHandle | null> {
|
||||
const objectHandle = await this.evaluateHandle((object, propertyName) => {
|
||||
const result = {__proto__: null};
|
||||
result[propertyName] = object[propertyName];
|
||||
|
@ -90,7 +91,7 @@ export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle>>
|
|||
return result;
|
||||
}
|
||||
|
||||
getProperties(): Promise<Map<string, JSHandle<ElementHandle>>> {
|
||||
getProperties(): Promise<Map<string, JSHandle>> {
|
||||
return this._context._delegate.getProperties(this);
|
||||
}
|
||||
|
||||
|
@ -98,7 +99,7 @@ export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle>>
|
|||
return this._context._delegate.handleJSONValue(this);
|
||||
}
|
||||
|
||||
asElement(): ElementHandle | null {
|
||||
asElement(): dom.ElementHandle | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,9 +50,9 @@ export function filterCookies(cookies: NetworkCookie[], urls: string[]) {
|
|||
|
||||
export type Headers = { [key: string]: string };
|
||||
|
||||
export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
_response: Response<ElementHandle> | null = null;
|
||||
_redirectChain: Request<ElementHandle>[];
|
||||
export class Request {
|
||||
_response: Response | null = null;
|
||||
_redirectChain: Request[];
|
||||
private _isNavigationRequest: boolean;
|
||||
private _failureText: string | null = null;
|
||||
private _url: string;
|
||||
|
@ -60,9 +60,9 @@ export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
private _method: string;
|
||||
private _postData: string;
|
||||
private _headers: Headers;
|
||||
private _frame: frames.Frame<ElementHandle>;
|
||||
private _frame: frames.Frame;
|
||||
|
||||
constructor(frame: frames.Frame<ElementHandle> | null, redirectChain: Request<ElementHandle>[], isNavigationRequest: boolean,
|
||||
constructor(frame: frames.Frame | null, redirectChain: Request[], isNavigationRequest: boolean,
|
||||
url: string, resourceType: string, method: string, postData: string, headers: Headers) {
|
||||
this._frame = frame;
|
||||
this._redirectChain = redirectChain;
|
||||
|
@ -98,11 +98,11 @@ export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
return this._headers;
|
||||
}
|
||||
|
||||
response(): Response<ElementHandle> | null {
|
||||
response(): Response | null {
|
||||
return this._response;
|
||||
}
|
||||
|
||||
frame(): frames.Frame<ElementHandle> | null {
|
||||
frame(): frames.Frame | null {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
|||
return this._isNavigationRequest;
|
||||
}
|
||||
|
||||
redirectChain(): Request<ElementHandle>[] {
|
||||
redirectChain(): Request[] {
|
||||
return this._redirectChain.slice();
|
||||
}
|
||||
|
||||
|
@ -130,8 +130,8 @@ export type RemoteAddress = {
|
|||
|
||||
type GetResponseBodyCallback = () => Promise<Buffer>;
|
||||
|
||||
export class Response<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
private _request: Request<ElementHandle>;
|
||||
export class Response {
|
||||
private _request: Request;
|
||||
private _contentPromise: Promise<Buffer> | null = null;
|
||||
private _bodyLoadedPromise: Promise<Error | null>;
|
||||
private _bodyLoadedPromiseFulfill: any;
|
||||
|
@ -142,7 +142,7 @@ export class Response<ElementHandle extends types.ElementHandle<ElementHandle>>
|
|||
private _headers: Headers;
|
||||
private _getResponseBodyCallback: GetResponseBodyCallback;
|
||||
|
||||
constructor(request: Request<ElementHandle>, status: number, statusText: string, headers: Headers, remoteAddress: RemoteAddress, getResponseBodyCallback: GetResponseBodyCallback) {
|
||||
constructor(request: Request, status: number, statusText: string, headers: Headers, remoteAddress: RemoteAddress, getResponseBodyCallback: GetResponseBodyCallback) {
|
||||
this._request = request;
|
||||
this._request._response = this;
|
||||
this._status = status;
|
||||
|
@ -205,11 +205,11 @@ export class Response<ElementHandle extends types.ElementHandle<ElementHandle>>
|
|||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
request(): Request<ElementHandle> {
|
||||
request(): Request {
|
||||
return this._request;
|
||||
}
|
||||
|
||||
frame(): frames.Frame<ElementHandle> | null {
|
||||
frame(): frames.Frame | null {
|
||||
return this._request.frame();
|
||||
}
|
||||
}
|
||||
|
|
19
src/types.ts
19
src/types.ts
|
@ -1,9 +1,6 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as input from './input';
|
||||
import * as js from './javascript';
|
||||
|
||||
type Boxed<Args extends any[], Handle> = { [Index in keyof Args]: Args[Index] | Handle };
|
||||
type PageFunction<Args extends any[], R = any> = string | ((...args: Args) => R | Promise<R>);
|
||||
type PageFunctionOn<On, Args extends any[], R = any> = string | ((on: On, ...args: Args) => R | Promise<R>);
|
||||
|
@ -14,19 +11,3 @@ export type $Eval<Handle> = <Args extends any[], R>(selector: string, pageFuncti
|
|||
export type $$Eval<Handle> = <Args extends any[], R>(selector: string, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args, Handle>) => Promise<R>;
|
||||
export type EvaluateOn<Handle> = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args, Handle>) => Promise<R>;
|
||||
export type EvaluateHandleOn<Handle> = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args, Handle>) => Promise<Handle>;
|
||||
|
||||
export interface ElementHandle<EHandle extends ElementHandle<EHandle>> extends js.JSHandle<EHandle> {
|
||||
$(selector: string): Promise<EHandle | null>;
|
||||
$x(expression: string): Promise<EHandle[]>;
|
||||
$$(selector: string): Promise<EHandle[]>;
|
||||
$eval: $Eval<js.JSHandle<EHandle>>;
|
||||
$$eval: $$Eval<js.JSHandle<EHandle>>;
|
||||
click(options?: input.ClickOptions): Promise<void>;
|
||||
dblclick(options?: input.MultiClickOptions): Promise<void>;
|
||||
tripleclick(options?: input.MultiClickOptions): Promise<void>;
|
||||
fill(value: string): Promise<void>;
|
||||
focus(): Promise<void>;
|
||||
hover(options?: input.PointerActionOptions): Promise<void>;
|
||||
select(...values: (string | EHandle | input.SelectOption)[]): Promise<string[]>;
|
||||
type(text: string, options: { delay: (number | undefined); } | undefined): Promise<void>;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the MIT license.
|
||||
|
||||
import { assert, helper } from './helper';
|
||||
import * as types from './types';
|
||||
import * as js from './javascript';
|
||||
import { TimeoutError } from './Errors';
|
||||
|
||||
|
@ -15,12 +14,12 @@ export type WaitTaskParams = {
|
|||
args: any[];
|
||||
};
|
||||
|
||||
export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||
readonly promise: Promise<js.JSHandle<ElementHandle>>;
|
||||
export class WaitTask {
|
||||
readonly promise: Promise<js.JSHandle>;
|
||||
private _cleanup: () => void;
|
||||
private _params: WaitTaskParams & { predicateBody: string };
|
||||
private _runCount: number;
|
||||
private _resolve: (result: js.JSHandle<ElementHandle>) => void;
|
||||
private _resolve: (result: js.JSHandle) => void;
|
||||
private _reject: (reason: Error) => void;
|
||||
private _timeoutTimer: NodeJS.Timer;
|
||||
private _terminated: boolean;
|
||||
|
@ -39,7 +38,7 @@ export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle>>
|
|||
};
|
||||
this._cleanup = cleanup;
|
||||
this._runCount = 0;
|
||||
this.promise = new Promise<js.JSHandle<ElementHandle>>((resolve, reject) => {
|
||||
this.promise = new Promise<js.JSHandle>((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
});
|
||||
|
@ -57,9 +56,9 @@ export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle>>
|
|||
this._doCleanup();
|
||||
}
|
||||
|
||||
async rerun(context: js.ExecutionContext<ElementHandle>) {
|
||||
async rerun(context: js.ExecutionContext) {
|
||||
const runCount = ++this._runCount;
|
||||
let success: js.JSHandle<ElementHandle> | null = null;
|
||||
let success: js.JSHandle | null = null;
|
||||
let error = null;
|
||||
try {
|
||||
success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args);
|
||||
|
|
|
@ -18,17 +18,17 @@
|
|||
import { TargetSession } from './Connection';
|
||||
import { helper } from '../helper';
|
||||
import { valueFromRemoteObject, releaseObject } from './protocolHelper';
|
||||
import { createJSHandle, ElementHandle } from './JSHandle';
|
||||
import { createJSHandle } from './JSHandle';
|
||||
import { Protocol } from './protocol';
|
||||
import * as js from '../javascript';
|
||||
|
||||
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
||||
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
||||
|
||||
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
|
||||
export type JSHandle = js.JSHandle<ElementHandle>;
|
||||
export type ExecutionContext = js.ExecutionContext;
|
||||
export type JSHandle = js.JSHandle;
|
||||
|
||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
|
||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
|
||||
private _globalObjectId?: string;
|
||||
_session: TargetSession;
|
||||
_contextId: number;
|
||||
|
|
|
@ -20,11 +20,11 @@ import { TimeoutError } from '../Errors';
|
|||
import * as frames from '../frames';
|
||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||
import * as js from '../javascript';
|
||||
import * as dom from '../dom';
|
||||
import { TimeoutSettings } from '../TimeoutSettings';
|
||||
import { TargetSession } from './Connection';
|
||||
import { Events } from './events';
|
||||
import { ExecutionContext, ExecutionContextDelegate } from './ExecutionContext';
|
||||
import { ElementHandle } from './JSHandle';
|
||||
import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager';
|
||||
import { Page } from './Page';
|
||||
import { Protocol } from './protocol';
|
||||
|
@ -42,9 +42,9 @@ type FrameData = {
|
|||
id: string,
|
||||
};
|
||||
|
||||
export type Frame = frames.Frame<ElementHandle>;
|
||||
export type Frame = frames.Frame;
|
||||
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
|
||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate {
|
||||
_session: TargetSession;
|
||||
_page: Page;
|
||||
_networkManager: NetworkManager;
|
||||
|
@ -277,7 +277,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate<E
|
|||
return watchDog.waitForNavigation();
|
||||
}
|
||||
|
||||
async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle> {
|
||||
async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise<dom.ElementHandle> {
|
||||
assert(false, 'Multiple isolated worlds are not implemented');
|
||||
return elementHandle;
|
||||
}
|
||||
|
|
|
@ -16,90 +16,113 @@
|
|||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { assert, debugError, helper } from '../helper';
|
||||
import { debugError, helper } from '../helper';
|
||||
import * as input from '../input';
|
||||
import * as dom from '../dom';
|
||||
import * as frames from '../frames';
|
||||
import { TargetSession } from './Connection';
|
||||
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext';
|
||||
import { ExecutionContext, ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
|
||||
import { FrameManager } from './FrameManager';
|
||||
import { Page } from './Page';
|
||||
import { Protocol } from './protocol';
|
||||
import Injected from '../injected/injected';
|
||||
import * as types from '../types';
|
||||
import * as js from '../javascript';
|
||||
|
||||
type SelectorRoot = Element | ShadowRoot | Document;
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
|
||||
const delegate = context._delegate as ExecutionContextDelegate;
|
||||
const frame = context.frame();
|
||||
if (remoteObject.subtype === 'node' && frame) {
|
||||
const frameManager = frame._delegate as FrameManager;
|
||||
return new ElementHandle(context, delegate._session, remoteObject, frameManager.page(), frameManager);
|
||||
const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._session, frameManager);
|
||||
return new dom.ElementHandle(context, frameManager.page().keyboard, frameManager.page().mouse, delegate);
|
||||
}
|
||||
const handle = new js.JSHandle(context);
|
||||
markJSHandle(handle, remoteObject);
|
||||
return handle;
|
||||
}
|
||||
|
||||
export class ElementHandle extends js.JSHandle<ElementHandle> {
|
||||
class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
private _client: TargetSession;
|
||||
private _remoteObject: Protocol.Runtime.RemoteObject;
|
||||
private _page: Page;
|
||||
private _frameManager: FrameManager;
|
||||
|
||||
constructor(context: ExecutionContext, client: TargetSession, remoteObject: Protocol.Runtime.RemoteObject, page: Page, frameManager: FrameManager) {
|
||||
super(context);
|
||||
constructor(client: TargetSession, frameManager: FrameManager) {
|
||||
this._client = client;
|
||||
this._remoteObject = remoteObject;
|
||||
this._page = page;
|
||||
this._frameManager = frameManager;
|
||||
markJSHandle(this, remoteObject);
|
||||
}
|
||||
|
||||
asElement(): ElementHandle | null {
|
||||
return this;
|
||||
async contentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
|
||||
throw new Error('contentFrame() is not implemented');
|
||||
}
|
||||
|
||||
async _scrollIntoViewIfNeeded() {
|
||||
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
|
||||
if (!element.isConnected)
|
||||
return 'Node is detached from document';
|
||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'Node is not of type HTMLElement';
|
||||
// force-scroll if page's javascript is disabled.
|
||||
if (!pageJavascriptEnabled) {
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
isJavascriptEnabled(): boolean {
|
||||
return this._frameManager.page()._javascriptEnabled;
|
||||
}
|
||||
|
||||
async boundingBox(handle: dom.ElementHandle): Promise<dom.Rect | null> {
|
||||
throw new Error('boundingBox() is not implemented');
|
||||
}
|
||||
|
||||
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
|
||||
const objectId = toRemoteObject(handle).objectId;
|
||||
this._client.send('DOM.getDocument');
|
||||
const {nodeId} = await this._client.send('DOM.requestNode', {objectId});
|
||||
const result = await this._client.send('Page.snapshotNode', {nodeId});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise<dom.Point> {
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
if (!relativePoint)
|
||||
return this._clickablePoint(handle);
|
||||
const box = await this.boundingBox(handle);
|
||||
return { x: box.x + relativePoint.x, y: box.y + relativePoint.y };
|
||||
}
|
||||
|
||||
private async _clickablePoint(handle: dom.ElementHandle): Promise<dom.Point> {
|
||||
const fromProtocolQuad = (quad: number[]): dom.Point[] => {
|
||||
return [
|
||||
{x: quad[0], y: quad[1]},
|
||||
{x: quad[2], y: quad[3]},
|
||||
{x: quad[4], y: quad[5]},
|
||||
{x: quad[6], y: quad[7]}
|
||||
];
|
||||
};
|
||||
|
||||
const intersectQuadWithViewport = (quad: dom.Point[], width: number, height: number): dom.Point[] => {
|
||||
return quad.map(point => ({
|
||||
x: Math.min(Math.max(point.x, 0), width),
|
||||
y: Math.min(Math.max(point.y, 0), height),
|
||||
}));
|
||||
};
|
||||
|
||||
const computeQuadArea = (quad: dom.Point[]) => {
|
||||
// Compute sum of all directed areas of adjacent triangles
|
||||
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
||||
let area = 0;
|
||||
for (let i = 0; i < quad.length; ++i) {
|
||||
const p1 = quad[i];
|
||||
const p2 = quad[(i + 1) % quad.length];
|
||||
area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
||||
}
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
});
|
||||
if (visibleRatio !== 1.0)
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
}, this._page._javascriptEnabled);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
}
|
||||
return Math.abs(area);
|
||||
};
|
||||
|
||||
async _clickablePoint() {
|
||||
const [result, viewport] = await Promise.all([
|
||||
this._client.send('DOM.getContentQuads', {
|
||||
objectId: this._remoteObject.objectId
|
||||
objectId: toRemoteObject(handle).objectId
|
||||
}).catch(debugError),
|
||||
this._page.evaluate(() => ({ clientWidth: innerWidth, clientHeight: innerHeight })),
|
||||
handle.evaluate(() => ({ clientWidth: innerWidth, clientHeight: innerHeight })),
|
||||
]);
|
||||
if (!result || !result.quads.length)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
// Filter out quads that have too small area to click into.
|
||||
const {clientWidth, clientHeight} = viewport;
|
||||
const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).map(quad => this._intersectQuadWithViewport(quad, clientWidth, clientHeight)).filter(quad => computeQuadArea(quad) > 1);
|
||||
const quads = result.quads.map(fromProtocolQuad)
|
||||
.map(quad => intersectQuadWithViewport(quad, clientWidth, clientHeight))
|
||||
.filter(quad => computeQuadArea(quad) > 1);
|
||||
if (!quads.length)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
// Return the middle point of the first quad.
|
||||
|
@ -116,190 +139,8 @@ export class ElementHandle extends js.JSHandle<ElementHandle> {
|
|||
};
|
||||
}
|
||||
|
||||
_fromProtocolQuad(quad: number[]): Array<{ x: number; y: number; }> {
|
||||
return [
|
||||
{x: quad[0], y: quad[1]},
|
||||
{x: quad[2], y: quad[3]},
|
||||
{x: quad[4], y: quad[5]},
|
||||
{x: quad[6], y: quad[7]}
|
||||
];
|
||||
}
|
||||
|
||||
_intersectQuadWithViewport(quad: Array<{ x: number; y: number; }>, width: number, height: number): Array<{ x: number; y: number; }> {
|
||||
return quad.map(point => ({
|
||||
x: Math.min(Math.max(point.x, 0), width),
|
||||
y: Math.min(Math.max(point.y, 0), height),
|
||||
}));
|
||||
}
|
||||
|
||||
async hover(): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.move(x, y);
|
||||
}
|
||||
|
||||
async click(options?: input.ClickOptions): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.click(x, y, options);
|
||||
}
|
||||
|
||||
async dblclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.dblclick(x, y, options);
|
||||
}
|
||||
|
||||
async tripleclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.tripleclick(x, y, options);
|
||||
}
|
||||
|
||||
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
|
||||
const options = values.map(value => typeof value === 'object' ? value : { value });
|
||||
for (const option of options) {
|
||||
if (option instanceof ElementHandle)
|
||||
continue;
|
||||
if (option.value !== undefined)
|
||||
assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"');
|
||||
if (option.label !== undefined)
|
||||
assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"');
|
||||
if (option.index !== undefined)
|
||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
||||
}
|
||||
return this.evaluate(input.selectFunction, ...options);
|
||||
}
|
||||
|
||||
async fill(value: string): Promise<void> {
|
||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
const error = await this.evaluate(input.fillFunction);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
await this.focus();
|
||||
await this._page.keyboard.sendCharacters(value);
|
||||
}
|
||||
|
||||
async setInputFiles(...files: (string|input.FilePayload)[]) {
|
||||
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
|
||||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||
const filePayloads = await input.loadFiles(files);
|
||||
const objectId = this._remoteObject.objectId;
|
||||
await this._client.send('DOM.setInputFiles', { objectId, files: filePayloads });
|
||||
}
|
||||
|
||||
async focus() {
|
||||
await this.evaluate(element => element.focus());
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
||||
await this.focus();
|
||||
await this._page.keyboard.type(text, options);
|
||||
}
|
||||
|
||||
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
|
||||
await this.focus();
|
||||
await this._page.keyboard.press(key, options);
|
||||
}
|
||||
|
||||
async screenshot(options: {path?: string} = {}): Promise<string | Buffer> {
|
||||
const objectId = this._remoteObject.objectId;
|
||||
this._client.send('DOM.getDocument');
|
||||
const {nodeId} = await this._client.send('DOM.requestNode', {objectId});
|
||||
const result = await this._client.send('Page.snapshotNode', {nodeId});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
const handle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelector('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(element, selector) => element.querySelectorAll(selector),
|
||||
selector
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
$eval: types.$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await elementHandle.evaluate(pageFunction, ...args as any);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
$$eval: types.$$Eval<JSHandle> = async (selector, pageFunction, ...args) => {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, selector: string, injected: Injected) => injected.querySelectorAll('css=' + selector, root),
|
||||
selector, await this._context._injected()
|
||||
);
|
||||
|
||||
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
const arrayHandle = await this.evaluateHandle(
|
||||
(root: SelectorRoot, expression: string, injected: Injected) => injected.querySelectorAll('xpath=' + expression, root),
|
||||
expression, await this._context._injected()
|
||||
);
|
||||
const properties = await arrayHandle.getProperties();
|
||||
await arrayHandle.dispose();
|
||||
const result = [];
|
||||
for (const property of properties.values()) {
|
||||
const elementHandle = property.asElement();
|
||||
if (elementHandle)
|
||||
result.push(elementHandle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isIntersectingViewport(): Promise<boolean> {
|
||||
return this.evaluate(async element => {
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
});
|
||||
return visibleRatio > 0;
|
||||
});
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
const objectId = toRemoteObject(handle);
|
||||
await this._client.send('DOM.setInputFiles', { objectId, files });
|
||||
}
|
||||
}
|
||||
|
||||
function computeQuadArea(quad) {
|
||||
// Compute sum of all directed areas of adjacent triangles
|
||||
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
||||
let area = 0;
|
||||
for (let i = 0; i < quad.length; ++i) {
|
||||
const p1 = quad[i];
|
||||
const p2 = quad[(i + 1) % quad.length];
|
||||
area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
||||
}
|
||||
return Math.abs(area);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import { Frame, FrameManager } from './FrameManager';
|
|||
import { assert, helper, RegisteredListener } from '../helper';
|
||||
import { Protocol } from './protocol';
|
||||
import * as network from '../network';
|
||||
import { ElementHandle } from './JSHandle';
|
||||
|
||||
export const NetworkManagerEvents = {
|
||||
Request: Symbol('Events.NetworkManager.Request'),
|
||||
|
@ -30,8 +29,8 @@ export const NetworkManagerEvents = {
|
|||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
||||
};
|
||||
|
||||
export type Request = network.Request<ElementHandle>;
|
||||
export type Response = network.Response<ElementHandle>;
|
||||
export type Request = network.Request;
|
||||
export type Response = network.Response;
|
||||
|
||||
export class NetworkManager extends EventEmitter {
|
||||
private _sesssion: TargetSession;
|
||||
|
@ -171,7 +170,7 @@ export class NetworkManager extends EventEmitter {
|
|||
|
||||
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||
|
||||
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
|
||||
export function toInterceptableRequest(request: network.Request): InterceptableRequest {
|
||||
return (request as any)[interceptableRequestSymbol];
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import { TargetSession, TargetSessionEvents } from './Connection';
|
|||
import { Events } from './events';
|
||||
import { Frame, FrameManager, FrameManagerEvents } from './FrameManager';
|
||||
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||
import { createJSHandle, ElementHandle } from './JSHandle';
|
||||
import { createJSHandle } from './JSHandle';
|
||||
import { JSHandle, toRemoteObject } from './ExecutionContext';
|
||||
import { NetworkManagerEvents, Response } from './NetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
|
@ -35,6 +35,7 @@ import { Target } from './Target';
|
|||
import { TaskQueue } from './TaskQueue';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
import * as dom from '../dom';
|
||||
import { Dialog, DialogType } from './Dialog';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
@ -219,7 +220,7 @@ export class Page extends EventEmitter {
|
|||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<ElementHandle | null> {
|
||||
async $(selector: string): Promise<dom.ElementHandle | null> {
|
||||
return this.mainFrame().$(selector);
|
||||
}
|
||||
|
||||
|
@ -236,19 +237,19 @@ export class Page extends EventEmitter {
|
|||
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle[]> {
|
||||
async $$(selector: string): Promise<dom.ElementHandle[]> {
|
||||
return this.mainFrame().$$(selector);
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
async $x(expression: string): Promise<dom.ElementHandle[]> {
|
||||
return this.mainFrame().$x(expression);
|
||||
}
|
||||
|
||||
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<ElementHandle> {
|
||||
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addScriptTag(options);
|
||||
}
|
||||
|
||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
|
||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addStyleTag(options);
|
||||
}
|
||||
|
||||
|
@ -458,7 +459,7 @@ export class Page extends EventEmitter {
|
|||
if (!this._fileChooserInterceptors.size)
|
||||
return;
|
||||
const context = await this._frameManager.frame(event.frameId)._utilityContext();
|
||||
const handle = createJSHandle(context, event.element) as ElementHandle;
|
||||
const handle = createJSHandle(context, event.element) as dom.ElementHandle;
|
||||
const interceptors = Array.from(this._fileChooserInterceptors);
|
||||
this._fileChooserInterceptors.clear();
|
||||
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
|
||||
|
@ -508,11 +509,11 @@ export class Page extends EventEmitter {
|
|||
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
||||
}
|
||||
|
||||
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
|
||||
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
|
||||
return this.mainFrame().waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<ElementHandle | null> {
|
||||
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
|
||||
return this.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
|
@ -588,6 +589,6 @@ export class ConsoleMessage {
|
|||
}
|
||||
|
||||
type FileChooser = {
|
||||
element: ElementHandle,
|
||||
element: dom.ElementHandle,
|
||||
multiple: boolean
|
||||
};
|
||||
|
|
|
@ -5,9 +5,9 @@ export { TimeoutError } from '../Errors';
|
|||
export { Browser, BrowserContext } from './Browser';
|
||||
export { BrowserFetcher } from './BrowserFetcher';
|
||||
export { ExecutionContext, JSHandle } from '../javascript';
|
||||
export { Frame } from './FrameManager';
|
||||
export { Frame } from '../frames';
|
||||
export { Mouse, Keyboard } from '../input';
|
||||
export { ElementHandle } from './JSHandle';
|
||||
export { ElementHandle } from '../dom';
|
||||
export { Request, Response } from '../network';
|
||||
export { ConsoleMessage, Page } from './Page';
|
||||
export { Playwright } from './Playwright';
|
||||
|
|
Загрузка…
Ссылка в новой задаче