Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
188 changes: 188 additions & 0 deletions docs/examples/drag-and-drop.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

declare(strict_types=1);

/*
* This file is part of the playwright-php/playwright package.
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

require_once __DIR__.'/../../vendor/autoload.php';

use PlaywrightPHP\Playwright;

$browser = Playwright::chromium();
$page = $browser->newPage();

// Navigate to a page with draggable elements
$page->setContent('
<!DOCTYPE html>
<html>
<head>
<style>
.container { padding: 20px; }
.draggable {
width: 100px;
height: 100px;
background: #4CAF50;
color: white;
text-align: center;
line-height: 100px;
cursor: move;
margin: 10px;
user-select: none;
}
.dropzone {
width: 200px;
height: 200px;
border: 2px dashed #ccc;
text-align: center;
margin: 20px;
background: #f9f9f9;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 10px;
box-sizing: border-box;
}
.dropzone.dragover {
background: #e8f5e8;
border-color: #4CAF50;
}
.dropzone.has-items {
background: #e8f4fd;
border-color: #2196F3;
border-style: solid;
}
.dropzone .label {
font-weight: bold;
margin-bottom: 10px;
color: #666;
}
.dropped-item {
background: #2196F3 !important;
color: white !important;
margin: 5px 0;
transform: scale(0.9);
border-radius: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Drag and Drop Demo</h1>

<div class="draggable" id="item1" draggable="true">Item 1</div>
<div class="draggable" id="item2" draggable="true">Item 2</div>
<div class="draggable" id="item3" draggable="true">Item 3</div>

<div class="dropzone" id="dropzone1">
<div class="label">Drop Zone 1</div>
</div>
<div class="dropzone" id="dropzone2">
<div class="label">Drop Zone 2</div>
</div>
</div>

<script>
// Add drag and drop event handlers
document.querySelectorAll(".draggable").forEach(item => {
item.addEventListener("dragstart", e => {
e.dataTransfer.setData("text/plain", e.target.id);
});
});

document.querySelectorAll(".dropzone").forEach(zone => {
zone.addEventListener("dragover", e => {
e.preventDefault();
zone.classList.add("dragover");
});

zone.addEventListener("dragleave", () => {
zone.classList.remove("dragover");
});

zone.addEventListener("drop", e => {
e.preventDefault();
const itemId = e.dataTransfer.getData("text/plain");
const item = document.getElementById(itemId);

// Add visual feedback
item.classList.add("dropped-item");

// Append to drop zone (keeping the label)
zone.appendChild(item);
zone.classList.remove("dragover");
zone.classList.add("has-items");
});
});
</script>
</body>
</html>
');

// 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";
25 changes: 25 additions & 0 deletions src/Locator/Locator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed> $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<string, mixed> $options
*/
Expand Down
7 changes: 7 additions & 0 deletions src/Locator/LocatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ public function uncheck(array $options = []): void;
*/
public function hover(array $options = []): void;

/**
* Drag this element to target element.
*
* @param array<string, mixed> $options
*/
public function dragTo(LocatorInterface $target, array $options = []): void;

/**
* @param array<string, mixed> $options
*/
Expand Down
101 changes: 101 additions & 0 deletions tests/Integration/Locator/LocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div id="draggable"
draggable="true"
style="width: 50px; height: 50px; background: red; margin: 10px;"
data-status="source">
Drag me
</div>
<div id="dropzone"
style="width: 150px; height: 150px; background: lightblue; margin: 10px; border: 2px dashed #ccc;"
data-status="target">
Drop here
</div>
`;

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 = `
<div id="precise-draggable"
draggable="true"
style="width: 100px; height: 100px; background: green; margin: 20px;"
data-drag-count="0">
Precise drag
</div>
<div id="precise-dropzone"
style="width: 200px; height: 200px; background: lightyellow; margin: 20px; border: 2px solid #999;"
data-drops="0">
Precise drop zone
</div>
`;

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;
Expand Down
Loading