feat: implement improved Drag n' Drop APIs (#10651)

This commit is contained in:
jrandolf 2023-09-14 11:14:30 +02:00 committed by GitHub
parent 67f72de274
commit 9342bac263
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 284 additions and 214 deletions

View File

@ -4,7 +4,7 @@ sidebar_label: ElementHandle.drag
# ElementHandle.drag() method
This method creates and captures a dragevent from the element.
Drags an element over the given element or point.
#### Signature:
@ -12,18 +12,20 @@ This method creates and captures a dragevent from the element.
class ElementHandle {
drag(
this: ElementHandle<Element>,
target: Point
): Promise<Protocol.Input.DragData>;
target: Point | ElementHandle<Element>
): Promise<Protocol.Input.DragData | void>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ----------- |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
| target | [Point](./puppeteer.point.md) | |
| Parameter | Type | Description |
| --------- | --------------------------------------------------------------------------------------------- | ----------- |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
| target | [Point](./puppeteer.point.md) \| [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
**Returns:**
Promise&lt;Protocol.Input.DragData&gt;
Promise&lt;Protocol.Input.DragData \| void&gt;
DEPRECATED. When drag interception is enabled, the drag payload is returned.

View File

@ -4,7 +4,9 @@ sidebar_label: ElementHandle.dragAndDrop
# ElementHandle.dragAndDrop() method
This method triggers a dragenter, dragover, and drop on the element.
> Warning: This API is now obsolete.
>
> Use `ElementHandle.drop` instead.
#### Signature:

View File

@ -4,7 +4,9 @@ sidebar_label: ElementHandle.dragEnter
# ElementHandle.dragEnter() method
This method creates a `dragenter` event on the element.
> Warning: This API is now obsolete.
>
> Do not use. `dragenter` will automatically be performed during dragging.
#### Signature:

View File

@ -4,7 +4,9 @@ sidebar_label: ElementHandle.dragOver
# ElementHandle.dragOver() method
This method creates a `dragover` event on the element.
> Warning: This API is now obsolete.
>
> Do not use. `dragover` will automatically be performed during dragging.
#### Signature:

View File

@ -4,7 +4,7 @@ sidebar_label: ElementHandle.drop
# ElementHandle.drop() method
This method triggers a drop on the element.
Drops the given element onto the current one.
#### Signature:
@ -12,17 +12,17 @@ This method triggers a drop on the element.
class ElementHandle {
drop(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
element: ElementHandle<Element>
): Promise<void>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ------------ |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
| data | Protocol.Input.DragData | _(Optional)_ |
| Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ----------- |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
| element | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
**Returns:**

View File

@ -0,0 +1,31 @@
---
sidebar_label: ElementHandle.drop_1
---
# ElementHandle.drop() method
> Warning: This API is now obsolete.
>
> No longer supported.
#### Signature:
```typescript
class ElementHandle {
drop(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ------------ |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
| data | Protocol.Input.DragData | _(Optional)_ |
**Returns:**
Promise&lt;void&gt;

View File

@ -61,11 +61,12 @@ The constructor for this class is marked as internal. Third-party code should no
| [clickablePoint(offset)](./puppeteer.elementhandle.clickablepoint.md) | | Returns the middle point within an element unless a specific offset is provided. |
| [contentFrame(this)](./puppeteer.elementhandle.contentframe.md) | | Resolves the frame associated with the element, if any. Always exists for HTMLIFrameElements. |
| [contentFrame()](./puppeteer.elementhandle.contentframe_1.md) | | |
| [drag(this, target)](./puppeteer.elementhandle.drag.md) | | This method creates and captures a dragevent from the element. |
| [dragAndDrop(this, target, options)](./puppeteer.elementhandle.draganddrop.md) | | This method triggers a dragenter, dragover, and drop on the element. |
| [dragEnter(this, data)](./puppeteer.elementhandle.dragenter.md) | | This method creates a <code>dragenter</code> event on the element. |
| [dragOver(this, data)](./puppeteer.elementhandle.dragover.md) | | This method creates a <code>dragover</code> event on the element. |
| [drop(this, data)](./puppeteer.elementhandle.drop.md) | | This method triggers a drop on the element. |
| [drag(this, target)](./puppeteer.elementhandle.drag.md) | | Drags an element over the given element or point. |
| [dragAndDrop(this, target, options)](./puppeteer.elementhandle.draganddrop.md) | | |
| [dragEnter(this, data)](./puppeteer.elementhandle.dragenter.md) | | |
| [dragOver(this, data)](./puppeteer.elementhandle.dragover.md) | | |
| [drop(this, element)](./puppeteer.elementhandle.drop.md) | | Drops the given element onto the current one. |
| [drop(this, data)](./puppeteer.elementhandle.drop_1.md) | | |
| [focus()](./puppeteer.elementhandle.focus.md) | | Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element. |
| [hover(this)](./puppeteer.elementhandle.hover.md) | | This method scrolls element into view if needed, and then uses [Page](./puppeteer.page.md) to hover over the center of the element. If the element is detached from DOM, the method throws an error. |
| [isHidden()](./puppeteer.elementhandle.ishidden.md) | | Checks if an element is hidden using the same mechanism as [ElementHandle.waitForSelector()](./puppeteer.elementhandle.waitforselector.md). |

View File

@ -4,6 +4,10 @@ sidebar_label: Page.isDragInterceptionEnabled
# Page.isDragInterceptionEnabled() method
> Warning: This API is now obsolete.
>
> We no longer support intercepting drag payloads. Use the new drag APIs found on [ElementHandle](./puppeteer.elementhandle.md) to drag (or just use the [Page.mouse](./puppeteer.page.mouse.md)).
`true` if drag events are being intercepted, `false` otherwise.
#### Signature:

View File

@ -4,6 +4,10 @@ sidebar_label: Page.setDragInterception
# Page.setDragInterception() method
> Warning: This API is now obsolete.
>
> We no longer support intercepting drag payloads. Use the new drag APIs found on [ElementHandle](./puppeteer.elementhandle.md) to drag (or just use the [Page.mouse](./puppeteer.page.mouse.md)).
#### Signature:
```typescript
@ -21,7 +25,3 @@ class Page {
**Returns:**
Promise&lt;void&gt;
## Remarks
Activating drag interception enables the `Input.drag`, methods This provides the capability to capture drag events emitted on the page, which can then be used to simulate drag-and-drop.

View File

@ -31,6 +31,7 @@ import {KeyInput} from '../common/USKeyboardLayout.js';
import {isString, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {throwIfDisposed} from '../util/decorators.js';
import {
KeyboardTypeOptions,
@ -731,59 +732,134 @@ export abstract class ElementHandle<
}
/**
* This method creates and captures a dragevent from the element.
* Drags an element over the given element or point.
*
* @returns DEPRECATED. When drag interception is enabled, the drag payload is
* returned.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async drag(
this: ElementHandle<Element>,
target: Point
): Promise<Protocol.Input.DragData>;
async drag(this: ElementHandle<Element>): Promise<Protocol.Input.DragData> {
throw new Error('Not implemented');
target: Point | ElementHandle<Element>
): Promise<Protocol.Input.DragData | void> {
await this.scrollIntoViewIfNeeded();
const page = this.frame.page();
if (page.isDragInterceptionEnabled()) {
const source = await this.clickablePoint();
if (target instanceof ElementHandle) {
target = await target.clickablePoint();
}
return await page.mouse.drag(source, target);
}
try {
if (!page._isDragging) {
page._isDragging = true;
await this.hover();
await page.mouse.down();
}
if (target instanceof ElementHandle) {
await target.hover();
} else {
await page.mouse.move(target.x, target.y);
}
} catch (error) {
page._isDragging = false;
throw error;
}
}
/**
* This method creates a `dragenter` event on the element.
* @deprecated Do not use. `dragenter` will automatically be performed during dragging.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async dragEnter(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async dragEnter(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
const page = this.frame.page();
await this.scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await page.mouse.dragEnter(target, data);
}
/**
* This method creates a `dragover` event on the element.
* @deprecated Do not use. `dragover` will automatically be performed during dragging.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async dragOver(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async dragOver(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
const page = this.frame.page();
await this.scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await page.mouse.dragOver(target, data);
}
/**
* This method triggers a drop on the element.
* Drops the given element onto the current one.
*/
async drop(
this: ElementHandle<Element>,
element: ElementHandle<Element>
): Promise<void>;
/**
* @deprecated No longer supported.
*/
async drop(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async drop(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
/**
* @internal
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async drop(
this: ElementHandle<Element>,
dataOrElement: ElementHandle<Element> | Protocol.Input.DragData = {
items: [],
dragOperationsMask: 1,
}
): Promise<void> {
const page = this.frame.page();
if ('items' in dataOrElement) {
await this.scrollIntoViewIfNeeded();
const destination = await this.clickablePoint();
await page.mouse.drop(destination, dataOrElement);
} else {
// Note if the rest errors, we still want dragging off because the errors
// is most likely something implying the mouse is no longer dragging.
await dataOrElement.drag(this);
page._isDragging = false;
await page.mouse.up();
}
}
/**
* This method triggers a dragenter, dragover, and drop on the element.
* @deprecated Use `ElementHandle.drop` instead.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async dragAndDrop(
this: ElementHandle<Element>,
target: ElementHandle<Node>,
options?: {delay: number}
): Promise<void>;
async dragAndDrop(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
): Promise<void> {
const page = this.frame.page();
assert(
page.isDragInterceptionEnabled(),
'Drag Interception is not enabled!'
);
await this.scrollIntoViewIfNeeded();
const startPoint = await this.clickablePoint();
const targetPoint = await target.clickablePoint();
await page.mouse.dragAndDrop(startPoint, targetPoint, options);
}
/**

View File

@ -509,6 +509,11 @@ export abstract class Page
extends EventEmitter<PageEvents>
implements AsyncDisposable, Disposable
{
/**
* @internal
*/
_isDragging = false;
#requestHandlers = new WeakMap<Handler<HTTPRequest>, Handler<HTTPRequest>>();
/**
@ -527,6 +532,10 @@ export abstract class Page
/**
* `true` if drag events are being intercepted, `false` otherwise.
*
* @deprecated We no longer support intercepting drag payloads. Use the new
* drag APIs found on {@link ElementHandle} to drag (or just use the
* {@link Page.mouse}).
*/
isDragInterceptionEnabled(): boolean {
throw new Error('Not implemented');
@ -791,10 +800,9 @@ export abstract class Page
/**
* @param enabled - Whether to enable drag interception.
*
* @remarks
* Activating drag interception enables the `Input.drag`,
* methods This provides the capability to capture drag events emitted
* on the page, which can then be used to simulate drag-and-drop.
* @deprecated We no longer support intercepting drag payloads. Use the new
* drag APIs found on {@link ElementHandle} to drag (or just use the
* {@link Page.mouse}).
*/
async setDragInterception(enabled: boolean): Promise<void>;
async setDragInterception(): Promise<void> {

View File

@ -17,7 +17,7 @@
import {Protocol} from 'devtools-protocol';
import {CDPSession} from '../api/CDPSession.js';
import {AutofillData, ElementHandle, Point} from '../api/ElementHandle.js';
import {AutofillData, ElementHandle} from '../api/ElementHandle.js';
import {Page, ScreenshotOptions} from '../api/Page.js';
import {assert} from '../util/assert.js';
import {throwIfDisposed} from '../util/decorators.js';
@ -103,74 +103,6 @@ export class CdpElementHandle<
}
}
/**
* This method creates and captures a dragevent from the element.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async drag(
this: CdpElementHandle<Element>,
target: Point
): Promise<Protocol.Input.DragData> {
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);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async dragEnter(
this: CdpElementHandle<Element>,
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
await this.scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await this.#page.mouse.dragEnter(target, data);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async dragOver(
this: CdpElementHandle<Element>,
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
await this.scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await this.#page.mouse.dragOver(target, data);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async drop(
this: CdpElementHandle<Element>,
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
await this.scrollIntoViewIfNeeded();
const destination = await this.clickablePoint();
await this.#page.mouse.drop(destination, data);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async dragAndDrop(
this: CdpElementHandle<Element>,
target: CdpElementHandle<Node>,
options?: {delay: number}
): Promise<void> {
assert(
this.#page.isDragInterceptionEnabled(),
'Drag Interception is not enabled!'
);
await this.scrollIntoViewIfNeeded();
const startPoint = await this.clickablePoint();
const targetPoint = await target.clickablePoint();
await this.#page.mouse.dragAndDrop(startPoint, targetPoint, options);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async uploadFile(

View File

@ -702,6 +702,10 @@ export class BidiPage extends Page {
'default' in pptrFunction ? pptrFunction.default : pptrFunction
);
}
override isDragInterceptionEnabled(): boolean {
return false;
}
}
function isConsoleLogEntry(

View File

@ -41,6 +41,24 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[drag-and-drop.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[drag-and-drop.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[drag-and-drop.spec] Legacy Drag n' Drop *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[elementhandle.spec] *",
"platforms": ["darwin", "linux", "win32"],
@ -1691,6 +1709,12 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[drag-and-drop.spec] Drag n' Drop should drag and drop",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "chrome"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames",
"platforms": ["darwin", "linux", "win32"],

View File

@ -13,24 +13,21 @@
<body>
<div id="drag" draggable="true">drag me</div>
<div id="drop"></div>
<div id="drag-state">0</div>
<script>
window.didDragStart = false;
window.didDragEnter = false;
window.didDragOver = false;
window.didDrop = false;
const drag = document.getElementById('drag');
const drop = document.getElementById('drop');
drag.addEventListener('dragstart', function(event) {
event.dataTransfer.setData('id', event.target.id);
window.didDragStart = true;
document.getElementById('drag-state').textContent += '1';
});
drop.addEventListener('dragenter', function(event) {
event.preventDefault();
window.didDragEnter = true;
document.getElementById('drag-state').textContent += '2';
});
drop.addEventListener('dragover', function(event) {
event.preventDefault();
window.didDragOver = true;
document.getElementById('drag-state').textContent += '3';
});
drop.addEventListener('drop', function(event) {
event.preventDefault();
@ -38,7 +35,7 @@
const el = document.getElementById(id);
if (el) {
event.target.appendChild(el);
window.didDrop = true;
document.getElementById('drag-state').textContent += '4';
}
});
</script>

View File

@ -14,11 +14,23 @@
* limitations under the License.
*/
import assert from 'assert';
import expect from 'expect';
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
describe('Input.drag', function () {
async function getDragState() {
const {page} = await getTestState({skipLaunch: true});
return parseInt(
await page.$eval('#drag-state', element => {
return element.innerHTML;
}),
10
);
}
describe("Legacy Drag n' Drop", function () {
setupTestBrowserHooks();
it('should throw an exception if not enabled before usage', async () => {
@ -45,12 +57,9 @@ describe('Input.drag', function () {
using draggable = (await page.$('#drag'))!;
const data = await draggable.drag({x: 1, y: 1});
assert(data instanceof Object);
expect(data.items).toHaveLength(1);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragStart;
})
).toBe(true);
expect(await getDragState()).toBe(1);
});
it('should emit a dragEnter', async () => {
const {page, server} = await getTestState();
@ -61,19 +70,11 @@ describe('Input.drag', function () {
expect(page.isDragInterceptionEnabled()).toBe(true);
using draggable = (await page.$('#drag'))!;
const data = await draggable.drag({x: 1, y: 1});
assert(data instanceof Object);
using dropzone = (await page.$('#drop'))!;
await dropzone.dragEnter(data);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragStart;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragEnter;
})
).toBe(true);
expect(await getDragState()).toBe(12);
});
it('should emit a dragOver event', async () => {
const {page, server} = await getTestState();
@ -84,25 +85,12 @@ describe('Input.drag', function () {
expect(page.isDragInterceptionEnabled()).toBe(true);
using draggable = (await page.$('#drag'))!;
const data = await draggable.drag({x: 1, y: 1});
assert(data instanceof Object);
using dropzone = (await page.$('#drop'))!;
await dropzone.dragEnter(data);
await dropzone.dragOver(data);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragStart;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragEnter;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragOver;
})
).toBe(true);
expect(await getDragState()).toBe(123);
});
it('can be dropped', async () => {
const {page, server} = await getTestState();
@ -114,30 +102,12 @@ describe('Input.drag', function () {
using draggable = (await page.$('#drag'))!;
using dropzone = (await page.$('#drop'))!;
const data = await draggable.drag({x: 1, y: 1});
assert(data instanceof Object);
await dropzone.dragEnter(data);
await dropzone.dragOver(data);
await dropzone.drop(data);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragStart;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragEnter;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragOver;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDrop;
})
).toBe(true);
expect(await getDragState()).toBe(12334);
});
it('can be dragged and dropped with a single function', async () => {
const {page, server} = await getTestState();
@ -150,44 +120,59 @@ describe('Input.drag', function () {
using dropzone = (await page.$('#drop'))!;
await draggable.dragAndDrop(dropzone);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragStart;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragEnter;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDragOver;
})
).toBe(true);
expect(
await page.evaluate(() => {
return (globalThis as any).didDrop;
})
).toBe(true);
expect(await getDragState()).toBe(12334);
});
it('can be disabled', async () => {
});
describe("Drag n' Drop", () => {
setupTestBrowserHooks();
it('should drop', async () => {
const {page, server} = await getTestState();
await page.goto(server.PREFIX + '/input/drag-and-drop.html');
expect(page.isDragInterceptionEnabled()).toBe(false);
await page.setDragInterception(true);
expect(page.isDragInterceptionEnabled()).toBe(true);
using draggable = (await page.$('#drag'))!;
await draggable.drag({x: 1, y: 1});
await page.setDragInterception(false);
try {
await draggable.drag({x: 1, y: 1});
} catch (error) {
expect((error as Error).message).toContain(
'Drag Interception is not enabled!'
);
}
using draggable = await page.$('#drag');
assert(draggable);
using dropzone = await page.$('#drop');
assert(dropzone);
await dropzone.drop(draggable);
expect(await getDragState()).toBe(1234);
});
it('should drop using mouse', async () => {
const {page, server} = await getTestState();
await page.goto(server.PREFIX + '/input/drag-and-drop.html');
using draggable = await page.$('#drag');
assert(draggable);
using dropzone = await page.$('#drop');
assert(dropzone);
await draggable.hover();
await page.mouse.down();
await dropzone.hover();
expect(await getDragState()).toBe(123);
await page.mouse.up();
expect(await getDragState()).toBe(1234);
});
it('should drag and drop', async () => {
const {page, server} = await getTestState();
await page.goto(server.PREFIX + '/input/drag-and-drop.html');
using draggable = await page.$('#drag');
assert(draggable);
using dropzone = await page.$('#drop');
assert(dropzone);
await draggable.drag(dropzone);
await dropzone.drop(draggable);
expect(await getDragState()).toBe(1234);
});
});