diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index 89d69a952d..12a4443a48 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -149,7 +149,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { await releaseObject(this._client, toRemoteObject(handle)); } - async handleJSONValue(handle: js.JSHandle): Promise { + async handleJSONValue(handle: js.JSHandle): Promise { const remoteObject = toRemoteObject(handle); if (remoteObject.objectId) { const response = await this._client.send('Runtime.callFunctionOn', { diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index 8163f5c3f4..8551557d97 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -99,11 +99,11 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate { await handle.evaluate(input.setFileInputFunction, files); } - async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise { + async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise> { 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>; } async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.DOMWorld): Promise { diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index a9a68ad8d6..c6ab4d6a6b 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -223,7 +223,7 @@ export class Page extends EventEmitter { this._timeoutSettings.setDefaultTimeout(timeout); } - async $(selector: string | types.Selector): Promise { + async $(selector: string | types.Selector): Promise | null> { return this.mainFrame().$(selector); } @@ -240,11 +240,11 @@ export class Page extends EventEmitter { return this.mainFrame().$$eval(selector, pageFunction, ...args as any); } - async $$(selector: string | types.Selector): Promise { + async $$(selector: string | types.Selector): Promise[]> { return this.mainFrame().$$(selector); } - async $x(expression: string): Promise { + async $x(expression: string): Promise[]> { return this.mainFrame().$x(expression); } diff --git a/src/chromium/Playwright.ts b/src/chromium/Playwright.ts index 076af17086..b9e22f27c5 100644 --- a/src/chromium/Playwright.ts +++ b/src/chromium/Playwright.ts @@ -35,7 +35,7 @@ export class Playwright { this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'Chromium'); } - launch(options: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise { + launch(options?: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise { return this._launcher.launch(options); } diff --git a/src/dom.ts b/src/dom.ts index 458ecb53c0..17bb39404c 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -24,10 +24,10 @@ export interface DOMWorldDelegate { boundingBox(handle: ElementHandle): Promise; screenshot(handle: ElementHandle, options?: types.ScreenshotOptions): Promise; setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise; - adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise; + adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise>; } -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 { @@ -60,7 +60,7 @@ export class DOMWorld { return this._injectedPromise; } - async adoptElementHandle(handle: ElementHandle): Promise { + async adoptElementHandle(handle: ElementHandle): Promise> { assert(handle.executionContext() !== this.context, 'Should not adopt to the same context'); return this.delegate.adoptElementHandle(handle, this); } @@ -75,10 +75,10 @@ export class DOMWorld { return { scope: selector.scope, selector: normalizeSelector(selector.selector), visible: selector.visible }; } - async $(selector: string | ScopedSelector): Promise { + async $(selector: string | ScopedSelector): Promise | null> { const resolved = await this.resolveSelector(selector); 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); if (visible === undefined || !element) return element; @@ -93,10 +93,10 @@ export class DOMWorld { return handle.asElement(); } - async $$(selector: string | ScopedSelector): Promise { + async $$(selector: string | ScopedSelector): Promise[]> { const resolved = await this.resolveSelector(selector); 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); if (visible !== undefined) return elements.filter(element => injected.isVisible(element) === visible); @@ -131,7 +131,7 @@ export class DOMWorld { $$eval: types.$$Eval = 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) => { + (injected: Injected, selector: string, scope?: Node, visible?: boolean) => { const elements = injected.querySelectorAll(selector, scope || document); if (visible !== undefined) return elements.filter(element => injected.isVisible(element) === visible); @@ -145,7 +145,7 @@ export class DOMWorld { } } -export class ElementHandle extends js.JSHandle { +export class ElementHandle extends js.JSHandle { private readonly _world: DOMWorld; constructor(context: js.ExecutionContext, remoteObject: any) { @@ -154,7 +154,7 @@ export class ElementHandle extends js.JSHandle { this._world = context._domWorld; } - asElement(): ElementHandle | null { + asElement(): ElementHandle | null { return this; } @@ -163,13 +163,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; } @@ -183,8 +185,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) @@ -336,13 +340,25 @@ 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((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!'); await this._world.delegate.setInputFiles(this, await input.loadFiles(files)); } 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) { @@ -374,7 +390,7 @@ export class ElementHandle extends js.JSHandle { return this._world.$(this._scopedSelector(selector)); } - $$(selector: string | types.Selector): Promise { + $$(selector: string | types.Selector): Promise[]> { 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); } - $x(expression: string): Promise { + $x(expression: string): Promise[]> { return this._world.$$({ scope: this, selector: 'xpath=' + expression }); } isIntersectingViewport(): Promise { - 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); @@ -441,7 +460,7 @@ export function waitForFunctionTask(pageFunction: Function | string, options: ty export function waitForSelectorTask(selector: string | types.Selector, timeout: number): Task { return async (domWorld: DOMWorld) => { 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); diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index 19ed5058db..cdb61b750e 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -134,7 +134,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { }); } - async handleJSONValue(handle: js.JSHandle): Promise { + async handleJSONValue(handle: js.JSHandle): Promise { const payload = handle._remoteObject; if (!payload.objectId) return deserializeValue(payload); diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index c3e6f10fcc..139667a9c8 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -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; @@ -101,13 +102,13 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate { await handle.evaluate(input.setFileInputFunction, files); } - async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise { + async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise> { 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; } diff --git a/src/frames.ts b/src/frames.ts index fefe473a21..36ed432d70 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -122,12 +122,12 @@ export class Frame { return context.evaluate(pageFunction, ...args as any); } - async $(selector: string | types.Selector): Promise { + async $(selector: string | types.Selector): Promise | null> { const domWorld = await this._mainDOMWorld(); return domWorld.$(types.clearSelector(selector)); } - async $x(expression: string): Promise { + async $x(expression: string): Promise[]> { 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 { + async $$(selector: string | types.Selector): Promise[]> { const domWorld = await this._mainDOMWorld(); return domWorld.$$(types.clearSelector(selector)); } diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 7f46facc3b..046ffa3169 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -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) @@ -29,9 +31,11 @@ class Injected { return element as Element; } - querySelectorAll(selector: string, root: SelectorRoot): Element[] { + querySelectorAll(selector: string, root: Node): Element[] { const parsed = this._parseSelector(selector); - let set = new Set([ root ]); + if (!root["querySelectorAll"]) + throw new Error('Node is not queryable.'); + let set = new Set([ root as SelectorRoot ]); for (const { engine, selector } of parsed) { const newSet = new Set(); for (const prev of set) { diff --git a/src/input.ts b/src/input.ts index 5e4e2885e3..829886ded1 100644 --- a/src/input.ts +++ b/src/input.ts @@ -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