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
50 changes: 50 additions & 0 deletions src/main/java/com/seleniumboot/locator/Locator.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@
@SeleniumBootApi(since = "1.4.0")
public final class Locator {

/** Records whether a native drag actually delivered a {@code drop} event to the target. */
private static final String ARM_DROP_LISTENER_JS =
"var t = arguments[0]; t.__sbDropped = false;"
+ "t.__sbDropHandler = function () { t.__sbDropped = true; };"
+ "t.addEventListener('drop', t.__sbDropHandler);";

private static final String COLLECT_DROP_RESULT_JS =
"var t = arguments[0]; t.removeEventListener('drop', t.__sbDropHandler);"
+ "var dropped = t.__sbDropped === true; delete t.__sbDropped; delete t.__sbDropHandler;"
+ "return dropped;";

/** HTML5 DnD fallback: replays the event sequence with one shared DataTransfer. */
private static final String SYNTHETIC_DRAG_JS =
"var src = arguments[0], dst = arguments[1], dt = new DataTransfer();"
+ "function fire(el, type) {"
+ " var r = el.getBoundingClientRect();"
+ " el.dispatchEvent(new DragEvent(type, {bubbles: true, cancelable: true, composed: true,"
+ " dataTransfer: dt, clientX: r.left + r.width / 2, clientY: r.top + r.height / 2}));"
+ "}"
+ "fire(src, 'dragstart'); fire(dst, 'dragenter'); fire(dst, 'dragover');"
+ "fire(dst, 'drop'); fire(src, 'dragend');";

/** How the base set of candidate elements is derived. */
private enum Kind { CSS_OR_BY, ROLE, TEXT, LABEL, PLACEHOLDER, TESTID, ALT_TEXT, TITLE }

Expand Down Expand Up @@ -305,6 +327,34 @@ public void jsClick() {
});
}

/**
* Drags this element onto {@code target} and drops it there.
*
* <p>Tries the native {@link Actions#dragAndDrop} gesture first. Pages built on the HTML5
* drag-and-drop API ({@code draggable="true"} plus {@code dragstart}/{@code drop} listeners)
* often ignore it, so when no {@code drop} event reached the target, the full sequence
* ({@code dragstart, dragenter, dragover, drop, dragend}) is dispatched with a synthetic
* {@code DataTransfer}. Both elements are waited for and re-resolved like every other
* terminal action.
*
* <pre>{@code
* Locator.byText("Card A").dragTo(Locator.ofCss("#done-column"));
* }</pre>
*
* @throws LocatorException if either element cannot be resolved within {@code timeouts.explicit}
*/
public void dragTo(Locator target) {
target.whenVisible(dst -> whenVisible(src -> {
JavascriptExecutor js = (JavascriptExecutor) driver();
js.executeScript(ARM_DROP_LISTENER_JS, dst);
new Actions(driver()).dragAndDrop(src, dst).perform();
if (!Boolean.TRUE.equals(js.executeScript(COLLECT_DROP_RESULT_JS, dst))) {
js.executeScript(SYNTHETIC_DRAG_JS, src, dst);
}
return null;
}));
}

/** Returns the number of elements currently matching the locator chain (no wait). */
public int count() {
return resolveAll().size();
Expand Down
106 changes: 106 additions & 0 deletions src/test/java/com/seleniumboot/unit/LocatorDragToTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.seleniumboot.unit;

import com.seleniumboot.config.SeleniumBootConfig;
import com.seleniumboot.driver.DriverManager;
import com.seleniumboot.internal.SeleniumBootContext;
import com.seleniumboot.locator.Locator;
import com.seleniumboot.locator.LocatorException;
import org.mockito.MockedStatic;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Interactive;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.testng.Assert.assertThrows;

/** {@link Locator#dragTo}: native gesture first, synthetic HTML5 DnD only when no drop landed. */
public class LocatorDragToTest {

private WebDriver driver;
private JavascriptExecutor js;
private MockedStatic<DriverManager> driverManagerMock;
private MockedStatic<SeleniumBootContext> contextMock;
private WebElement src;
private WebElement dst;

@BeforeMethod
public void setup() {
driver = mock(WebDriver.class, withSettings().extraInterfaces(JavascriptExecutor.class, Interactive.class));
js = (JavascriptExecutor) driver;
driverManagerMock = mockStatic(DriverManager.class);
driverManagerMock.when(DriverManager::getDriver).thenReturn(driver);

SeleniumBootConfig.Timeouts timeouts = new SeleniumBootConfig.Timeouts();
timeouts.setExplicit(1);
SeleniumBootConfig config = new SeleniumBootConfig();
config.setTimeouts(timeouts);
contextMock = mockStatic(SeleniumBootContext.class);
contextMock.when(SeleniumBootContext::getConfig).thenReturn(config);

src = visible();
dst = visible();
when(driver.findElements(By.id("src"))).thenReturn(List.of(src));
when(driver.findElements(By.id("dst"))).thenReturn(List.of(dst));
}

@AfterMethod
public void teardown() {
driverManagerMock.close();
contextMock.close();
}

private static WebElement visible() {
WebElement el = mock(WebElement.class);
when(el.isDisplayed()).thenReturn(true);
when(el.isEnabled()).thenReturn(true);
return el;
}

@Test
public void dragTo_nativeDropLands_doesNotDispatchSyntheticEvents() {
when(js.executeScript(contains("__sbDropped === true"), any())).thenReturn(true);

Locator.of(By.id("src")).dragTo(Locator.of(By.id("dst")));

verify((Interactive) driver).perform(any());
verify(js, never()).executeScript(contains("dragstart"), any(), any());
}

@Test
public void dragTo_nativeDropIgnored_fallsBackToSyntheticDataTransfer() {
when(js.executeScript(contains("__sbDropped === true"), any())).thenReturn(false);

Locator.of(By.id("src")).dragTo(Locator.of(By.id("dst")));

verify((Interactive) driver).perform(any());
verify(js).executeScript(contains("dragstart"), eq(src), eq(dst));
}

@Test
public void dragTo_targetNeverResolves_throwsLocatorException() {
when(driver.findElements(By.id("missing"))).thenReturn(List.of());

assertThrows(LocatorException.class,
() -> Locator.of(By.id("src")).dragTo(Locator.of(By.id("missing"))));
verify((Interactive) driver, never()).perform(any());
}

@Test
public void dragTo_sourceNeverResolves_throwsLocatorException() {
when(driver.findElements(By.id("missing"))).thenReturn(List.of());

assertThrows(LocatorException.class,
() -> Locator.of(By.id("missing")).dragTo(Locator.of(By.id("dst"))));
verify((Interactive) driver, never()).perform(any());
}
}
Loading