Skip to content

Commit

Permalink
[plugin-web-app] Unify syntax of steps performing drag-n-drop action (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
TykovkaV authored Dec 11, 2023
1 parent 8e1f143 commit 4b1b72a
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 26 deletions.
42 changes: 42 additions & 0 deletions docs/modules/plugins/pages/plugin-web-app.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,48 @@ When I select element located `$locator` and upload file `$filePath`
When I select element located by `id(uploadfile)` and upload file `/folder/file_for_upload.png`
----

=== Drag and drop steps

This set of steps facilitates the dragging and dropping of elements on a web page.

==== Drag and drop using mouse

Performs drag-and-drop operation for the specified elements.

[source,gherkin]
----
When I drag element located by `$draggable` and drop it at $location of element located by `$target`
----

* `$draggable` - The <<_locator,locator>> to identify the element to be dragged.
* `$location` - The target position relative to the target element (TOP, BOTTOM, LEFT, RIGHT, CENTER, LEFT_TOP, RIGHT_TOP, LEFT_BOTTOM, RIGHT_BOTTOM).
* `$target` - The <<_locator,locator>> to identify the target element.

.Dragging the image on the page
[source,gherkin]
----
When I drag element located by `id(item-number)` and drop it at right bottom of element located by `id(item-number)`
----

==== Drag and drop simulation

Simulates a drag-and-drop operation for the specified elements,
replicating the behavior without actual mouse actions performed.

[source,gherkin]
----
When I simulate drag of element located by `$draggable` and drop at element located by `$target`
----

* `$draggable` - The <<_locator,locator>> to identify the element to be dragged.
* `$target` - The <<_locator,locator>> to identify the target element.

.Simulation of dragging an image onto the page
[source,gherkin]
----
When I simulate drag of element located by `id(item-number)` and drop at element located by `id(item-number)`
----

=== Check each element has a specific number of children elements

Checks that each element specified by the locator contains an exact number of visible child elements specified by another locator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
package org.vividus.steps.ui.web;

import java.time.Duration;
import java.util.Optional;
import java.util.function.BiConsumer;

import org.apache.commons.lang3.ObjectUtils;
import org.jbehave.core.annotations.When;
import org.openqa.selenium.Point;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.vividus.annotation.Replacement;
import org.vividus.selenium.IWebDriverProvider;
import org.vividus.selenium.locator.Locator;
import org.vividus.steps.ui.validation.IBaseValidations;
Expand All @@ -34,6 +36,11 @@
@TakeScreenshotOnFailure
public class DragAndDropSteps
{
private static final int RIGHT_OFFSET = 10;
private static final int LEFT_OFFSET = -10;
private static final String DRAGGABLE_ELEMENT = "Draggable element";
private static final String TARGET_ELEMENT = "Target element";
private static final String SIMULATE_DRAG_AND_DROP_JS = "simulate-drag-and-drop.js";
private final IWebDriverProvider webDriverProvider;
private final WebJavascriptActions javascriptActions;
private final IBaseValidations baseValidations;
Expand All @@ -59,9 +66,47 @@ public DragAndDropSteps(IWebDriverProvider webDriverProvider, WebJavascriptActio
* @param location location relatively to the <b>target</b> element (<b>TOP</b>,<b>BOTTOM</b>,<b>LEFT</b>,
* <b>RIGHT</b>,<b>CENTER</b>,<b>LEFT_TOP</b>,<b>RIGHT_TOP</b>,<b>LEFT_BOTTOM</b>,<b>RIGHT_BOTTOM</b>)
* @param target target element
* @deprecated Use step:
* "When I drag element located by `$draggable` and drop it at $location of element located by `$target`" instead
*/
@Deprecated(since = "0.6.5", forRemoval = true)
@Replacement(versionToRemoveStep = "0.7.0", replacementFormatPattern =
"When I drag element located by `%1$s` and drop it at %2$s of element located by `%3$s`")
@When("I drag element located `$draggable` and drop it at $location of element located `$target`")
@SuppressWarnings("checkstyle:MagicNumber")
public void dragAndDropToTargetAtLocationDeprecated(Locator draggable, Location location, Locator target)
{
performDragAndDropDeprecated(draggable, target, (draggableElement, targetElement) ->
{
Point offsetPoint = location.getPoint(draggableElement.getRect(), targetElement.getRect());
new Actions(webDriverProvider.get())
.clickAndHold(draggableElement)
// Selenium bug: https://github.com/SeleniumHQ/selenium/issues/1365#issuecomment-547786925
.moveByOffset(RIGHT_OFFSET, 0)
.moveByOffset(LEFT_OFFSET, 0)
.moveByOffset(offsetPoint.getX(), offsetPoint.getY())
.release()
// Wait for DOM stabilization
.pause(Duration.ofSeconds(1))
.perform();
});
}

/**
* Drags the <b>draggable</b> element and moves it relatively to the <b>target</b> element in
* accordance to provided <b>location</b>.
* <br>
* <i>Example</i>
* <br>
* <code>When I drag element located by `By.xpath(//div[@class='draggable'])` and drop it at RIGHT_TOP of element
* located by `By.xpath(//div[@class='target'])`</code>
* If this step doesn't work, try to use step simulating drag&amp;drop
* @param draggable draggable element
* @param location location relatively to the <b>target</b> element (<b>TOP</b>,<b>BOTTOM</b>,<b>LEFT</b>,
* <b>RIGHT</b>,<b>CENTER</b>,<b>LEFT_TOP</b>,<b>RIGHT_TOP</b>,<b>LEFT_BOTTOM</b>,<b>RIGHT_BOTTOM</b>)
* @param target target element
*/
@When("I drag element located by `$draggable` and drop it at $location of element located by `$target`")
public void dragAndDropToTargetAtLocation(Locator draggable, Location location, Locator target)
{
performDragAndDrop(draggable, target, (draggableElement, targetElement) ->
Expand All @@ -70,8 +115,8 @@ public void dragAndDropToTargetAtLocation(Locator draggable, Location location,
new Actions(webDriverProvider.get())
.clickAndHold(draggableElement)
// Selenium bug: https://github.com/SeleniumHQ/selenium/issues/1365#issuecomment-547786925
.moveByOffset(10, 0)
.moveByOffset(-10, 0)
.moveByOffset(RIGHT_OFFSET, 0)
.moveByOffset(LEFT_OFFSET, 0)
.moveByOffset(offsetPoint.getX(), offsetPoint.getY())
.release()
// Wait for DOM stabilization
Expand Down Expand Up @@ -106,25 +151,79 @@ public void dragAndDropToTargetAtLocation(Locator draggable, Location location,
* </p>
* @param draggable draggable element
* @param target target element
* @deprecated Use step:
* "When I simulate drag of element located by `$draggable` and drop at element located by `$target`" instead
*/
@Deprecated(since = "0.6.5", forRemoval = true)
@Replacement(versionToRemoveStep = "0.7.0", replacementFormatPattern =
"When I simulate drag of element located by `%1$s` and drop at element located by `%2$s`")
@When("I simulate drag of element located `$draggable` and drop at element located `$target`")
public void simulateDragAndDropDeprecated(Locator draggable, Locator target)
{
performDragAndDropDeprecated(draggable, target, (draggableElement, targetElement) ->
// See gist for details: https://gist.github.com/valfirst/7f36c8755676cdf8943a8a8f08eab2e3
javascriptActions.executeScriptFromResource(getClass(),
SIMULATE_DRAG_AND_DROP_JS, draggableElement,
targetElement));
}

/**
* Simulates drag of the <b>draggable</b> element and its drop at the <b>target</b> element via JavaScript.
* <br>
* <i>Example</i>
* <br>
* <code>When I simulate drag of element located `By.xpath(//div[@class='draggable'])` and drop at element located
* `By.xpath(//div[@class='target'])`</code>
* <p>
* The reason of having this step is that Selenium WebDriver doesn't support HTML5 drag&amp;drop:
* </p>
* <ul>
* <li><a href="https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/3604">
* Issue 3604: HTML5 Drag and Drop with Selenium WebDriver
* </a>
* </li>
* <li><a href="https://github.com/SeleniumHQ/selenium/issues/1365">
* Issue 1365: Actions drag and drop method
* </a>
* </li>
* </ul>
* <p>
* As workaround for these issue the step simulates HTML5 drag&amp;drop via JavaScript. There is no difference in
* actual drag&amp;drop and its simulation via JavaScript from the functional side.
* </p>
* @param draggable draggable element
* @param target target element
*/
@When("I simulate drag of element located by `$draggable` and drop at element located by `$target`")
public void simulateDragAndDrop(Locator draggable, Locator target)
{
performDragAndDrop(draggable, target, (draggableElement, targetElement) ->
// See gist for details: https://gist.github.com/valfirst/7f36c8755676cdf8943a8a8f08eab2e3
javascriptActions.executeScriptFromResource(getClass(), "simulate-drag-and-drop.js", draggableElement,
javascriptActions.executeScriptFromResource(getClass(), SIMULATE_DRAG_AND_DROP_JS, draggableElement,
targetElement));
}

private void performDragAndDrop(Locator draggable, Locator target,
private void performDragAndDropDeprecated(Locator draggable, Locator target,
BiConsumer<WebElement, WebElement> dragAndDropExecutor)
{
WebElement draggableElement = baseValidations.assertIfElementExists("Draggable element", draggable);
WebElement targetElement = baseValidations.assertIfElementExists("Target element", target);
WebElement draggableElement = baseValidations.assertIfElementExists(DRAGGABLE_ELEMENT, draggable);
WebElement targetElement = baseValidations.assertIfElementExists(TARGET_ELEMENT, target);

if (ObjectUtils.allNotNull(draggableElement, targetElement))
{
dragAndDropExecutor.accept(draggableElement, targetElement);
}
}

private void performDragAndDrop(Locator draggable, Locator target,
BiConsumer<WebElement, WebElement> dragAndDropExecutor)
{
Optional<WebElement> draggableElement = baseValidations.assertElementExists(DRAGGABLE_ELEMENT, draggable);
Optional<WebElement> targetElement = baseValidations.assertElementExists(TARGET_ELEMENT, target);

if (draggableElement.isPresent() && targetElement.isPresent())
{
dragAndDropExecutor.accept(draggableElement.get(), targetElement.get());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

import java.util.Optional;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
Expand All @@ -47,6 +48,19 @@
@ExtendWith(MockitoExtension.class)
class DragAndDropStepsTests
{
private static final String DRAGGABLE_ELEMENT = "Draggable element";
private static final String TARGET_ELEMENT = "Target element";
private static final String SIMULATE_DRAG_AND_DROP_JS = "simulate-drag-and-drop.js";
private static final String DRAG_AND_DROP_ACTIONS = "{id=default mouse, "
+ "type=pointer, parameters={pointerType=mouse}, "
+ "actions=["
+ "{duration=100, x=0, y=0, type=pointerMove, origin=Mock for WebElement, hashCode: %s}, "
+ "{button=0, type=pointerDown}, "
+ "{duration=200, x=10, y=0, type=pointerMove, origin=pointer}, "
+ "{duration=200, x=-10, y=0, type=pointerMove, origin=pointer}, "
+ "{duration=200, x=100, y=50, type=pointerMove, origin=pointer}, "
+ "{button=0, type=pointerUp}, "
+ "{duration=1000, type=pause}]}";
@Mock
private IWebDriverProvider webDriverProvider;

Expand All @@ -60,23 +74,15 @@ class DragAndDropStepsTests
private DragAndDropSteps dragAndDropSteps;

@Test
void shouldDragAndDropToTargetAtLocation()
void shouldDragAndDropToTargetAtLocationDeprecated()
{
WebElement draggable = mockWebElementWithRect(100, 100, 50, 50);
WebElement target = mockWebElementWithRect(200, 200, 50, 50);
WebDriver driver = mock(WebDriver.class, withSettings().extraInterfaces(Interactive.class));
when(webDriverProvider.get()).thenReturn(driver);
testDragAndDropToTargetAtLocation(draggable, Location.TOP, target);
testDragAndDropToTargetAtLocationDeprecated(draggable, Location.TOP, target);
String actionsSequence = String.format(
"{id=default mouse, type=pointer, parameters={pointerType=mouse}, "
+ "actions=["
+ "{duration=100, x=0, y=0, type=pointerMove, origin=Mock for WebElement, hashCode: %s}, "
+ "{button=0, type=pointerDown}, "
+ "{duration=200, x=10, y=0, type=pointerMove, origin=pointer}, "
+ "{duration=200, x=-10, y=0, type=pointerMove, origin=pointer}, "
+ "{duration=200, x=100, y=50, type=pointerMove, origin=pointer}, "
+ "{button=0, type=pointerUp}, "
+ "{duration=1000, type=pause}]}", draggable.hashCode());
DRAG_AND_DROP_ACTIONS, draggable.hashCode());
verify((Interactive) driver).perform(
argThat(arg -> arg.iterator().next().encode().toString().equals(actionsSequence)));
}
Expand All @@ -97,6 +103,35 @@ static Stream<Arguments> getElements()
);
}

@Test
void shouldDragAndDropToTargetAtLocation()
{
WebElement draggable = mockWebElementWithRect(100, 100, 50, 50);
WebElement target = mockWebElementWithRect(200, 200, 50, 50);
WebDriver driver = mock(WebDriver.class, withSettings().extraInterfaces(Interactive.class));
when(webDriverProvider.get()).thenReturn(driver);
testDragAndDropToTargetAtLocation(draggable, Location.TOP, target);
String actionsSequence = String.format(
DRAG_AND_DROP_ACTIONS, draggable.hashCode());
verify((Interactive) driver).perform(
argThat(arg -> arg.iterator().next().encode().toString().equals(actionsSequence)));
}

@MethodSource("getElements")
@ParameterizedTest
void shouldNotDragAndDropWhenAnyElementIsNotFoundDeprecated(WebElement draggable, WebElement target)
{
testDragAndDropToTargetAtLocationDeprecated(draggable, Location.TOP, target);
verifyNoInteractions(webDriverProvider);
}

private void testDragAndDropToTargetAtLocationDeprecated(WebElement draggable, Location location, WebElement target)
{
Locator draggableLocator = mockDraggableElementSearchDeprecated(draggable);
Locator targetLocator = mockTargetElementSearchDeprecated(target);
dragAndDropSteps.dragAndDropToTargetAtLocationDeprecated(draggableLocator, location, targetLocator);
}

@MethodSource("getElements")
@ParameterizedTest
void shouldNotDragAndDropWhenAnyElementIsNotFound(WebElement draggable, WebElement target)
Expand All @@ -112,33 +147,63 @@ private void testDragAndDropToTargetAtLocation(WebElement draggable, Location lo
dragAndDropSteps.dragAndDropToTargetAtLocation(draggableLocator, location, targetLocator);
}

@Test
void shouldSimulateDragAndDropDeprecated()
{
WebElement draggable = mock();
WebElement target = mock();
Locator draggableLocator = mockDraggableElementSearchDeprecated(draggable);
Locator targetLocator = mockTargetElementSearchDeprecated(target);
dragAndDropSteps.simulateDragAndDropDeprecated(draggableLocator, targetLocator);
verify(javascriptActions)
.executeScriptFromResource(DragAndDropSteps.class, SIMULATE_DRAG_AND_DROP_JS, draggable, target);
}

@Test
void shouldSimulateDragAndDrop()
{
WebElement draggable = mock(WebElement.class);
WebElement target = mock(WebElement.class);
WebElement draggable = mock();
WebElement target = mock();
Locator draggableLocator = mockDraggableElementSearch(draggable);
Locator targetLocator = mockTargetElementSearch(target);
dragAndDropSteps.simulateDragAndDrop(draggableLocator, targetLocator);
verify(javascriptActions).executeScriptFromResource(DragAndDropSteps.class, "simulate-drag-and-drop.js",
verify(javascriptActions).executeScriptFromResource(DragAndDropSteps.class, SIMULATE_DRAG_AND_DROP_JS,
draggable, target);
}

private Locator mockDraggableElementSearchDeprecated(WebElement draggable)
{
return mockElementSearchDeprecated(draggable, DRAGGABLE_ELEMENT);
}

private Locator mockTargetElementSearchDeprecated(WebElement target)
{
return mockElementSearchDeprecated(target, TARGET_ELEMENT);
}

private Locator mockDraggableElementSearch(WebElement draggable)
{
return mockElementSearch(draggable, "Draggable element");
return mockElementSearch(draggable, DRAGGABLE_ELEMENT);
}

private Locator mockTargetElementSearch(WebElement target)
{
return mockElementSearch(target, "Target element");
return mockElementSearch(target, TARGET_ELEMENT);
}

private Locator mockElementSearch(WebElement element, String assertionDescription)
private Locator mockElementSearchDeprecated(WebElement element, String assertionDescription)
{
Locator locator = mock(Locator.class);
lenient().when(baseValidations.assertIfElementExists(assertionDescription, locator)).thenReturn(
element);
return locator;
}

private Locator mockElementSearch(WebElement element, String assertionDescription)
{
Locator locator = mock(Locator.class);
lenient().when(baseValidations.assertElementExists(assertionDescription, locator))
.thenReturn(Optional.ofNullable(element));
return locator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Scenario: Step verification 'When I drag element located `$origin` and drop it a
Given I am on page with URL `https://4qp6vjp319.codesandbox.io/`
When I wait until element located by `xpath(//div[@id='root']/ul)` appears
When I change context to element located by `xpath(//div[@id='root']/ul)`
Then text matches `item 0.*item 1.*item 2.*item 3.*`
When I drag element located `By.xpath(//li[contains(., 'item 0')])` and drop it at top of element located `By.xpath(//li[contains(., 'item 3')])`
Then text matches `item 1.*item 2.*item 0.*item 3.*`
Then text matches `item 0.*item 1.*item 2.*item 3.*item 4.*item 5.*item 6.*`
When I drag element located by `By.xpath(//li[contains(., 'item 0')])` and drop it at top of element located by `By.xpath(//li[contains(., 'item 3')])`
Then text matches `item 1.*item 2.*item 0.*item 3.*item 4.*item 5.*item 6.*`
When I wait until element located by `xpath(//div[@id='root']/ul)` appears
When I drag element located `By.xpath(//li[contains(., 'item 2')])` and drop it at top of element located `By.xpath(//li[contains(., 'item 5')])`
Then text matches `item 1.*item 0.*item 3.*item 4.*item 2.*item 5.*item 6.*`

0 comments on commit 4b1b72a

Please sign in to comment.