From 64fd61e2af9c99cb24baf79a63ff8cf700839604 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Mon, 17 May 2021 00:45:50 +0200 Subject: [PATCH 01/12] build: allow to customize tmpdir --- src/node/Launcher.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/Launcher.ts b/src/node/Launcher.ts index 669d04d7ad089..b6e70a4afc0dc 100644 --- a/src/node/Launcher.ts +++ b/src/node/Launcher.ts @@ -73,12 +73,13 @@ class ChromeLauncher implements ProductLauncher { handleSIGHUP = true, ignoreHTTPSErrors = false, defaultViewport = { width: 800, height: 600 }, + tmpdir = os.tmpdir, slowMo = 0, timeout = 30000, waitForInitialPage = true, } = options; - const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-'); + const profilePath = path.join(tmpdir(), 'puppeteer_dev_chrome_profile-'); const chromeArguments = []; if (!ignoreDefaultArgs) chromeArguments.push(...this.defaultArgs(options)); else if (Array.isArray(ignoreDefaultArgs)) From cdc84236bd1d6a27ebfda24ea689232c8bcd6ff0 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Mon, 17 May 2021 00:50:57 +0200 Subject: [PATCH 02/12] Update Launcher.ts --- src/node/Launcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/Launcher.ts b/src/node/Launcher.ts index b6e70a4afc0dc..a6d6b935942e0 100644 --- a/src/node/Launcher.ts +++ b/src/node/Launcher.ts @@ -370,7 +370,7 @@ class FirefoxLauncher implements ProductLauncher { async _createProfile(extraPrefs: { [x: string]: unknown }): Promise { const profilePath = await mkdtempAsync( - path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-') + path.join(tmpdir(), 'puppeteer_dev_firefox_profile-') ); const prefsJS = []; const userJS = []; From d85bd7914d8f839b7f91d395616ac54a51e72090 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Mon, 17 May 2021 09:43:01 +0200 Subject: [PATCH 03/12] Update Launcher.ts --- src/node/Launcher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/Launcher.ts b/src/node/Launcher.ts index a6d6b935942e0..0382973f91ede 100644 --- a/src/node/Launcher.ts +++ b/src/node/Launcher.ts @@ -73,13 +73,13 @@ class ChromeLauncher implements ProductLauncher { handleSIGHUP = true, ignoreHTTPSErrors = false, defaultViewport = { width: 800, height: 600 }, - tmpdir = os.tmpdir, + tmpDir = os.tmpdir, slowMo = 0, timeout = 30000, waitForInitialPage = true, } = options; - const profilePath = path.join(tmpdir(), 'puppeteer_dev_chrome_profile-'); + const profilePath = path.join(tmpDir(), 'puppeteer_dev_chrome_profile-'); const chromeArguments = []; if (!ignoreDefaultArgs) chromeArguments.push(...this.defaultArgs(options)); else if (Array.isArray(ignoreDefaultArgs)) @@ -370,7 +370,7 @@ class FirefoxLauncher implements ProductLauncher { async _createProfile(extraPrefs: { [x: string]: unknown }): Promise { const profilePath = await mkdtempAsync( - path.join(tmpdir(), 'puppeteer_dev_firefox_profile-') + path.join(tmpDir(), 'puppeteer_dev_firefox_profile-') ); const prefsJS = []; const userJS = []; From 083ea5ff7e8f6a51b16db9699e237864961c9eaa Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 18 May 2021 16:35:02 +0200 Subject: [PATCH 04/12] refactor: add .tmpDir helper --- experimental/puppeteer-firefox/lib/Launcher.js | 4 +++- src/node/Launcher.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/experimental/puppeteer-firefox/lib/Launcher.js b/experimental/puppeteer-firefox/lib/Launcher.js index 77a8a7186af8c..d47d494f25adb 100644 --- a/experimental/puppeteer-firefox/lib/Launcher.js +++ b/experimental/puppeteer-firefox/lib/Launcher.js @@ -30,7 +30,9 @@ const WebSocketTransport = require('./WebSocketTransport'); const mkdtempAsync = util.promisify(fs.mkdtemp); const removeFolderAsync = util.promisify(removeFolder); -const FIREFOX_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_firefox_profile-'); +const tmpDir = () => process.env.PUPPETEER_TMP_DIR || os.tmpdir() + +const FIREFOX_PROFILE_PATH = path.join(tmpDir(), 'puppeteer_firefox_profile-'); const DEFAULT_ARGS = [ '-no-remote', diff --git a/src/node/Launcher.ts b/src/node/Launcher.ts index 0382973f91ede..572bc91f73102 100644 --- a/src/node/Launcher.ts +++ b/src/node/Launcher.ts @@ -29,8 +29,11 @@ import { BrowserLaunchArgumentOptions, PuppeteerNodeLaunchOptions, } from './LaunchOptions.js'; + import { Product } from '../common/Product.js'; +const tmpDir = () => process.env.PUPPETEER_TMP_DIR || os.tmpdir() + /** * Describes a launcher - a class that is able to create and launch a browser instance. * @public @@ -73,7 +76,6 @@ class ChromeLauncher implements ProductLauncher { handleSIGHUP = true, ignoreHTTPSErrors = false, defaultViewport = { width: 800, height: 600 }, - tmpDir = os.tmpdir, slowMo = 0, timeout = 30000, waitForInitialPage = true, From fca4ca23f71a91d7c25d81c6161eeff2255919a3 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 18 May 2021 16:39:06 +0200 Subject: [PATCH 05/12] docs: add PUPPETEER_TMP_DIR --- docs/api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.md b/docs/api.md index bcb114e888add..9b73243a54fa3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -440,6 +440,7 @@ If Puppeteer doesn't find them in the environment during the installation step, - `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` - defines HTTP proxy settings that are used to download and run Chromium. - `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step. +- `PUPPETEER_TMP_DIR` - defines the directory to be used by Puppeteer for creating temporary files. It's [os.tmpdir](https://nodejs.org/api/os.html#os_os_tmpdir) by default. - `PUPPETEER_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`. - `PUPPETEER_DOWNLOAD_PATH` - overwrite the path for the downloads folder. Defaults to `/.local-chromium`, where `` is Puppeteer's package root. - `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Puppeteer to use. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. From 48954c6adb12a9f1bcca5e3dd67c195e21563abc 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 06/12] feature: 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 `