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
+ +
+
Drop Zone 1
+
+
+
Drop Zone 2
+
+
+ + + + +'); + +// 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); + } }