feat: make JSHandle generic (#140)
This makes it so that JSHandles and ElementHandles are aware of what types they point to. As a fun bonus, `$eval('input')` knows its going to get an HTMLInputElement.
Most of this patch is casting things where previously we just assumed ElementHandles held the right kind of node. This gets us closer to being able to turn on `noImplicityAny` as well.
#6
This commit is contained in:
parent
e4fad11c16
commit
39b22b41c5
|
|
@ -149,7 +149,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
|
||||||
await releaseObject(this._client, toRemoteObject(handle));
|
await releaseObject(this._client, toRemoteObject(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleJSONValue(handle: js.JSHandle): Promise<any> {
|
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
|
||||||
const remoteObject = toRemoteObject(handle);
|
const remoteObject = toRemoteObject(handle);
|
||||||
if (remoteObject.objectId) {
|
if (remoteObject.objectId) {
|
||||||
const response = await this._client.send('Runtime.callFunctionOn', {
|
const response = await this._client.send('Runtime.callFunctionOn', {
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,11 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
await handle.evaluate(input.setFileInputFunction, files);
|
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', {
|
const nodeInfo = await this._client.send('DOM.describeNode', {
|
||||||
objectId: toRemoteObject(handle).objectId,
|
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> {
|
async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.DOMWorld): Promise<dom.ElementHandle> {
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ export class Page extends EventEmitter {
|
||||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
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);
|
return this.mainFrame().$(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,11 +240,11 @@ export class Page extends EventEmitter {
|
||||||
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
|
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$(selector: string | types.Selector): Promise<dom.ElementHandle[]> {
|
async $$(selector: string | types.Selector): Promise<dom.ElementHandle<Element>[]> {
|
||||||
return this.mainFrame().$$(selector);
|
return this.mainFrame().$$(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $x(expression: string): Promise<dom.ElementHandle[]> {
|
async $x(expression: string): Promise<dom.ElementHandle<Element>[]> {
|
||||||
return this.mainFrame().$x(expression);
|
return this.mainFrame().$x(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export class Playwright {
|
||||||
this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'Chromium');
|
this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'Chromium');
|
||||||
}
|
}
|
||||||
|
|
||||||
launch(options: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise<Browser> {
|
launch(options?: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise<Browser> {
|
||||||
return this._launcher.launch(options);
|
return this._launcher.launch(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
59
src/dom.ts
59
src/dom.ts
|
|
@ -24,10 +24,10 @@ export interface DOMWorldDelegate {
|
||||||
boundingBox(handle: ElementHandle): Promise<types.Rect | null>;
|
boundingBox(handle: ElementHandle): Promise<types.Rect | null>;
|
||||||
screenshot(handle: ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer>;
|
screenshot(handle: ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer>;
|
||||||
setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise<void>;
|
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 };
|
type ResolvedSelector = { scope?: ElementHandle, selector: string, visible?: boolean, disposeScope?: boolean };
|
||||||
|
|
||||||
export class DOMWorld {
|
export class DOMWorld {
|
||||||
|
|
@ -60,7 +60,7 @@ export class DOMWorld {
|
||||||
return this._injectedPromise;
|
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');
|
assert(handle.executionContext() !== this.context, 'Should not adopt to the same context');
|
||||||
return this.delegate.adoptElementHandle(handle, this);
|
return this.delegate.adoptElementHandle(handle, this);
|
||||||
}
|
}
|
||||||
|
|
@ -75,10 +75,10 @@ export class DOMWorld {
|
||||||
return { scope: selector.scope, selector: normalizeSelector(selector.selector), visible: selector.visible };
|
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 resolved = await this.resolveSelector(selector);
|
||||||
const handle = await this.context.evaluateHandle(
|
const handle = await this.context.evaluateHandle(
|
||||||
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
|
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
|
||||||
const element = injected.querySelector(selector, scope || document);
|
const element = injected.querySelector(selector, scope || document);
|
||||||
if (visible === undefined || !element)
|
if (visible === undefined || !element)
|
||||||
return element;
|
return element;
|
||||||
|
|
@ -93,10 +93,10 @@ export class DOMWorld {
|
||||||
return handle.asElement();
|
return handle.asElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $$(selector: string | ScopedSelector): Promise<ElementHandle[]> {
|
async $$(selector: string | ScopedSelector): Promise<ElementHandle<Element>[]> {
|
||||||
const resolved = await this.resolveSelector(selector);
|
const resolved = await this.resolveSelector(selector);
|
||||||
const arrayHandle = await this.context.evaluateHandle(
|
const arrayHandle = await this.context.evaluateHandle(
|
||||||
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
|
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
|
||||||
const elements = injected.querySelectorAll(selector, scope || document);
|
const elements = injected.querySelectorAll(selector, scope || document);
|
||||||
if (visible !== undefined)
|
if (visible !== undefined)
|
||||||
return elements.filter(element => injected.isVisible(element) === visible);
|
return elements.filter(element => injected.isVisible(element) === visible);
|
||||||
|
|
@ -131,7 +131,7 @@ export class DOMWorld {
|
||||||
$$eval: types.$$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
|
$$eval: types.$$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
|
||||||
const resolved = await this.resolveSelector(selector);
|
const resolved = await this.resolveSelector(selector);
|
||||||
const arrayHandle = await this.context.evaluateHandle(
|
const arrayHandle = await this.context.evaluateHandle(
|
||||||
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
|
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
|
||||||
const elements = injected.querySelectorAll(selector, scope || document);
|
const elements = injected.querySelectorAll(selector, scope || document);
|
||||||
if (visible !== undefined)
|
if (visible !== undefined)
|
||||||
return elements.filter(element => injected.isVisible(element) === visible);
|
return elements.filter(element => injected.isVisible(element) === visible);
|
||||||
|
|
@ -145,7 +145,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;
|
private readonly _world: DOMWorld;
|
||||||
|
|
||||||
constructor(context: js.ExecutionContext, remoteObject: any) {
|
constructor(context: js.ExecutionContext, remoteObject: any) {
|
||||||
|
|
@ -154,7 +154,7 @@ export class ElementHandle extends js.JSHandle {
|
||||||
this._world = context._domWorld;
|
this._world = context._domWorld;
|
||||||
}
|
}
|
||||||
|
|
||||||
asElement(): ElementHandle | null {
|
asElement(): ElementHandle<T> | null {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,13 +163,15 @@ export class ElementHandle extends js.JSHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _scrollIntoViewIfNeeded() {
|
async _scrollIntoViewIfNeeded() {
|
||||||
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
|
const error = await this.evaluate(async (node: Node, pageJavascriptEnabled: boolean) => {
|
||||||
if (!element.isConnected)
|
if (!node.isConnected)
|
||||||
return 'Node is detached from document';
|
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';
|
return 'Node is not of type HTMLElement';
|
||||||
|
const element = node as Element;
|
||||||
// force-scroll if page's javascript is disabled.
|
// force-scroll if page's javascript is disabled.
|
||||||
if (!pageJavascriptEnabled) {
|
if (!pageJavascriptEnabled) {
|
||||||
|
//@ts-ignore because only Chromium still supports 'instant'
|
||||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -183,8 +185,10 @@ export class ElementHandle extends js.JSHandle {
|
||||||
// there are rafs.
|
// there are rafs.
|
||||||
requestAnimationFrame(() => {});
|
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'});
|
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}, this._world.delegate.isJavascriptEnabled());
|
}, this._world.delegate.isJavascriptEnabled());
|
||||||
if (error)
|
if (error)
|
||||||
|
|
@ -336,13 +340,25 @@ export class ElementHandle extends js.JSHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(...files: (string|input.FilePayload)[]) {
|
async setInputFiles(...files: (string|input.FilePayload)[]) {
|
||||||
const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple);
|
const multiple = await this.evaluate((node: Node) => {
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'INPUT')
|
||||||
|
throw new Error('Node is not an HTMLInputElement');
|
||||||
|
const input = node as HTMLInputElement;
|
||||||
|
return input.multiple;
|
||||||
|
});
|
||||||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||||
await this._world.delegate.setInputFiles(this, await input.loadFiles(files));
|
await this._world.delegate.setInputFiles(this, await input.loadFiles(files));
|
||||||
}
|
}
|
||||||
|
|
||||||
async focus() {
|
async focus() {
|
||||||
await this.evaluate(element => element.focus());
|
const errorMessage = await this.evaluate((element: Node) => {
|
||||||
|
if (!element['focus'])
|
||||||
|
return 'Node is not an HTML or SVG element.';
|
||||||
|
(element as HTMLElement|SVGElement).focus();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (errorMessage)
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
||||||
|
|
@ -374,7 +390,7 @@ export class ElementHandle extends js.JSHandle {
|
||||||
return this._world.$(this._scopedSelector(selector));
|
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));
|
return this._world.$$(this._scopedSelector(selector));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,12 +402,15 @@ export class ElementHandle extends js.JSHandle {
|
||||||
return this._world.$$eval(this._scopedSelector(selector), pageFunction, ...args as any);
|
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 });
|
return this._world.$$({ scope: this, selector: 'xpath=' + expression });
|
||||||
}
|
}
|
||||||
|
|
||||||
isIntersectingViewport(): Promise<boolean> {
|
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 visibleRatio = await new Promise(resolve => {
|
||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver(entries => {
|
||||||
resolve(entries[0].intersectionRatio);
|
resolve(entries[0].intersectionRatio);
|
||||||
|
|
@ -441,7 +460,7 @@ export function waitForFunctionTask(pageFunction: Function | string, options: ty
|
||||||
export function waitForSelectorTask(selector: string | types.Selector, timeout: number): Task {
|
export function waitForSelectorTask(selector: string | types.Selector, timeout: number): Task {
|
||||||
return async (domWorld: DOMWorld) => {
|
return async (domWorld: DOMWorld) => {
|
||||||
const resolved = await domWorld.resolveSelector(selector);
|
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)
|
if (visible !== undefined)
|
||||||
return injected.pollRaf(predicate, timeout);
|
return injected.pollRaf(predicate, timeout);
|
||||||
return injected.pollMutation(predicate, timeout);
|
return injected.pollMutation(predicate, timeout);
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleJSONValue(handle: js.JSHandle): Promise<any> {
|
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
|
||||||
const payload = handle._remoteObject;
|
const payload = handle._remoteObject;
|
||||||
if (!payload.objectId)
|
if (!payload.objectId)
|
||||||
return deserializeValue(payload);
|
return deserializeValue(payload);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import * as types from '../types';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { JugglerSession } from './Connection';
|
import { JugglerSession } from './Connection';
|
||||||
import { FrameManager } from './FrameManager';
|
import { FrameManager } from './FrameManager';
|
||||||
|
import { Protocol } from './protocol';
|
||||||
|
|
||||||
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
readonly keyboard: input.Keyboard;
|
readonly keyboard: input.Keyboard;
|
||||||
|
|
@ -101,13 +102,13 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
await handle.evaluate(input.setFileInputFunction, files);
|
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');
|
assert(false, 'Multiple isolated worlds are not implemented');
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRemoteObject(handle: dom.ElementHandle): any {
|
function toRemoteObject(handle: dom.ElementHandle): Protocol.RemoteObject {
|
||||||
return handle._remoteObject;
|
return handle._remoteObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,12 +122,12 @@ export class Frame {
|
||||||
return context.evaluate(pageFunction, ...args as any);
|
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();
|
const domWorld = await this._mainDOMWorld();
|
||||||
return domWorld.$(types.clearSelector(selector));
|
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();
|
const domWorld = await this._mainDOMWorld();
|
||||||
return domWorld.$$('xpath=' + expression);
|
return domWorld.$$('xpath=' + expression);
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ export class Frame {
|
||||||
return domWorld.$$eval(selector, pageFunction, ...args as any);
|
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();
|
const domWorld = await this._mainDOMWorld();
|
||||||
return domWorld.$$(types.clearSelector(selector));
|
return domWorld.$$(types.clearSelector(selector));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@ class Injected {
|
||||||
this.engines.set(engine.name, engine);
|
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);
|
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) {
|
for (const { engine, selector } of parsed) {
|
||||||
const next = engine.query((element as Element).shadowRoot || element, selector);
|
const next = engine.query((element as Element).shadowRoot || element, selector);
|
||||||
if (!next)
|
if (!next)
|
||||||
|
|
@ -29,9 +31,11 @@ class Injected {
|
||||||
return element as Element;
|
return element as Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
querySelectorAll(selector: string, root: SelectorRoot): Element[] {
|
querySelectorAll(selector: string, root: Node): Element[] {
|
||||||
const parsed = this._parseSelector(selector);
|
const parsed = this._parseSelector(selector);
|
||||||
let set = new Set<SelectorRoot>([ root ]);
|
if (!root["querySelectorAll"])
|
||||||
|
throw new Error('Node is not queryable.');
|
||||||
|
let set = new Set<SelectorRoot>([ root as SelectorRoot ]);
|
||||||
for (const { engine, selector } of parsed) {
|
for (const { engine, selector } of parsed) {
|
||||||
const newSet = new Set<Element>();
|
const newSet = new Set<Element>();
|
||||||
for (const prev of set) {
|
for (const prev of set) {
|
||||||
|
|
|
||||||
10
src/input.ts
10
src/input.ts
|
|
@ -287,9 +287,10 @@ export class Mouse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectFunction = (element: HTMLSelectElement, ...optionsToSelect: (Node | SelectOption)[]) => {
|
export const selectFunction = (node: Node, ...optionsToSelect: (Node | SelectOption)[]) => {
|
||||||
if (element.nodeName.toLowerCase() !== 'select')
|
if (node.nodeName.toLowerCase() !== 'select')
|
||||||
throw new Error('Element is not a <select> element.');
|
throw new Error('Element is not a <select> element.');
|
||||||
|
const element = node as HTMLSelectElement;
|
||||||
|
|
||||||
const options = Array.from(element.options);
|
const options = Array.from(element.options);
|
||||||
element.value = undefined;
|
element.value = undefined;
|
||||||
|
|
@ -315,9 +316,10 @@ export const selectFunction = (element: HTMLSelectElement, ...optionsToSelect: (
|
||||||
return options.filter(option => option.selected).map(option => option.value);
|
return options.filter(option => option.selected).map(option => option.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fillFunction = (element: HTMLElement) => {
|
export const fillFunction = (node: Node) => {
|
||||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||||
return 'Node is not of type HTMLElement';
|
return 'Node is not of type HTMLElement';
|
||||||
|
const element = node as HTMLElement;
|
||||||
if (element.nodeName.toLowerCase() === 'input') {
|
if (element.nodeName.toLowerCase() === 'input') {
|
||||||
const input = element as HTMLInputElement;
|
const input = element as HTMLInputElement;
|
||||||
const type = input.getAttribute('type') || '';
|
const type = input.getAttribute('type') || '';
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export interface ExecutionContextDelegate {
|
||||||
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
|
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
|
||||||
releaseHandle(handle: JSHandle): Promise<void>;
|
releaseHandle(handle: JSHandle): Promise<void>;
|
||||||
handleToString(handle: JSHandle, includeType: boolean): string;
|
handleToString(handle: JSHandle, includeType: boolean): string;
|
||||||
handleJSONValue(handle: JSHandle): Promise<any>;
|
handleJSONValue<T>(handle: JSHandle<T>): Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutionContext {
|
export class ExecutionContext {
|
||||||
|
|
@ -38,7 +38,7 @@ export class ExecutionContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JSHandle {
|
export class JSHandle<T = any> {
|
||||||
readonly _context: ExecutionContext;
|
readonly _context: ExecutionContext;
|
||||||
readonly _remoteObject: any;
|
readonly _remoteObject: any;
|
||||||
_disposed = false;
|
_disposed = false;
|
||||||
|
|
@ -52,11 +52,11 @@ export class JSHandle {
|
||||||
return this._context;
|
return this._context;
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate: types.EvaluateOn = (pageFunction, ...args) => {
|
evaluate: types.EvaluateOn<T> = (pageFunction, ...args) => {
|
||||||
return this._context.evaluate(pageFunction, this, ...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);
|
return this._context.evaluateHandle(pageFunction, this, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ export class JSHandle {
|
||||||
return this._context._delegate.getProperties(this);
|
return this._context._delegate.getProperties(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonValue(): Promise<any> {
|
jsonValue(): Promise<T> {
|
||||||
return this._context._delegate.handleJSONValue(this);
|
return this._context._delegate.handleJSONValue(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
16
src/types.ts
16
src/types.ts
|
|
@ -3,17 +3,21 @@
|
||||||
|
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
import { helper } from './helper';
|
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 PageFunction<Args extends any[], R = any> = string | ((...args: Args) => R | Promise<R>);
|
||||||
type PageFunctionOn<On, Args extends any[], R = any> = string | ((on: On, ...args: Args) => R | Promise<R>);
|
type PageFunctionOn<On, Args extends any[], R = any> = string | ((on: On, ...args: Args) => R | Promise<R>);
|
||||||
|
|
||||||
|
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 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 EvaluateHandle = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<Handle<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 $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<S = string | Selector> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element[], 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 = <Args extends any[], R>(pageFunction: PageFunctionOn<any, 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 = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args>) => Promise<js.JSHandle>;
|
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 Rect = { x: number, y: number, width: number, height: number };
|
||||||
export type Point = { x: number, y: number };
|
export type Point = { x: number, y: number };
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate {
|
||||||
await releaseObject(this._session, toRemoteObject(handle));
|
await releaseObject(this._session, toRemoteObject(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleJSONValue(handle: js.JSHandle): Promise<any> {
|
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
|
||||||
const remoteObject = toRemoteObject(handle);
|
const remoteObject = toRemoteObject(handle);
|
||||||
if (remoteObject.objectId) {
|
if (remoteObject.objectId) {
|
||||||
const response = await this._session.send('Runtime.callFunctionOn', {
|
const response = await this._session.send('Runtime.callFunctionOn', {
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
await this._client.send('DOM.setInputFiles', { objectId, files });
|
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');
|
assert(false, 'Multiple isolated worlds are not implemented');
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue