mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
232 lines
8.9 KiB
TypeScript
232 lines
8.9 KiB
TypeScript
![]() |
// 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;
|
||
|
});
|
||
|
}
|
||
|
}
|