diff --git a/bin/lib/handlers.js b/bin/lib/handlers.js
index 7fe0b12..c85b8b1 100644
--- a/bin/lib/handlers.js
+++ b/bin/lib/handlers.js
@@ -254,7 +254,8 @@ class LocatorHandler extends BaseHandler {
getAttribute: () => PromiseUtils.wrapValue(locator.getAttribute(command.name)),
selectOption: () => PromiseUtils.wrapValues(locator.selectOption(command.values, command.options)),
screenshot: () => PromiseUtils.wrapBinary(locator.screenshot(command.options)),
- evaluate: () => this.evaluateLocator(locator, command)
+ evaluate: () => this.evaluateLocator(locator, command),
+ dragAndDrop: () => this.handleDragAndDrop(page, command)
});
return await this.executeWithRegistry(registry, method);
@@ -298,6 +299,31 @@ class LocatorHandler extends BaseHandler {
throw error;
}
}
+
+ async handleDragAndDrop(page, command) {
+ logger.info('Handling drag and drop', {
+ selector: command.selector,
+ target: command.target,
+ options: command.options
+ });
+
+ try {
+ // Use page.dragAndDrop which is the native Playwright method
+ await page.dragAndDrop(command.selector, command.target, {
+ strict: true,
+ ...command.options
+ });
+
+ return this.createValueResult(true);
+ } catch (error) {
+ logger.error('Drag and drop failed', {
+ selector: command.selector,
+ target: command.target,
+ error: error.message
+ });
+ throw error;
+ }
+ }
}
class InteractionHandler extends BaseHandler {
diff --git a/docs/examples/drag-and-drop.php b/docs/examples/drag-and-drop.php
new file mode 100644
index 0000000..498949e
--- /dev/null
+++ b/docs/examples/drag-and-drop.php
@@ -0,0 +1,188 @@
+newPage();
+
+// Navigate to a page with draggable elements
+$page->setContent('
+
+
+
+
+
+
+
+
Drag and Drop Demo
+
+
Item 1
+
Item 2
+
Item 3
+
+
+
+
+
+
+
+
+');
+
+// Take initial screenshot
+$page->screenshot('/tmp/drag-drop-initial.png');
+echo "Initial screenshot saved: /tmp/drag-drop-initial.png\n";
+
+// Example 1: Basic drag and drop
+echo "Performing basic drag and drop...\n";
+$draggableItem = $page->locator('#item1');
+$dropZone = $page->locator('#dropzone1');
+
+$draggableItem->dragTo($dropZone);
+$page->screenshot('/tmp/drag-drop-after-item1.png');
+echo "Screenshot after dragging item1: /tmp/drag-drop-after-item1.png\n";
+
+// Example 2: Drag with specific positions
+echo "Performing drag with specific source and target positions...\n";
+$draggableItem2 = $page->locator('#item2');
+$dropZone2 = $page->locator('#dropzone2');
+
+$draggableItem2->dragTo($dropZone2, [
+ 'sourcePosition' => ['x' => 50, 'y' => 50], // Center of source element
+ 'targetPosition' => ['x' => 100, 'y' => 100], // Center of target element
+]);
+$page->screenshot('/tmp/drag-drop-after-item2.png');
+echo "Screenshot after dragging item2 with positions: /tmp/drag-drop-after-item2.png\n";
+
+// Example 3: Force drag (bypass actionability checks)
+echo "Performing forced drag and drop...\n";
+$draggableItem3 = $page->locator('#item3');
+
+$draggableItem3->dragTo($dropZone, [
+ 'force' => true,
+ 'timeout' => 5000,
+]);
+$page->screenshot('/tmp/drag-drop-after-item3.png');
+echo "Screenshot after forced drag of item3: /tmp/drag-drop-after-item3.png\n";
+
+// Example 4: Verify drop results
+echo "Verifying drop results...\n";
+$dropZone1Items = $page->locator('#dropzone1 .draggable');
+$dropZone2Items = $page->locator('#dropzone2 .draggable');
+
+$zone1Count = $dropZone1Items->count();
+$zone2Count = $dropZone2Items->count();
+
+echo "Drop Zone 1 contains $zone1Count items\n";
+echo "Drop Zone 2 contains $zone2Count items\n";
+
+// Take final screenshot
+$page->screenshot('/tmp/drag-drop-final.png');
+echo "Final screenshot saved: /tmp/drag-drop-final.png\n";
+
+// Cleanup
+$page->close();
+$browser->close();
+
+echo "\nDrag and drop examples completed successfully!\n";
+echo "Screenshots saved in /tmp/ directory:\n";
+echo "- drag-drop-initial.png\n";
+echo "- drag-drop-after-item1.png\n";
+echo "- drag-drop-after-item2.png\n";
+echo "- drag-drop-after-item3.png\n";
+echo "- drag-drop-final.png\n";
diff --git a/src/Locator/Locator.php b/src/Locator/Locator.php
index 1a77666..9b57584 100644
--- a/src/Locator/Locator.php
+++ b/src/Locator/Locator.php
@@ -344,6 +344,31 @@ public function hover(array $options = []): void
$this->transport->processEvents();
}
+ /**
+ * Drag this element to the target element.
+ *
+ * Supports the following options:
+ * - sourcePosition: {x: int, y: int} - A point to use relative to the top-left corner of element
+ * - targetPosition: {x: int, y: int} - A point to use relative to the top-left corner of target element
+ * - force: bool - Whether to bypass the actionability checks
+ * - timeout: int - Maximum time in milliseconds
+ *
+ * @param array $options
+ */
+ public function dragTo(LocatorInterface $target, array $options = []): void
+ {
+ $this->waitForActionable();
+
+ $targetSelector = $target->getSelector();
+
+ $this->sendCommand('locator.dragAndDrop', [
+ 'target' => $targetSelector,
+ 'options' => $options,
+ ]);
+
+ $this->transport->processEvents();
+ }
+
/**
* @param array $options
*/
diff --git a/src/Locator/LocatorInterface.php b/src/Locator/LocatorInterface.php
index 2359936..ffbca17 100644
--- a/src/Locator/LocatorInterface.php
+++ b/src/Locator/LocatorInterface.php
@@ -52,6 +52,13 @@ public function uncheck(array $options = []): void;
*/
public function hover(array $options = []): void;
+ /**
+ * Drag this element to target element.
+ *
+ * @param array $options
+ */
+ public function dragTo(LocatorInterface $target, array $options = []): void;
+
/**
* @param array $options
*/
diff --git a/tests/Integration/Locator/LocatorTest.php b/tests/Integration/Locator/LocatorTest.php
index 53d2a3f..8eeb200 100644
--- a/tests/Integration/Locator/LocatorTest.php
+++ b/tests/Integration/Locator/LocatorTest.php
@@ -198,6 +198,107 @@ public function itBlursAnElement(): void
$this->assertSame('1', $blurred);
}
+ #[Test]
+ public function itPerformsDragAndDrop(): void
+ {
+ // Add drag and drop HTML elements
+ $this->page->evaluate(<<<'JS'
+ () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ Drag me
+
+
+ Drop here
+
+ `;
+
+ const dropzone = container.querySelector('#dropzone');
+ dropzone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ });
+
+ dropzone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ const draggable = document.querySelector('#draggable');
+ dropzone.appendChild(draggable);
+ dropzone.dataset.status = 'dropped';
+ });
+
+ document.body.appendChild(container);
+ }
+ JS);
+
+ $draggable = $this->page->locator('#draggable');
+ $dropzone = $this->page->locator('#dropzone');
+
+ // Verify initial state
+ $this->assertEquals('source', $draggable->getAttribute('data-status'));
+ $this->assertEquals('target', $dropzone->getAttribute('data-status'));
+
+ // Perform drag and drop
+ $draggable->dragTo($dropzone);
+
+ // Verify the drop was successful
+ $this->assertEquals('dropped', $dropzone->getAttribute('data-status'));
+ }
+
+ #[Test]
+ public function itPerformsDragAndDropWithOptions(): void
+ {
+ // Add drag and drop HTML elements
+ $this->page->evaluate(<<<'JS'
+ () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ Precise drag
+
+
+ Precise drop zone
+
+ `;
+
+ const dropzone = container.querySelector('#precise-dropzone');
+ dropzone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ });
+
+ dropzone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ const currentDrops = parseInt(dropzone.dataset.drops) + 1;
+ dropzone.dataset.drops = currentDrops.toString();
+ });
+
+ document.body.appendChild(container);
+ }
+ JS);
+
+ $draggable = $this->page->locator('#precise-draggable');
+ $dropzone = $this->page->locator('#precise-dropzone');
+
+ // Perform drag and drop with specific positions
+ $draggable->dragTo($dropzone, [
+ 'sourcePosition' => ['x' => 50, 'y' => 50], // center of draggable
+ 'targetPosition' => ['x' => 100, 'y' => 100], // center of dropzone
+ ]);
+
+ // Verify the drop was successful
+ $this->assertEquals('1', $dropzone->getAttribute('data-drops'));
+ }
+
private static function findFreePort(): int
{
return 0;
diff --git a/tests/Unit/Locator/LocatorMethodsTest.php b/tests/Unit/Locator/LocatorMethodsTest.php
index 6855ad4..8461ef0 100644
--- a/tests/Unit/Locator/LocatorMethodsTest.php
+++ b/tests/Unit/Locator/LocatorMethodsTest.php
@@ -301,4 +301,108 @@ public function testEvaluateWithNullResult(): void
$result = $this->locator->evaluate('element.nonexistent');
$this->assertNull($result);
}
+
+ public function testDragToBasic(): void
+ {
+ $targetLocator = new Locator($this->transport, 'page1', '.target');
+
+ $this->transport
+ ->expects($this->exactly(3))
+ ->method('send')
+ ->willReturnCallback(function ($payload) {
+ // First call: isVisible check
+ if ('locator.isVisible' === $payload['action']) {
+ return ['value' => true];
+ }
+ // Second call: isEnabled check
+ if ('locator.isEnabled' === $payload['action']) {
+ return ['value' => true];
+ }
+ // Third call: actual dragAndDrop
+ if ('locator.dragAndDrop' === $payload['action']
+ && '.target' === $payload['target']
+ && [] === $payload['options']) {
+ return ['value' => true];
+ }
+
+ return [];
+ });
+
+ $this->transport
+ ->expects($this->once())
+ ->method('processEvents');
+
+ $this->locator->dragTo($targetLocator);
+ }
+
+ public function testDragToWithOptions(): void
+ {
+ $targetLocator = new Locator($this->transport, 'page1', '.target');
+ $options = [
+ 'sourcePosition' => ['x' => 10, 'y' => 15],
+ 'targetPosition' => ['x' => 20, 'y' => 25],
+ 'force' => true,
+ 'timeout' => 5000,
+ ];
+
+ $this->transport
+ ->expects($this->exactly(3))
+ ->method('send')
+ ->willReturnCallback(function ($payload) use ($options) {
+ // First call: isVisible check
+ if ('locator.isVisible' === $payload['action']) {
+ return ['value' => true];
+ }
+ // Second call: isEnabled check
+ if ('locator.isEnabled' === $payload['action']) {
+ return ['value' => true];
+ }
+ // Third call: actual dragAndDrop
+ if ('locator.dragAndDrop' === $payload['action']
+ && '.target' === $payload['target']
+ && $options === $payload['options']) {
+ return ['value' => true];
+ }
+
+ return [];
+ });
+
+ $this->transport
+ ->expects($this->once())
+ ->method('processEvents');
+
+ $this->locator->dragTo($targetLocator, $options);
+ }
+
+ public function testDragToWithComplexSelectors(): void
+ {
+ $targetLocator = new Locator($this->transport, 'page1', '#dropzone .drop-target[data-accept="files"]');
+
+ $this->transport
+ ->expects($this->exactly(3))
+ ->method('send')
+ ->willReturnCallback(function ($payload) {
+ // First call: isVisible check
+ if ('locator.isVisible' === $payload['action']) {
+ return ['value' => true];
+ }
+ // Second call: isEnabled check
+ if ('locator.isEnabled' === $payload['action']) {
+ return ['value' => true];
+ }
+ // Third call: actual dragAndDrop
+ if ('locator.dragAndDrop' === $payload['action']
+ && '#dropzone .drop-target[data-accept="files"]' === $payload['target']) {
+ return ['value' => true];
+ }
+
+ return [];
+ });
+
+ $this->transport
+ ->expects($this->once())
+ ->method('processEvents');
+
+ $this->locator->dragTo($targetLocator);
+ }
}