diff --git a/docs/modules/plugins/pages/plugin-web-app.adoc b/docs/modules/plugins/pages/plugin-web-app.adoc index 0d275bb46e..d189fa83ff 100644 --- a/docs/modules/plugins/pages/plugin-web-app.adoc +++ b/docs/modules/plugins/pages/plugin-web-app.adoc @@ -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 diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/steps/ui/web/DragAndDropSteps.java b/vividus-plugin-web-app/src/main/java/org/vividus/steps/ui/web/DragAndDropSteps.java index 5b26c74064..af42a46de7 100644 --- a/vividus-plugin-web-app/src/main/java/org/vividus/steps/ui/web/DragAndDropSteps.java +++ b/vividus-plugin-web-app/src/main/java/org/vividus/steps/ui/web/DragAndDropSteps.java @@ -17,6 +17,7 @@ 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; @@ -24,6 +25,7 @@ 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; @@ -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; @@ -59,9 +66,47 @@ public DragAndDropSteps(IWebDriverProvider webDriverProvider, WebJavascriptActio * @param location location relatively to the target element (TOP,BOTTOM,LEFT, * RIGHT,CENTER,LEFT_TOP,RIGHT_TOP,LEFT_BOTTOM,RIGHT_BOTTOM) * @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 draggable element and moves it relatively to the target element in + * accordance to provided location. + *
+ * Example + *
+ * 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'])` + * If this step doesn't work, try to use step simulating drag&drop + * @param draggable draggable element + * @param location location relatively to the target element (TOP,BOTTOM,LEFT, + * RIGHT,CENTER,LEFT_TOP,RIGHT_TOP,LEFT_BOTTOM,RIGHT_BOTTOM) + * @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) -> @@ -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 @@ -106,25 +151,79 @@ public void dragAndDropToTargetAtLocation(Locator draggable, Location location, *

* @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 draggable element and its drop at the target element via JavaScript. + *
+ * Example + *
+ * When I simulate drag of element located `By.xpath(//div[@class='draggable'])` and drop at element located + * `By.xpath(//div[@class='target'])` + *

+ * The reason of having this step is that Selenium WebDriver doesn't support HTML5 drag&drop: + *

+ * + *

+ * As workaround for these issue the step simulates HTML5 drag&drop via JavaScript. There is no difference in + * actual drag&drop and its simulation via JavaScript from the functional side. + *

+ * @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 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 dragAndDropExecutor) + { + Optional draggableElement = baseValidations.assertElementExists(DRAGGABLE_ELEMENT, draggable); + Optional targetElement = baseValidations.assertElementExists(TARGET_ELEMENT, target); + + if (draggableElement.isPresent() && targetElement.isPresent()) + { + dragAndDropExecutor.accept(draggableElement.get(), targetElement.get()); + } + } } diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/steps/ui/web/DragAndDropStepsTests.java b/vividus-plugin-web-app/src/test/java/org/vividus/steps/ui/web/DragAndDropStepsTests.java index 098e12f184..f63fb1f8a7 100644 --- a/vividus-plugin-web-app/src/test/java/org/vividus/steps/ui/web/DragAndDropStepsTests.java +++ b/vividus-plugin-web-app/src/test/java/org/vividus/steps/ui/web/DragAndDropStepsTests.java @@ -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; @@ -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; @@ -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))); } @@ -97,6 +103,35 @@ static Stream 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) @@ -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; + } } diff --git a/vividus-tests/src/main/resources/story/integration/DragAndDropSteps.story b/vividus-tests/src/main/resources/story/integration/DragAndDropSteps.story index 5bb87095b8..ac38e52c9c 100644 --- a/vividus-tests/src/main/resources/story/integration/DragAndDropSteps.story +++ b/vividus-tests/src/main/resources/story/integration/DragAndDropSteps.story @@ -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.*`