dgozman comments

This commit is contained in:
Joel Einbinder 2019-12-05 20:48:09 +00:00
parent fc5898892b
commit 40b64a5b60
11 changed files with 67 additions and 51 deletions

View file

@ -194,11 +194,11 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise<dom.ElementHandle> {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.DOMWorld): Promise<dom.ElementHandle<T>> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId,
});
return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to);
return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise<dom.ElementHandle<T>>;
}
async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.DOMWorld): Promise<dom.ElementHandle> {

View file

@ -222,7 +222,7 @@ export class Page extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector: string | types.Selector): Promise<dom.ElementHandle | null> {
async $(selector: string | types.Selector): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().$(selector);
}
@ -239,11 +239,11 @@ export class Page extends EventEmitter {
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string | types.Selector): Promise<dom.ElementHandle[]> {
async $$(selector: string | types.Selector): Promise<dom.ElementHandle<Element>[]> {
return this.mainFrame().$$(selector);
}
async $x(expression: string): Promise<dom.ElementHandle[]> {
async $x(expression: string): Promise<dom.ElementHandle<Element>[]> {
return this.mainFrame().$x(expression);
}

View file

@ -30,7 +30,7 @@ export class Playwright {
this._launcher = new Launcher(projectRoot, preferredRevision);
}
launch(options: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise<Browser> {
launch(options?: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise<Browser> {
return this._launcher.launch(options);
}

View file

@ -23,10 +23,10 @@ export interface DOMWorldDelegate {
screenshot(handle: ElementHandle, options?: any): Promise<string | Buffer>;
ensurePointerActionPoint(handle: ElementHandle, relativePoint?: types.Point): Promise<types.Point>;
setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise<void>;
adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise<ElementHandle>;
adoptElementHandle<T extends Node>(handle: ElementHandle<T>, to: DOMWorld): Promise<ElementHandle<T>>;
}
export type ScopedSelector = types.Selector & { scope?: ElementHandle };
type ScopedSelector = types.Selector & { scope?: ElementHandle };
type ResolvedSelector = { scope?: ElementHandle, selector: string, visible?: boolean, disposeScope?: boolean };
export class DOMWorld {
@ -59,7 +59,7 @@ export class DOMWorld {
return this._injectedPromise;
}
async adoptElementHandle(handle: ElementHandle): Promise<ElementHandle> {
async adoptElementHandle<T extends Node>(handle: ElementHandle<T>): Promise<ElementHandle<T>> {
assert(handle.executionContext() !== this.context, 'Should not adopt to the same context');
return this.delegate.adoptElementHandle(handle, this);
}
@ -74,11 +74,11 @@ export class DOMWorld {
return { scope: selector.scope, selector: normalizeSelector(selector.selector), visible: selector.visible };
}
async $(selector: string | ScopedSelector): Promise<ElementHandle | null> {
async $(selector: string | ScopedSelector): Promise<ElementHandle<Element> | null> {
const resolved = await this.resolveSelector(selector);
const handle = await this.context.evaluateHandle(
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
const element = injected.querySelector(selector, scope || document);
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
const element = injected.querySelector(selector, scope as SelectorRoot || document);
if (visible === undefined || !element)
return element;
return injected.isVisible(element) === visible ? element : undefined;
@ -92,11 +92,11 @@ export class DOMWorld {
return handle.asElement();
}
async $$(selector: string | ScopedSelector): Promise<ElementHandle[]> {
async $$(selector: string | ScopedSelector): Promise<ElementHandle<Element>[]> {
const resolved = await this.resolveSelector(selector);
const arrayHandle = await this.context.evaluateHandle(
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
const elements = injected.querySelectorAll(selector, scope || document);
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
const elements = injected.querySelectorAll(selector, scope as SelectorRoot || document);
if (visible !== undefined)
return elements.filter(element => injected.isVisible(element) === visible);
return elements;
@ -130,8 +130,8 @@ export class DOMWorld {
$$eval: types.$$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
const resolved = await this.resolveSelector(selector);
const arrayHandle = await this.context.evaluateHandle(
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
const elements = injected.querySelectorAll(selector, scope || document);
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
const elements = injected.querySelectorAll(selector, scope as SelectorRoot || document);
if (visible !== undefined)
return elements.filter(element => injected.isVisible(element) === visible);
return elements;
@ -144,7 +144,7 @@ export class DOMWorld {
}
}
export class ElementHandle extends js.JSHandle {
export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
private readonly _world: DOMWorld;
constructor(context: js.ExecutionContext, remoteObject: any) {
@ -153,7 +153,7 @@ export class ElementHandle extends js.JSHandle {
this._world = context._domWorld;
}
asElement(): ElementHandle | null {
asElement(): ElementHandle<T> | null {
return this;
}
@ -162,13 +162,15 @@ export class ElementHandle extends js.JSHandle {
}
async _scrollIntoViewIfNeeded() {
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
if (!element.isConnected)
const error = await this.evaluate(async (node: Node, pageJavascriptEnabled: boolean) => {
if (!node.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
if (node.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
const element = node as Element;
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
//@ts-ignore because only Chromium still supports 'instant'
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}
@ -182,8 +184,10 @@ export class ElementHandle extends js.JSHandle {
// there are rafs.
requestAnimationFrame(() => {});
});
if (visibleRatio !== 1.0)
if (visibleRatio !== 1.0) {
//@ts-ignore because only Chromium still supports 'instant'
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
}
return false;
}, this._world.delegate.isJavascriptEnabled());
if (error)
@ -242,13 +246,13 @@ export class ElementHandle extends js.JSHandle {
}
async setInputFiles(...files: (string|input.FilePayload)[]) {
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
const multiple = await this.evaluate((element: Node) => !!(element as HTMLInputElement).multiple);
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
await this._world.delegate.setInputFiles(this, await input.loadFiles(files));
}
async focus() {
await this.evaluate(element => element.focus());
await this.evaluate((element: Node) => (element as HTMLElement).focus());
}
async type(text: string, options: { delay: (number | undefined); } | undefined) {
@ -280,7 +284,7 @@ export class ElementHandle extends js.JSHandle {
return this._world.$(this._scopedSelector(selector));
}
$$(selector: string | types.Selector): Promise<ElementHandle[]> {
$$(selector: string | types.Selector): Promise<ElementHandle<Element>[]> {
return this._world.$$(this._scopedSelector(selector));
}
@ -292,12 +296,15 @@ export class ElementHandle extends js.JSHandle {
return this._world.$$eval(this._scopedSelector(selector), pageFunction, ...args as any);
}
$x(expression: string): Promise<ElementHandle[]> {
$x(expression: string): Promise<ElementHandle<Element>[]> {
return this._world.$$({ scope: this, selector: 'xpath=' + expression });
}
isIntersectingViewport(): Promise<boolean> {
return this.evaluate(async element => {
return this.evaluate(async (node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE)
throw new Error('Node is not of type HTMLElement');
const element = node as Element;
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
@ -348,7 +355,7 @@ export function waitForSelectorTask(selector: string | ScopedSelector, timeout:
return async (domWorld: DOMWorld) => {
// TODO: we should not be able to adopt selector scope from a different document - handle this case.
const resolved = await domWorld.resolveSelector(selector);
return domWorld.context.evaluateHandle((injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined, timeout: number) => {
return domWorld.context.evaluateHandle((injected: Injected, selector: string, scope: Node | undefined, visible: boolean | undefined, timeout: number) => {
if (visible !== undefined)
return injected.pollRaf(predicate, timeout);
return injected.pollMutation(predicate, timeout);

View file

@ -22,6 +22,7 @@ import * as types from '../types';
import * as frames from '../frames';
import { JugglerSession } from './Connection';
import { FrameManager } from './FrameManager';
import { Protocol } from './protocol';
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
readonly keyboard: input.Keyboard;
@ -138,13 +139,13 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise<dom.ElementHandle> {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.DOMWorld): Promise<dom.ElementHandle<T>> {
assert(false, 'Multiple isolated worlds are not implemented');
return handle;
}
}
function toRemoteObject(handle: dom.ElementHandle): any {
function toRemoteObject(handle: dom.ElementHandle): Protocol.RemoteObject {
return handle._remoteObject;
}

View file

@ -122,12 +122,12 @@ export class Frame {
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string | types.Selector): Promise<dom.ElementHandle | null> {
async $(selector: string | types.Selector): Promise<dom.ElementHandle<Element> | null> {
const domWorld = await this._mainDOMWorld();
return domWorld.$(types.clearSelector(selector));
}
async $x(expression: string): Promise<dom.ElementHandle[]> {
async $x(expression: string): Promise<dom.ElementHandle<Element>[]> {
const domWorld = await this._mainDOMWorld();
return domWorld.$$('xpath=' + expression);
}
@ -142,7 +142,7 @@ export class Frame {
return domWorld.$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string | types.Selector): Promise<dom.ElementHandle[]> {
async $$(selector: string | types.Selector): Promise<dom.ElementHandle<Element>[]> {
const domWorld = await this._mainDOMWorld();
return domWorld.$$(types.clearSelector(selector));
}

View file

@ -17,9 +17,11 @@ class Injected {
this.engines.set(engine.name, engine);
}
querySelector(selector: string, root: SelectorRoot): Element | undefined {
querySelector(selector: string, root: Node): Element | undefined {
const parsed = this._parseSelector(selector);
let element = root;
if (!root["querySelector"])
throw new Error('Node is not queryable.');
let element = root as SelectorRoot;
for (const { engine, selector } of parsed) {
const next = engine.query((element as Element).shadowRoot || element, selector);
if (!next)

View file

@ -287,9 +287,10 @@ export class Mouse {
}
}
export const selectFunction = (element: HTMLSelectElement, ...optionsToSelect: (Node | SelectOption)[]) => {
if (element.nodeName.toLowerCase() !== 'select')
export const selectFunction = (node: Node, ...optionsToSelect: (Node | SelectOption)[]) => {
if (node.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const element = node as HTMLSelectElement;
const options = Array.from(element.options);
element.value = undefined;
@ -315,9 +316,10 @@ export const selectFunction = (element: HTMLSelectElement, ...optionsToSelect: (
return options.filter(option => option.selected).map(option => option.value);
};
export const fillFunction = (element: HTMLElement) => {
if (element.nodeType !== Node.ELEMENT_NODE)
export const fillFunction = (node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
const element = node as HTMLElement;
if (element.nodeName.toLowerCase() === 'input') {
const input = element as HTMLInputElement;
const type = input.getAttribute('type') || '';

View file

@ -38,7 +38,7 @@ export class ExecutionContext {
}
}
export class JSHandle {
export class JSHandle<T = any> {
readonly _context: ExecutionContext;
readonly _remoteObject: any;
_disposed = false;
@ -52,11 +52,11 @@ export class JSHandle {
return this._context;
}
evaluate: types.EvaluateOn = (pageFunction, ...args) => {
evaluate: types.EvaluateOn<T> = (pageFunction, ...args) => {
return this._context.evaluate(pageFunction, this, ...args);
}
evaluateHandle: types.EvaluateHandleOn = (pageFunction, ...args) => {
evaluateHandle: types.EvaluateHandleOn<T> = (pageFunction, ...args) => {
return this._context.evaluateHandle(pageFunction, this, ...args);
}
@ -76,7 +76,7 @@ export class JSHandle {
return this._context._delegate.getProperties(this);
}
jsonValue(): Promise<any> {
jsonValue(): Promise<T> {
return this._context._delegate.handleJSONValue(this);
}

View file

@ -3,17 +3,21 @@
import * as js from './javascript';
import { helper } from './helper';
import * as dom from './dom';
type Boxed<Args extends any[]> = { [Index in keyof Args]: Args[Index] | js.JSHandle };
type Boxed<Args extends any[]> = { [Index in keyof Args]: Args[Index] | js.JSHandle<Args[Index]> };
type PageFunction<Args extends any[], R = any> = string | ((...args: Args) => R | Promise<R>);
type PageFunctionOn<On, Args extends any[], R = any> = string | ((on: On, ...args: Args) => R | Promise<R>);
type Handle<T> = T extends Node ? dom.ElementHandle<T> : js.JSHandle<T>;
type ElementForSelector<T> = T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element;
export type Evaluate = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandle = <Args extends any[]>(pageFunction: PageFunction<Args>, ...args: Boxed<Args>) => Promise<js.JSHandle>;
export type $Eval<S = string | Selector> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $$Eval<S = string | Selector> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateOn = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandleOn = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args>) => Promise<js.JSHandle>;
export type EvaluateHandle = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<Handle<R>>;
export type $Eval<O = string | Selector> = <Args extends any[], R, S extends O>(selector: S, pageFunction: PageFunctionOn<ElementForSelector<S>, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $$Eval<O = string | Selector> = <Args extends any[], R, S extends O>(selector: S, pageFunction: PageFunctionOn<ElementForSelector<S>[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateOn<T> = <Args extends any[], R>(pageFunction: PageFunctionOn<T, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandleOn<T> = <Args extends any[], R>(pageFunction: PageFunctionOn<T, Args, R>, ...args: Boxed<Args>) => Promise<Handle<R>>;
export type Rect = { x: number, y: number, width: number, height: number };
export type Point = { x: number, y: number };

View file

@ -141,7 +141,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
await this._client.send('DOM.setInputFiles', { objectId, files });
}
async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise<dom.ElementHandle> {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.DOMWorld): Promise<dom.ElementHandle<T>> {
assert(false, 'Multiple isolated worlds are not implemented');
return handle;
}