Skip to content

Commit

Permalink
fix(drag-n-drop): send two mousemove events to target to make Angular…
Browse files Browse the repository at this point in the history
… work
  • Loading branch information
mxschmitt committed Feb 21, 2025
1 parent e4ceac8 commit b930eba
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 1 deletion.
4 changes: 4 additions & 0 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,10 @@ export class Frame extends SdkObject {
// Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation.
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, options.strict, false /* performActionPreChecks */, async handle => {
return handle._retryPointerAction(progress, 'move and up', false, async point => {
// NOTE: Normal browsers emit usually a lot of dragover/mousemove events during drag'n
// drop operations. We want to emit minimal to make the ecosystem work. When using
// Native drag'n drop the browser does emit dropover events instead.
await this._page.mouse.move(point.x, point.y);
await this._page.mouse.move(point.x, point.y);
await this._page.mouse.up();
}, {
Expand Down
129 changes: 129 additions & 0 deletions tests/assets/drag-n-drop-manual.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<style>
* {
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
div:not(.mouse-helper) {
margin: 0;
padding: 0;
}
#source {
color: blue;
border: 1px solid black;
position: absolute;
left: 20px;
top: 20px;
width: 200px;
height: 100px;
cursor: move;
user-select: none;
}
#target {
border: 1px solid black;
position: absolute;
left: 40px;
top: 200px;
width: 400px;
height: 300px;
}
</style>

<body>
<div>
<p id="source">
Select this element, drag it to the Drop Zone and then release the selection to move the element.</p>
</div>
<div id="target">Drop Zone</div>

<script>
// Elements
const sourceElement = document.getElementById('source');
const targetElement = document.getElementById('target');

// State variables
let isDragging = false;
let offsetX, offsetY;
let originalPosition = { left: 0, top: 0 };

// Mouse down handler - start dragging
sourceElement.addEventListener('mousedown', function(e) {
e.preventDefault();

// Store the original position
const rect = sourceElement.getBoundingClientRect();
originalPosition = {
left: rect.left,
top: rect.top
};

// Calculate the mouse offset
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;

// Start dragging
isDragging = true;
sourceElement.style.border = 'dashed'

console.log('mousedown', e.clientX, e.clientY);
});

// Mouse move handler - move the element
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;

// Set new position
sourceElement.style.left = (e.clientX - offsetX) + 'px';
sourceElement.style.top = (e.clientY - offsetY) + 'px';
});

// Mouse up handler - stop dragging and check if dropped on target
document.addEventListener('mouseup', function(e) {
if (!isDragging) return;

isDragging = false;

// Check if dropped on target area
const sourceRect = sourceElement.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();

const isOverlapping = !(
sourceRect.right < targetRect.left ||
sourceRect.left > targetRect.right ||
sourceRect.bottom < targetRect.top ||
sourceRect.top > targetRect.bottom
);

if (isOverlapping) {
// Successful drop - append to target
targetElement.appendChild(sourceElement);

// Reset position relative to the new parent
sourceElement.style.removeProperty('position')
sourceElement.style.removeProperty('top')
sourceElement.style.removeProperty('left')

console.log('Drop successful');
} else {
// Failed drop - return to original position
sourceElement.style.left = originalPosition.left + 'px';
sourceElement.style.top = originalPosition.top + 'px';

console.log('Drop failed - returning to original position');
}
});

// Cancel dragging if mouse leaves the window
document.addEventListener('mouseleave', function() {
if (isDragging) {
isDragging = false;

// Return to original position
sourceElement.style.left = originalPosition.left + 'px';
sourceElement.style.top = originalPosition.top + 'px';
}
});
</script>
</body>
47 changes: 47 additions & 0 deletions tests/page/page-drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ it.describe('Drag and drop', () => {
]);
});

it('should send the right events when using dragTo', async ({ server, page, browserName }) => {
await page.goto(server.PREFIX + '/drag-n-drop.html');
const events = await trackEvents(await page.$('body'));
await page.locator('#source').dragTo(page.locator('#target'));
expect(await events.jsonValue()).toEqual([
'mousemove at 120;86',
'mousedown at 120;86',
browserName === 'firefox' ? 'dragstart at 120;86' : 'mousemove at 240;350',
browserName === 'firefox' ? 'mousemove at 240;350' : 'dragstart at 120;86',
'dragenter at 240;350',
// NOTE: Normal browsers emit usually a lot of dragover events during drag'n
// drop operations. We want to emit minimal (2) to make the ecosystem work.
'dragover at 240;350',
'dragover at 240;350',
'drop at 240;350',
'dragend',
]);
});

it('should not send dragover on the first mousemove', async ({ server, page, browserName }) => {
it.fixme(browserName !== 'chromium');

Expand Down Expand Up @@ -293,6 +312,34 @@ it.describe('Drag and drop', () => {
expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true); // could not find source in target
});

it('should work with manual drag\'n drop and emit correct mousemove events', {
annotation: {
type: 'issue',
description: 'https://github.com/microsoft/playwright/issues/34688',
}
}, async ({ page, server }) => {
await page.goto(server.PREFIX + '/drag-n-drop-manual.html');
const events = await page.evaluateHandle(() => {
const events = [];
document.addEventListener('drop', (event: MouseEvent) => {
events.push({
type: event.type,
x: event.clientX,
y: event.clientY,
});
});
return events;
});
await page.dragAndDrop('#source', '#target');
expect(await events.jsonValue()).toEqual([
{ type: 'mousemove', x: 120, y: 86 },
// NOTE: Normal browsers emit usually a lot of mousemove events during drag'n
// drop operations. We want to emit minimal(2) to make the ecosystem work.
{ type: 'mousemove', x: 240, y: 350 },
{ type: 'mousemove', x: 240, y: 350 },
]);
});

it('should allow specifying the position', async ({ page, server }) => {
await page.setContent(`
<div style="width:100px;height:100px;background:red;" id="red">
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright-test/playwright-test-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type RunResult = {
interrupted: number,
didNotRun: number,
report: JSONReport,
results: any[],
results: JSONReportTestResult[],
};

type TSCResult = {
Expand Down

0 comments on commit b930eba

Please sign in to comment.