From a91b8aca3728b2c2e310e9446897d729bf983377 Mon Sep 17 00:00:00 2001 From: Dan Park <79119751+danparksf@users.noreply.github.com> Date: Fri, 4 Jun 2021 03:25:36 -0700 Subject: [PATCH] feat: add drag-and-drop support (#7150) This commit adds drag-and-drop support, leveraging new additions to the CDP Input domain (Input.setInterceptDrags, Input.dispatchDragEvent, and Input.dragIntercepted). --- docs/api.md | 125 ++++++++++++++++++++++++ src/common/Input.ts | 94 ++++++++++++++++++ src/common/JSHandle.ts | 75 +++++++++++++- src/common/Page.ts | 18 ++++ test/assets/input/drag-and-drop.html | 46 +++++++++ test/drag-and-drop.spec.ts | 125 ++++++++++++++++++++++++ utils/doclint/check_public_api/index.js | 112 +++++++++++++++++++++ 7 files changed, 591 insertions(+), 4 deletions(-) create mode 100644 test/assets/input/drag-and-drop.html create mode 100644 test/drag-and-drop.spec.ts diff --git a/docs/api.md b/docs/api.md index 3cd2ffacf792d..2e7e35965f58f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -155,6 +155,7 @@ * [page.goto(url[, options])](#pagegotourl-options) * [page.hover(selector)](#pagehoverselector) * [page.isClosed()](#pageisclosed) + * [page.isDragInterceptionEnabled](#pageisdraginterceptionenabled) * [page.isJavaScriptEnabled()](#pageisjavascriptenabled) * [page.keyboard](#pagekeyboard) * [page.mainFrame()](#pagemainframe) @@ -171,6 +172,7 @@ * [page.setCookie(...cookies)](#pagesetcookiecookies) * [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) * [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) + * [page.setDragInterception(enabled)](#pagesetdraginterceptionenabled) * [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) * [page.setGeolocation(options)](#pagesetgeolocationoptions) * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) @@ -214,6 +216,11 @@ - [class: Mouse](#class-mouse) * [mouse.click(x, y[, options])](#mouseclickx-y-options) * [mouse.down([options])](#mousedownoptions) + * [mouse.drag(start, target)](#mousedragstart-target) + * [mouse.dragAndDrop(start, target[, options])](#mousedraganddropstart-target-options) + * [mouse.dragEnter(target, data)](#mousedragentertarget-data) + * [mouse.dragOver(target, data)](#mousedragovertarget-data) + * [mouse.drop(target, data)](#mousedroptarget-data) * [mouse.move(x, y[, options])](#mousemovex-y-options) * [mouse.up([options])](#mouseupoptions) * [mouse.wheel([options])](#mousewheeloptions) @@ -294,8 +301,14 @@ * [elementHandle.boundingBox()](#elementhandleboundingbox) * [elementHandle.boxModel()](#elementhandleboxmodel) * [elementHandle.click([options])](#elementhandleclickoptions) + * [elementHandle.clickablePoint()](#elementhandleclickablepoint) * [elementHandle.contentFrame()](#elementhandlecontentframe) * [elementHandle.dispose()](#elementhandledispose) + * [elementHandle.drag(target)](#elementhandledragtarget) + * [elementHandle.dragAndDrop(target[, options])](#elementhandledraganddroptarget-options) + * [elementHandle.dragEnter([data])](#elementhandledragenterdata) + * [elementHandle.dragOver([data])](#elementhandledragoverdata) + * [elementHandle.drop([data])](#elementhandledropdata) * [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args) * [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args) * [elementHandle.executionContext()](#elementhandleexecutioncontext) @@ -1919,6 +1932,12 @@ Shortcut for [page.mainFrame().hover(selector)](#framehoverselector). Indicates that the page has been closed. +#### page.isDragInterceptionEnabled + +- returns: <[boolean]> + +Indicates that drag events are being intercepted. + #### page.isJavaScriptEnabled() - returns: <[boolean]> @@ -2183,6 +2202,13 @@ This setting will change the default maximum time for the following methods and > **NOTE** [`page.setDefaultNavigationTimeout`](#pagesetdefaultnavigationtimeouttimeout) takes priority over [`page.setDefaultTimeout`](#pagesetdefaulttimeouttimeout) +#### page.setDragInterception(enabled) + +- `enabled` <[boolean]> +- returns: <[Promise]> + +Enables the Input.drag methods. This provides the capability to cpature drag events emitted on the page, which can then be used to simulate drag-and-drop. + #### page.setExtraHTTPHeaders(headers) - `headers` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings. @@ -2958,6 +2984,62 @@ Shortcut for [`mouse.move`](#mousemovex-y-options), [`mouse.down`](#mousedownopt Dispatches a `mousedown` event. +#### mouse.drag(start, target) + +- `start` <[Object]> the position to start dragging from + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- `target` <[Object]> the position to drag to + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- returns: <[Promise<[DragData]>]> + +This method creates and captures a dragevent from a given point. + +#### mouse.dragAndDrop(start, target[, options]) + +- `start` <[Object]> + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- `target` <[Object]> + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- `options` <[Object]> + - `delay` <[number]> how long to delay before dropping onto the target point +- returns: <[Promise<[DragData]>]> + +This method drags from a given start point and drops onto a target point. + +#### mouse.dragEnter(target, data) + +- `target` <[Object]> + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- `data` <[Object]> +- returns: <[Promise]]> + +This method triggers a dragenter event from the target point. + +#### mouse.dragOver(target, data) + +- `target` <[Object]> + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- `data` <[Object]> +- returns: <[Promise]]> + +This method triggers a dragover event from the target point. + +#### mouse.drop(target, data) + +- `target` <[Object]> + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- `data` <[Object]> +- returns: <[Promise]]> + +This method triggers a drop event from the target point. + #### mouse.move(x, y[, options]) - `x` <[number]> @@ -4033,6 +4115,10 @@ This method returns boxes of the element, or `null` if the element is not visibl This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element. If the element is detached from DOM, the method throws an error. +#### elementHandle.clickablePoint() + +- returns: <[Promise<[Point]>]> Resolves to the x, y point that describes the element's position. + #### elementHandle.contentFrame() - returns: <[Promise]> Resolves to the content frame for element handles referencing iframe nodes, or null otherwise @@ -4043,6 +4129,45 @@ If the element is detached from DOM, the method throws an error. The `elementHandle.dispose` method stops referencing the element handle. +#### elementHandle.drag(target) + +- `target` <[Object]> + - `x` <[number]> x coordinate + - `y` <[number]> y coordinate +- returns: <[Promise<[DragData]>]> + +This method creates and captures a drag event from the element. + +#### elementHandle.dragAndDrop(target[, options]) + +- `target` <[ElementHandle]> +- `options` <[Object]> + - `delay` <[number]> how long to delay before dropping onto the target element +- returns: <[Promise]> + +This method will drag a given element and drop it onto a target element. + +#### elementHandle.dragEnter([data]) + +- `data` <[Object]> drag data created from `element.drag` +- returns: <[Promise]> + +This method will trigger a dragenter event from the given element. + +#### elementHandle.dragOver([data]) + +- `data` <[Object]> drag data created from `element.drag` +- returns: <[Promise]> + +This method will trigger a dragover event from the given element. + +#### elementHandle.drop([data]) + +- `data` <[Object]> drag data created from `element.drag` +- returns: <[Promise]> + +This method will trigger a drop event from the given element. + #### elementHandle.evaluate(pageFunction[, ...args]) - `pageFunction` <[function]\([Object]\)> Function to be evaluated in browser context diff --git a/src/common/Input.ts b/src/common/Input.ts index c568856a7d647..293c9ab461826 100644 --- a/src/common/Input.ts +++ b/src/common/Input.ts @@ -17,6 +17,8 @@ import { assert } from './assert.js'; import { CDPSession } from './Connection.js'; import { keyDefinitions, KeyDefinition, KeyInput } from './USKeyboardLayout.js'; +import { Protocol } from 'devtools-protocol'; +import { Point } from './JSHandle.js'; type KeyDescription = Required< Pick @@ -485,6 +487,98 @@ export class Mouse { pointerType: 'mouse', }); } + + /** + * Dispatches a `drag` event. + * @param start - starting point for drag + * @param target - point to drag to + * ``` + */ + async drag(start: Point, target: Point): Promise { + const promise = new Promise((resolve) => { + this._client.once('Input.dragIntercepted', (event) => + resolve(event.data) + ); + }); + await this.move(start.x, start.y); + await this.down(); + await this.move(target.x, target.y); + return promise; + } + + /** + * Dispatches a `dragenter` event. + * @param target - point for emitting `dragenter` event + * ``` + */ + async dragEnter(target: Point, data: Protocol.Input.DragData): Promise { + await this._client.send('Input.dispatchDragEvent', { + type: 'dragEnter', + x: target.x, + y: target.y, + modifiers: this._keyboard._modifiers, + data, + }); + } + + /** + * Dispatches a `dragover` event. + * @param target - point for emitting `dragover` event + * ``` + */ + async dragOver(target: Point, data: Protocol.Input.DragData): Promise { + await this._client.send('Input.dispatchDragEvent', { + type: 'dragOver', + x: target.x, + y: target.y, + modifiers: this._keyboard._modifiers, + data, + }); + } + + /** + * Performs a dragenter, dragover, and drop in sequence. + * @param target - point to drop on + * @param data - drag data containing items and operations mask + * @param options - An object of options. Accepts delay which, + * if specified, is the time to wait between `dragover` and `drop` in milliseconds. + * Defaults to 0. + * ``` + */ + async drop(target: Point, data: Protocol.Input.DragData): Promise { + await this._client.send('Input.dispatchDragEvent', { + type: 'drop', + x: target.x, + y: target.y, + modifiers: this._keyboard._modifiers, + data, + }); + } + + /** + * Performs a drag, dragenter, dragover, and drop in sequence. + * @param target - point to drag from + * @param target - point to drop on + * @param options - An object of options. Accepts delay which, + * if specified, is the time to wait between `dragover` and `drop` in milliseconds. + * Defaults to 0. + * ``` + */ + async dragAndDrop( + start: Point, + target: Point, + options: { delay?: number } = {} + ): Promise { + const { delay = null } = options; + const data = await this.drag(start, target); + await this.dragEnter(target, data); + await this.dragOver(target, data); + if (delay) { + await new Promise((resolve) => setTimeout(resolve, delay)); + } + await this.drop(target, data); + await this.up(); + } } /** diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index db83587be5aa9..8ec0ef52e7461 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -411,7 +411,7 @@ export class ElementHandle< if (error) throw new Error(error); } - private async _clickablePoint(): Promise<{ x: number; y: number }> { + async clickablePoint(): Promise { const [result, layoutMetrics] = await Promise.all([ this._client .send('DOM.getContentQuads', { @@ -482,7 +482,7 @@ export class ElementHandle< */ async hover(): Promise { await this._scrollIntoViewIfNeeded(); - const { x, y } = await this._clickablePoint(); + const { x, y } = await this.clickablePoint(); await this._page.mouse.move(x, y); } @@ -493,10 +493,69 @@ export class ElementHandle< */ async click(options: ClickOptions = {}): Promise { await this._scrollIntoViewIfNeeded(); - const { x, y } = await this._clickablePoint(); + const { x, y } = await this.clickablePoint(); await this._page.mouse.click(x, y, options); } + /** + * This method creates and captures a dragevent from the element. + */ + async drag(target: Point): Promise { + assert( + this._page.isDragInterceptionEnabled, + 'Drag Interception is not enabled!' + ); + await this._scrollIntoViewIfNeeded(); + const start = await this.clickablePoint(); + return await this._page.mouse.drag(start, target); + } + + /** + * This method creates a `dragenter` event on the element. + */ + async dragEnter( + data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 } + ): Promise { + await this._scrollIntoViewIfNeeded(); + const target = await this.clickablePoint(); + await this._page.mouse.dragEnter(target, data); + } + + /** + * This method creates a `dragover` event on the element. + */ + async dragOver( + data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 } + ): Promise { + await this._scrollIntoViewIfNeeded(); + const target = await this.clickablePoint(); + await this._page.mouse.dragOver(target, data); + } + + /** + * This method triggers a drop on the element. + */ + async drop( + data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 } + ): Promise { + await this._scrollIntoViewIfNeeded(); + const destination = await this.clickablePoint(); + await this._page.mouse.drop(destination, data); + } + + /** + * This method triggers a dragenter, dragover, and drop on the element. + */ + async dragAndDrop( + target: ElementHandle, + options?: { delay: number } + ): Promise { + await this._scrollIntoViewIfNeeded(); + const startPoint = await this.clickablePoint(); + const targetPoint = await target.clickablePoint(); + await this._page.mouse.dragAndDrop(startPoint, targetPoint, options); + } + /** * Triggers a `change` and `input` event once all the provided options have been * selected. If there's no `