diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index 5dc847ca8a..446df6cdd2 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -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; -export type JSHandle = js.JSHandle; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { _client: CDPSession; _contextId: number; @@ -140,7 +141,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate> { diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 68fcb937f8..64f06f53fb 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -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, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +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 { + async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise { const nodeInfo = await this._client.send('DOM.describeNode', { objectId: toRemoteObject(elementHandle).objectId, }); diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index 91e4bddff5..c5a8943e39 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -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 { + async contentFrame(handle: dom.ElementHandle): Promise { 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 { + private _getBoxModel(handle: dom.ElementHandle): Promise { 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 { 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 { + async screenshot(handle: dom.ElementHandle, options: any = {}): Promise { let needsViewportReset = false; let boundingBox = await this.boundingBox(handle); @@ -129,7 +121,7 @@ class ElementHandleDelegate { return imageData; } - async ensurePointerActionPoint(handle: ElementHandle, relativePoint?: Point): Promise { + async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: dom.Point): Promise { await handle._scrollIntoViewIfNeeded(); if (!relativePoint) return this._clickablePoint(handle); @@ -150,8 +142,8 @@ class ElementHandleDelegate { return r.point; } - private async _clickablePoint(handle: ElementHandle): Promise { - const fromProtocolQuad = (quad: number[]): Point[] => { + private async _clickablePoint(handle: dom.ElementHandle): Promise { + 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 { - 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 { - 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, options?: input.PointerActionOptions): Promise { - 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 { - return this._performPointerAction(point => this._mouse.move(point.x, point.y), options); - } - - click(options?: input.ClickOptions): Promise { - return this._performPointerAction(point => this._mouse.click(point.x, point.y, options), options); - } - - dblclick(options?: input.MultiClickOptions): Promise { - return this._performPointerAction(point => this._mouse.dblclick(point.x, point.y, options), options); - } - - tripleclick(options?: input.MultiClickOptions): Promise { - return this._performPointerAction(point => this._mouse.tripleclick(point.x, point.y, options), options); - } - - async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise { - 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 { - 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 { - return this._delegate.screenshot(this, options); - } - - async $(selector: string): Promise { - 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 { - 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 = 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 = 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 { - 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 { - 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 { + await handle.evaluate(input.setFileInputFunction, files); } } diff --git a/src/chromium/NetworkManager.ts b/src/chromium/NetworkManager.ts index 09deddea42..83bbc25448 100644 --- a/src/chromium/NetworkManager.ts +++ b/src/chromium/NetworkManager.ts @@ -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; -export type Response = network.Response; +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): InterceptableRequest { +export function toInterceptableRequest(request: network.Request): InterceptableRequest { return (request as any)[interceptableRequestSymbol]; } diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index e498eb7cb5..5e50be7ea2 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -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 { + async $(selector: string): Promise { 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 { + async $$(selector: string): Promise { return this.mainFrame().$$(selector); } - async $x(expression: string): Promise { + async $x(expression: string): Promise { return this.mainFrame().$x(expression); } - async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise { + async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise { return this.mainFrame().addScriptTag(options); } - async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { + async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { 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 { + select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise { 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 { + waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise { return this.mainFrame().waitForSelector(selector, options); } - waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise { + waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise { return this.mainFrame().waitForXPath(xpath, options); } @@ -731,6 +732,6 @@ export class ConsoleMessage { } type FileChooser = { - element: ElementHandle, + element: dom.ElementHandle, multiple: boolean }; diff --git a/src/chromium/api.ts b/src/chromium/api.ts index bcc99cb689..c649754f0c 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -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'; diff --git a/src/chromium/features/accessibility.ts b/src/chromium/features/accessibility.ts index 34974773fc..7a787c5f15 100644 --- a/src/chromium/features/accessibility.ts +++ b/src/chromium/features/accessibility.ts @@ -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 { const { interestingOnly = true, diff --git a/src/dom.ts b/src/dom.ts new file mode 100644 index 0000000000..7b15b5392a --- /dev/null +++ b/src/dom.ts @@ -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; + boundingBox(handle: ElementHandle): Promise; + screenshot(handle: ElementHandle, options?: any): Promise; + ensurePointerActionPoint(handle: ElementHandle, relativePoint?: Point): Promise; + setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise; +// 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 { + 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, options?: input.PointerActionOptions): Promise { + 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 { + return this._performPointerAction(point => this._mouse.move(point.x, point.y), options); + } + + click(options?: input.ClickOptions): Promise { + return this._performPointerAction(point => this._mouse.click(point.x, point.y, options), options); + } + + dblclick(options?: input.MultiClickOptions): Promise { + return this._performPointerAction(point => this._mouse.dblclick(point.x, point.y, options), options); + } + + tripleclick(options?: input.MultiClickOptions): Promise { + return this._performPointerAction(point => this._mouse.tripleclick(point.x, point.y, options), options); + } + + async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise { + 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 { + 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 { + return this._delegate.boundingBox(this); + } + + async screenshot(options: any = {}): Promise { + return this._delegate.screenshot(this, options); + } + + async $(selector: string): Promise { + 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 { + 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 = 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 = 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 { + 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 { + 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; + }); + } +} diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index fb785d3b47..f4f1a6880d 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -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; -export type JSHandle = js.JSHandle; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { _session: JugglerSession; _executionContextId: string; diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index ab46a3f2a0..684cd56180 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -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, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +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 { + async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise { assert(false, 'Multiple isolated worlds are not implemented'); return elementHandle; } diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index e298f64e50..1dd88c7d9c 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -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 { - _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 { + async contentFrame(handle: dom.ElementHandle): Promise { 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 { 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 { 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 { })); } - isIntersectingViewport(): Promise { - 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 { + 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 { - 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 { + type Quad = {p1: dom.Point, p2: dom.Point, p3: dom.Point, p4: dom.Point}; - async $$(selector: string): Promise { - 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 = 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 = 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> { - 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 { - await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._clickablePoint(); - await this._page.mouse.dblclick(x, y, options); - } - - async tripleclick(options?: input.MultiClickOptions): Promise { - 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 { - 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 { - 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 { // Return the middle point of the first quad. return computeQuadCenter(quads[0]); } + + async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise { + 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 -}; diff --git a/src/firefox/NetworkManager.ts b/src/firefox/NetworkManager.ts index 412588c0b5..b57f476ed0 100644 --- a/src/firefox/NetworkManager.ts +++ b/src/firefox/NetworkManager.ts @@ -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; -export type Response = network.Response; +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): InterceptableRequest { +export function toInterceptableRequest(request: network.Request): InterceptableRequest { return (request as any)[interceptableRequestSymbol]; } diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index 5c127c2a0a..13d1d3ce28 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -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; private _networkManager: NetworkManager; _frameManager: FrameManager; + _javascriptEnabled = true; private _eventListeners: RegisteredListener[]; private _viewport: Viewport; private _disconnectPromise: Promise; @@ -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 { + addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise { return this.mainFrame().addScriptTag(options); } - addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise { + addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise { 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 { + waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise { return this._frameManager.mainFrame().waitForSelector(selector, options); } - waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise { + waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise { return this._frameManager.mainFrame().waitForXPath(xpath, options); } @@ -481,11 +484,11 @@ export class Page extends EventEmitter { return this._frameManager.mainFrame().title(); } - $(selector: string): Promise { + $(selector: string): Promise { return this._frameManager.mainFrame().$(selector); } - $$(selector: string): Promise> { + $$(selector: string): Promise> { 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> { + $x(expression: string): Promise> { 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 }; diff --git a/src/firefox/api.ts b/src/firefox/api.ts index 0a08f152d2..60ae22b6e8 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -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'; diff --git a/src/frames.ts b/src/frames.ts index 9dfb719c1b..01ed530f61 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -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> = { - contextPromise: Promise>; - contextResolveCallback: (c: js.ExecutionContext) => void; - context: js.ExecutionContext | null; - waitTasks: Set>; +type World = { + contextPromise: Promise; + contextResolveCallback: (c: js.ExecutionContext) => void; + context: js.ExecutionContext | null; + waitTasks: Set; }; export type NavigateOptions = { @@ -43,24 +44,24 @@ export type GotoOptions = NavigateOptions & { referer?: string, }; -export interface FrameDelegate> { +export interface FrameDelegate { timeoutSettings(): TimeoutSettings; - navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise | null>; - waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise | null>; - setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; - adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext): Promise; + navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise; + waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise; + setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; + adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext): Promise; } -export class Frame> { - _delegate: FrameDelegate; - private _parentFrame: Frame; +export class Frame { + _delegate: FrameDelegate; + private _parentFrame: Frame; private _url = ''; private _detached = false; - private _worlds = new Map>(); - private _childFrames = new Set>(); + private _worlds = new Map(); + private _childFrames = new Set(); private _name: string; - constructor(delegate: FrameDelegate, parentFrame: Frame | null) { + constructor(delegate: FrameDelegate, parentFrame: Frame | null) { this._delegate = delegate; this._parentFrame = parentFrame; @@ -73,65 +74,65 @@ export class Frame> { this._parentFrame._childFrames.add(this); } - goto(url: string, options?: GotoOptions): Promise | null> { + goto(url: string, options?: GotoOptions): Promise { return this._delegate.navigateFrame(this, url, options); } - waitForNavigation(options?: NavigateOptions): Promise | null> { + waitForNavigation(options?: NavigateOptions): Promise { return this._delegate.waitForFrameNavigation(this, options); } - _mainContext(): Promise> { + _mainContext(): Promise { 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> { + _utilityContext(): Promise { 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> { + executionContext(): Promise { return this._mainContext(); } - evaluateHandle: types.EvaluateHandle> = async (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { const context = await this._mainContext(); return context.evaluateHandle(pageFunction, ...args as any); } - evaluate: types.Evaluate> = async (pageFunction, ...args) => { + evaluate: types.Evaluate = async (pageFunction, ...args) => { const context = await this._mainContext(); return context.evaluate(pageFunction, ...args as any); } - async $(selector: string): Promise { + async $(selector: string): Promise { const context = await this._mainContext(); const document = await context._document(); return document.$(selector); } - async $x(expression: string): Promise { + async $x(expression: string): Promise { const context = await this._mainContext(); const document = await context._document(); return document.$x(expression); } - $eval: types.$Eval> = async (selector, pageFunction, ...args) => { + $eval: types.$Eval = 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> = async (selector, pageFunction, ...args) => { + $$eval: types.$$Eval = 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 { + async $$(selector: string): Promise { const context = await this._mainContext(); const document = await context._document(); return document.$$(selector); @@ -161,11 +162,11 @@ export class Frame> { return this._url; } - parentFrame(): Frame | null { + parentFrame(): Frame | null { return this._parentFrame; } - childFrames(): Frame[] { + childFrames(): Frame[] { return Array.from(this._childFrames); } @@ -177,7 +178,7 @@ export class Frame> { url?: string; path?: string; content?: string; type?: string; - }): Promise { + }): Promise { const { url = null, path = null, @@ -234,7 +235,7 @@ export class Frame> { } } - async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { + async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { const { url = null, path = null, @@ -344,15 +345,15 @@ export class Frame> { await handle.dispose(); } - async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { + async select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise { 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> { await handle.dispose(); } - waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise | null> { + waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise { const xPathPattern = '//'; if (helper.isString(selectorOrFunctionOrTimeout)) { @@ -388,7 +389,7 @@ export class Frame> { async waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; - timeout?: number; } | undefined): Promise { + timeout?: number; } | undefined): Promise { 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> { async waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; - timeout?: number; } | undefined): Promise { + timeout?: number; } | undefined): Promise { 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> { waitForFunction( pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } = {}, - ...args): Promise> { + ...args): Promise { const { polling = 'raf', timeout = this._delegate.timeoutSettings().timeout(), @@ -452,7 +453,7 @@ export class Frame> { this._parentFrame = null; } - private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise> { + private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise { const task = new WaitTask(params, () => world.waitTasks.delete(task)); world.waitTasks.add(task); if (world.context) @@ -460,7 +461,7 @@ export class Frame> { return task.promise; } - private _setContext(worldType: WorldType, context: js.ExecutionContext | 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> { } } - _contextCreated(worldType: WorldType, context: js.ExecutionContext) { + _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> { this._setContext(worldType, context); } - _contextDestroyed(context: js.ExecutionContext) { + _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, dispose: boolean): Promise { + private async _adoptElementHandle(elementHandle: dom.ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise { if (elementHandle.executionContext() === context) return elementHandle; const handle = this._delegate.adoptElementHandle(elementHandle, context); diff --git a/src/javascript.ts b/src/javascript.ts index e47cb3660a..bb2ea98349 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -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> { - evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise; - getProperties(handle: JSHandle): Promise>>; - releaseHandle(handle: JSHandle): Promise; - handleToString(handle: JSHandle): string; - handleJSONValue(handle: JSHandle): Promise; +export interface ExecutionContextDelegate { + evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise; + getProperties(handle: JSHandle): Promise>; + releaseHandle(handle: JSHandle): Promise; + handleToString(handle: JSHandle): string; + handleJSONValue(handle: JSHandle): Promise; } -export class ExecutionContext> { - _delegate: ExecutionContextDelegate; - private _frame: frames.Frame; - private _injectedPromise: Promise> | null = null; - private _documentPromise: Promise | null = null; +export class ExecutionContext { + _delegate: ExecutionContextDelegate; + private _frame: frames.Frame; + private _injectedPromise: Promise | null = null; + private _documentPromise: Promise | null = null; - constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) { + constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) { this._delegate = delegate; this._frame = frame; } - frame(): frames.Frame | null { + frame(): frames.Frame | null { return this._frame; } - evaluate: types.Evaluate> = (pageFunction, ...args) => { + evaluate: types.Evaluate = (pageFunction, ...args) => { return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args); } - evaluateHandle: types.EvaluateHandle> = (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandle = (pageFunction, ...args) => { return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args); } - _injected(): Promise> { + _injected(): Promise { if (!this._injectedPromise) { const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source]; const source = ` @@ -51,34 +52,34 @@ export class ExecutionContext { + _document(): Promise { if (!this._documentPromise) this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement()!); return this._documentPromise; } } -export class JSHandle> { - _context: ExecutionContext; +export class JSHandle { + _context: ExecutionContext; _disposed = false; - constructor(context: ExecutionContext) { + constructor(context: ExecutionContext) { this._context = context; } - executionContext(): ExecutionContext { + executionContext(): ExecutionContext { return this._context; } - evaluate: types.EvaluateOn> = (pageFunction, ...args) => { + evaluate: types.EvaluateOn = (pageFunction, ...args) => { return this._context.evaluate(pageFunction, this, ...args); } - evaluateHandle: types.EvaluateHandleOn> = (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandleOn = (pageFunction, ...args) => { return this._context.evaluateHandle(pageFunction, this, ...args); } - async getProperty(propertyName: string): Promise | null> { + async getProperty(propertyName: string): Promise { const objectHandle = await this.evaluateHandle((object, propertyName) => { const result = {__proto__: null}; result[propertyName] = object[propertyName]; @@ -90,7 +91,7 @@ export class JSHandle> return result; } - getProperties(): Promise>> { + getProperties(): Promise> { return this._context._delegate.getProperties(this); } @@ -98,7 +99,7 @@ export class JSHandle> return this._context._delegate.handleJSONValue(this); } - asElement(): ElementHandle | null { + asElement(): dom.ElementHandle | null { return null; } diff --git a/src/network.ts b/src/network.ts index 0d3de397ec..cb24994ecc 100644 --- a/src/network.ts +++ b/src/network.ts @@ -50,9 +50,9 @@ export function filterCookies(cookies: NetworkCookie[], urls: string[]) { export type Headers = { [key: string]: string }; -export class Request> { - _response: Response | null = null; - _redirectChain: Request[]; +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> { private _method: string; private _postData: string; private _headers: Headers; - private _frame: frames.Frame; + private _frame: frames.Frame; - constructor(frame: frames.Frame | null, redirectChain: Request[], 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> { return this._headers; } - response(): Response | null { + response(): Response | null { return this._response; } - frame(): frames.Frame | null { + frame(): frames.Frame | null { return this._frame; } @@ -110,7 +110,7 @@ export class Request> { return this._isNavigationRequest; } - redirectChain(): Request[] { + redirectChain(): Request[] { return this._redirectChain.slice(); } @@ -130,8 +130,8 @@ export type RemoteAddress = { type GetResponseBodyCallback = () => Promise; -export class Response> { - private _request: Request; +export class Response { + private _request: Request; private _contentPromise: Promise | null = null; private _bodyLoadedPromise: Promise; private _bodyLoadedPromiseFulfill: any; @@ -142,7 +142,7 @@ export class Response> private _headers: Headers; private _getResponseBodyCallback: GetResponseBodyCallback; - constructor(request: Request, 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> return JSON.parse(content); } - request(): Request { + request(): Request { return this._request; } - frame(): frames.Frame | null { + frame(): frames.Frame | null { return this._request.frame(); } } diff --git a/src/types.ts b/src/types.ts index 1d03323d08..e575e659d6 100644 --- a/src/types.ts +++ b/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 = { [Index in keyof Args]: Args[Index] | Handle }; type PageFunction = string | ((...args: Args) => R | Promise); type PageFunctionOn = string | ((on: On, ...args: Args) => R | Promise); @@ -14,19 +11,3 @@ export type $Eval = (selector: string, pageFuncti export type $$Eval = (selector: string, pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateHandleOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; - -export interface ElementHandle> extends js.JSHandle { - $(selector: string): Promise; - $x(expression: string): Promise; - $$(selector: string): Promise; - $eval: $Eval>; - $$eval: $$Eval>; - click(options?: input.ClickOptions): Promise; - dblclick(options?: input.MultiClickOptions): Promise; - tripleclick(options?: input.MultiClickOptions): Promise; - fill(value: string): Promise; - focus(): Promise; - hover(options?: input.PointerActionOptions): Promise; - select(...values: (string | EHandle | input.SelectOption)[]): Promise; - type(text: string, options: { delay: (number | undefined); } | undefined): Promise; -} diff --git a/src/waitTask.ts b/src/waitTask.ts index fda9fb57f4..f2494f7fb5 100644 --- a/src/waitTask.ts +++ b/src/waitTask.ts @@ -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> { - readonly promise: Promise>; +export class WaitTask { + readonly promise: Promise; private _cleanup: () => void; private _params: WaitTaskParams & { predicateBody: string }; private _runCount: number; - private _resolve: (result: js.JSHandle) => 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> }; this._cleanup = cleanup; this._runCount = 0; - this.promise = new Promise>((resolve, reject) => { + this.promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); @@ -57,9 +56,9 @@ export class WaitTask> this._doCleanup(); } - async rerun(context: js.ExecutionContext) { + async rerun(context: js.ExecutionContext) { const runCount = ++this._runCount; - let success: js.JSHandle | 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); diff --git a/src/webkit/ExecutionContext.ts b/src/webkit/ExecutionContext.ts index 09fa3f5015..6abb12a317 100644 --- a/src/webkit/ExecutionContext.ts +++ b/src/webkit/ExecutionContext.ts @@ -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; -export type JSHandle = js.JSHandle; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { private _globalObjectId?: string; _session: TargetSession; _contextId: number; diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 9bdb425984..1132ac6fa4 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -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; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +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 { + async adoptElementHandle(elementHandle: dom.ElementHandle, context: ExecutionContext): Promise { assert(false, 'Multiple isolated worlds are not implemented'); return elementHandle; } diff --git a/src/webkit/JSHandle.ts b/src/webkit/JSHandle.ts index de26bd3072..05da5e0b4b 100644 --- a/src/webkit/JSHandle.ts +++ b/src/webkit/JSHandle.ts @@ -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 { +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 { + 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 { + throw new Error('boundingBox() is not implemented'); + } + + async screenshot(handle: dom.ElementHandle, options: any = {}): Promise { + 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 { + 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 { + 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 { }; } - _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 { - await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._clickablePoint(); - await this._page.mouse.move(x, y); - } - - async click(options?: input.ClickOptions): Promise { - await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._clickablePoint(); - await this._page.mouse.click(x, y, options); - } - - async dblclick(options?: input.MultiClickOptions): Promise { - await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._clickablePoint(); - await this._page.mouse.dblclick(x, y, options); - } - - async tripleclick(options?: input.MultiClickOptions): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 = 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 = 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 { - 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 { - 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 { + 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); -} diff --git a/src/webkit/NetworkManager.ts b/src/webkit/NetworkManager.ts index eed204e280..dc2689131a 100644 --- a/src/webkit/NetworkManager.ts +++ b/src/webkit/NetworkManager.ts @@ -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; -export type Response = network.Response; +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): InterceptableRequest { +export function toInterceptableRequest(request: network.Request): InterceptableRequest { return (request as any)[interceptableRequestSymbol]; } diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index bdd2fd6296..1258ed87f2 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -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 { + async $(selector: string): Promise { 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 { + async $$(selector: string): Promise { return this.mainFrame().$$(selector); } - async $x(expression: string): Promise { + async $x(expression: string): Promise { return this.mainFrame().$x(expression); } - async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise { + async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise { return this.mainFrame().addScriptTag(options); } - async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { + async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { 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 { + waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise { return this.mainFrame().waitForSelector(selector, options); } - waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise { + waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise { return this.mainFrame().waitForXPath(xpath, options); } @@ -588,6 +589,6 @@ export class ConsoleMessage { } type FileChooser = { - element: ElementHandle, + element: dom.ElementHandle, multiple: boolean }; diff --git a/src/webkit/api.ts b/src/webkit/api.ts index a0424f3a15..99603f3457 100644 --- a/src/webkit/api.ts +++ b/src/webkit/api.ts @@ -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';