All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.shaft.gui.element.TouchActions Maven / Gradle / Ivy

Go to download

SHAFT is a unified test automation engine. Powered by best-in-class frameworks, SHAFT provides a wizard-like syntax to drive your automation efficiently, maximize your ROI, and minimize your learning curve. Stop reinventing the wheel. Upgrade now!

There is a newer version: 8.2.20240402
Show newest version
package com.shaft.gui.element;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.shaft.driver.SHAFT;
import com.shaft.driver.internal.DriverFactoryHelper;
import com.shaft.driver.internal.WizardHelpers;
import com.shaft.gui.element.internal.ElementActionsHelper;
import com.shaft.gui.element.internal.FluentElementActions;
import com.shaft.gui.internal.image.ScreenshotManager;
import com.shaft.tools.io.ReportManager;
import com.shaft.tools.io.internal.ReportManagerHelper;
import com.shaft.validation.internal.WebDriverElementValidationsBuilder;
import io.appium.java_client.AppiumBy;
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.ios.IOSDriver;
import org.openqa.selenium.*;
import org.openqa.selenium.interactions.*;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import static com.shaft.gui.element.internal.ElementActionsHelper.formatLocatorToString;
import static java.util.Arrays.asList;

@SuppressWarnings({"unused"})
public class TouchActions {
    private static final int DEFAULT_NUMBER_OF_ATTEMPTS_TO_SCROLL_TO_ELEMENT = 5;
    private static final boolean CAPTURE_CLICKED_ELEMENT_TEXT = SHAFT.Properties.reporting.captureElementName();

    public TouchActions(WebDriver driver) {
        new TouchActions();
    }

    public TouchActions() {
    }

    public static TouchActions getInstance() {
        return new TouchActions();
    }

    /**
     * This is a convenience method to be able to call Element Actions from within the current Touch Actions instance.
     * 

* Sample use would look like this: * new TouchActions(driver).tap(username_textbox).performElementAction().type(username_textbox, "username"); * * @return a FluentElementActions object */ public FluentElementActions performElementAction() { return FluentElementActions.getInstance(); } public FluentElementActions element() { return FluentElementActions.getInstance(); } public TouchActions and() { return this; } public WebDriverElementValidationsBuilder assertThat(By elementLocator) { return new WizardHelpers.WebDriverAssertions().element(elementLocator); } public WebDriverElementValidationsBuilder verifyThat(By elementLocator) { return new WizardHelpers.WebDriverVerifications().element(elementLocator); } /** * Sends a key-press via the device soft keyboard. * * @param key the key that should be pressed * @return a self-reference to be used to chain actions */ public TouchActions nativeKeyboardKeyPress(KeyboardKeys key) { try { ((RemoteWebDriver) DriverFactoryHelper.getDriver().get()).executeScript("mobile: performEditorAction", key.getValue()); ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), key.name(), null, null); } catch (Exception rootCauseException) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null, rootCauseException); } return this; } /** * Hides the device native soft keyboard. * * @return a self-reference to be used to chain actions */ public TouchActions hideNativeKeyboard() { try { if (DriverFactoryHelper.getDriver().get() instanceof AndroidDriver androidDriver) { androidDriver.hideKeyboard(); } else if (DriverFactoryHelper.getDriver().get() instanceof IOSDriver iosDriver) { iosDriver.hideKeyboard(); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null); } } catch (Exception rootCauseException) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null, rootCauseException); } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), null, null, null); return this; } /** * Taps an element once on a touch-enabled screen * * @param elementReferenceScreenshot relative path to the reference image from the local object repository * @return a self-reference to be used to chain actions */ @SuppressWarnings("unchecked") public TouchActions tap(String elementReferenceScreenshot) { // Wait for element presence and get the needed data var objects = ElementActionsHelper.waitForElementPresence(DriverFactoryHelper.getDriver().get(), elementReferenceScreenshot); byte[] currentScreenImage = (byte[]) objects.get(0); byte[] referenceImage = (byte[]) objects.get(1); List coordinates = (List) objects.get(2); // Prepare screenshots for reporting var screenshot = ScreenshotManager.prepareImageForReport(currentScreenImage, "tap - Current Screen Image"); var referenceScreenshot = ScreenshotManager.prepareImageForReport(referenceImage, "tap - Reference Screenshot"); List> attachments = new LinkedList<>(); attachments.add(referenceScreenshot); attachments.add(screenshot); // If coordinates are empty then OpenCV couldn't find the element on screen if (Collections.emptyList().equals(coordinates)) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), "Couldn't find reference element on the current screen. If you can see it in the attached image then kindly consider cropping it and updating your reference image under this path \"" + elementReferenceScreenshot + "\".", null, attachments); } else { // Perform tap action by coordinates // if (DriverFactoryHelper.isMobileNativeExecution()) { PointerInput input = new PointerInput(PointerInput.Kind.TOUCH, "finger1"); Sequence tap = new Sequence(input, 0); tap.addAction(input.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), coordinates.get(0), coordinates.get(1))); tap.addAction(input.createPointerDown(PointerInput.MouseButton.LEFT.asArg())); tap.addAction(new Pause(input, Duration.ofMillis(200))); tap.addAction(input.createPointerUp(PointerInput.MouseButton.LEFT.asArg())); try { ((RemoteWebDriver) DriverFactoryHelper.getDriver().get()).perform(ImmutableList.of(tap)); } catch (UnsupportedCommandException exception) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null, exception); } // } else { // (new org.openqa.selenium.interactions.touch.TouchActions(DriverFactoryHelper.getDriver().get())) // .down(coordinates.get(0), coordinates.get(1)) // .up(coordinates.get(0), coordinates.get(1)) // .perform(); // } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), null, attachments, null); } return this; } /** * Taps an element once on a touch-enabled screen * * @param elementLocator the locator of the webElement under test (By xpath, id, * selector, name ...etc) * @return a self-reference to be used to chain actions */ public TouchActions tap(By elementLocator) { try{ String elementText = ""; if (CAPTURE_CLICKED_ELEMENT_TEXT) { try { if (DriverFactoryHelper.isMobileNativeExecution()) { elementText = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1)).getAttribute("text"); } else { elementText = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1)).getText(); } } catch (Exception e) { // do nothing } } List screenshot = ElementActionsHelper.takeScreenshot(DriverFactoryHelper.getDriver().get(), elementLocator, "tap", null, true); // takes screenshot before clicking the element out of view try { // fixing https://github.com/ShaftHQ/SHAFT_ENGINE/issues/501 ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1)).click(); } catch (Exception e) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, e); } if (elementText == null || elementText.equals("")) { elementText = formatLocatorToString(elementLocator); } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), elementLocator, elementText.replaceAll("\n", " "), screenshot, null); } catch (Throwable throwable) { // has to be throwable to catch assertion errors in case element was not found ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, throwable); } return this; } /** * Double-Taps an element on a touch-enabled screen * * @param elementLocator the locator of the webElement under test (By xpath, id, * selector, name ...etc) * @return a self-reference to be used to chain actions */ public TouchActions doubleTap(By elementLocator) { try { String elementText = ""; try { elementText = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1)).getText(); } catch (Exception e) { // do nothing } // takes screenshot before clicking the element out of view var screenshot = ElementActionsHelper.takeScreenshot(DriverFactoryHelper.getDriver().get(), elementLocator, "doubleTap", null, true); List> attachments = new LinkedList<>(); attachments.add(screenshot); try { (new Actions(DriverFactoryHelper.getDriver().get())).doubleClick(((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1))).perform(); } catch (Exception e) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, e); } if (elementText != null && !elementText.equals("")) { ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), elementLocator, elementText.replaceAll("\n", " "), screenshot, null); } else { ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), elementLocator, Thread.currentThread().getStackTrace()[1].getMethodName(), null, attachments, null); } } catch (Throwable throwable) { // has to be throwable to catch assertion errors in case element was not found ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, throwable); } return this; } /** * Performs a long-tap on an element to trigger the context menu on a * touch-enabled screen * * @param elementLocator the locator of the webElement under test (By xpath, id, * selector, name ...etc) * @return a self-reference to be used to chain actions */ public TouchActions longTap(By elementLocator) { try { String elementText = ""; try { elementText = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1)).getText(); } catch (Exception e) { // do nothing } // takes screenshot before clicking the element out of view List screenshot = ElementActionsHelper.takeScreenshot(DriverFactoryHelper.getDriver().get(), elementLocator, "longPress", null, true); List> attachments = new LinkedList<>(); attachments.add(screenshot); try { new Actions(DriverFactoryHelper.getDriver().get()).clickAndHold(((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1))).perform(); } catch (Exception e) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, e); } if (elementText != null && !elementText.equals("")) { ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), elementLocator, elementText.replaceAll("\n", " "), screenshot, null); } else { ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), elementLocator, Thread.currentThread().getStackTrace()[1].getMethodName(), null, attachments, null); } } catch (Throwable throwable) { // has to be throwable to catch assertion errors in case element was not found ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, throwable); } return this; } /** * Send the currently active app to the background, and return after a certain number of seconds. * * @param secondsToSpendInTheBackground number of seconds before returning to the app * @return a self-reference to be used to chain actions */ public TouchActions sendAppToBackground(int secondsToSpendInTheBackground) { if (DriverFactoryHelper.isMobileNativeExecution()) { if (DriverFactoryHelper.getDriver().get() instanceof AndroidDriver androidDriver) { androidDriver.runAppInBackground(Duration.ofSeconds(secondsToSpendInTheBackground)); } else if (DriverFactoryHelper.getDriver().get() instanceof IOSDriver iosDriver) { iosDriver.runAppInBackground(Duration.ofSeconds(secondsToSpendInTheBackground)); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null); } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), null, null, null); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null); } return this; } /** * Send the currently active app to the background and leave the app deactivated. * * @return a self-reference to be used to chain actions */ public TouchActions sendAppToBackground() { return sendAppToBackground(-1); } /** * Activates an app that has been previously deactivated or sent to the background. * * @param appPackageName the full name for the app package that you want to activate. for example [com.apple.Preferences] or [io.appium.android.apis] * @return a self-reference to be used to chain actions */ public TouchActions activateAppFromBackground(String appPackageName) { if (DriverFactoryHelper.isMobileNativeExecution()) { if (DriverFactoryHelper.getDriver().get() instanceof AndroidDriver androidDriver) { androidDriver.activateApp(appPackageName); } else if (DriverFactoryHelper.getDriver().get() instanceof IOSDriver iosDriver) { iosDriver.activateApp(appPackageName); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null); } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), null, null, null); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null); } return this; } /** * Swipes the sourceElement onto the destinationElement on a touch-enabled * screen * * @param sourceElementLocator the locator of the webElement that needs to * be swiped (By xpath, id, selector, name * ...etc) * @param destinationElementLocator the locator of the webElement that you'll * drop the sourceElement on (By xpath, id, * selector, name ...etc) * @return a self-reference to be used to chain actions */ public TouchActions swipeToElement(By sourceElementLocator, By destinationElementLocator) { try { WebElement sourceElement = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), sourceElementLocator).get(1)); WebElement destinationElement = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), destinationElementLocator).get(1)); String startLocation = sourceElement.getLocation().toString(); try { new Actions(DriverFactoryHelper.getDriver().get()).dragAndDrop(sourceElement, destinationElement).perform(); } catch (Exception e) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), sourceElementLocator, e); } String endLocation = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), sourceElementLocator).get(1)).getLocation().toString(); String reportMessage = "Start point: " + startLocation + ", End point: " + endLocation; if (!endLocation.equals(startLocation)) { ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), sourceElementLocator, Thread.currentThread().getStackTrace()[1].getMethodName(), reportMessage, null, null); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), reportMessage, sourceElementLocator); } } catch (Throwable throwable) { // has to be throwable to catch assertion errors in case element was not found ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), sourceElementLocator, throwable); } return this; } /** * Swipes an element with the desired x and y offset. Swiping direction is * determined by the positive/negative nature of the offset. Swiping destination * is determined by the value of the offset. * * @param elementLocator the locator of the webElement under test (By xpath, id, * selector, name ...etc) * @param xOffset the horizontal offset by which the element should be * swiped. positive value is "right" and negative value is * "left" * @param yOffset the vertical offset by which the element should be * swiped. positive value is "down" and negative value is * "up" * @return a self-reference to be used to chain actions */ public TouchActions swipeByOffset(By elementLocator, int xOffset, int yOffset) { try { WebElement sourceElement = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1)); Point elementLocation = sourceElement.getLocation(); String startLocation = elementLocation.toString(); try { new Actions(DriverFactoryHelper.getDriver().get()).dragAndDropBy(sourceElement, xOffset, yOffset).perform(); } catch (Exception e) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, e); } String endLocation = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), elementLocator).get(1)).getLocation().toString(); String reportMessage = "Start point: " + startLocation + ", End point: " + endLocation; if (!endLocation.equals(startLocation)) { ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), elementLocator, Thread.currentThread().getStackTrace()[1].getMethodName(), reportMessage, null, null); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), reportMessage, elementLocator); } } catch (Throwable throwable) { // has to be throwable to catch assertion errors in case element was not found ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), elementLocator, throwable); } return this; } /** * Attempts to scroll the element into view in case of native mobile elements. * * @param targetElementLocator the locator of the webElement under test (By xpath, id, * selector, name ...etc) * @param swipeDirection SwipeDirection.DOWN, UP, RIGHT, or LEFT * @return a self-reference to be used to chain actions */ public TouchActions swipeElementIntoView(By targetElementLocator, SwipeDirection swipeDirection) { return swipeElementIntoView(null, targetElementLocator, swipeDirection); } //TODO: swipeToEndOfView(SwipeDirection swipeDirection) //TODO: waitUntilElementIsNotVisible(String elementReferenceScreenshot) /** * Waits until a specific element is now visible on the current screen * * @param elementReferenceScreenshot relative path to the reference image from the local object repository * @return a self-reference to be used to chain actions */ @SuppressWarnings("unchecked") public TouchActions waitUntilElementIsVisible(String elementReferenceScreenshot) { var visualIdentificationObjects = ElementActionsHelper.waitForElementPresence(DriverFactoryHelper.getDriver().get(), elementReferenceScreenshot); byte[] currentScreenImage = (byte[]) visualIdentificationObjects.get(0); byte[] referenceImage = (byte[]) visualIdentificationObjects.get(1); List coordinates = (List) visualIdentificationObjects.get(2); // prepare attachments var screenshot = ScreenshotManager.prepareImageForReport(currentScreenImage, "waitUntilElementIsVisible - Current Screen Image"); var referenceScreenshot = ScreenshotManager.prepareImageForReport(referenceImage, "waitUntilElementIsVisible - Reference Screenshot"); List> attachments = new LinkedList<>(); attachments.add(referenceScreenshot); attachments.add(screenshot); if (!Collections.emptyList().equals(coordinates)) { ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), null, attachments, null); } else { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), "Couldn't find reference element on the current screen. If you can see it in the attached image then kindly consider cropping it and updating your reference image under this path \"" + elementReferenceScreenshot + "\".", null, attachments); } return this; } /** * Attempts to scroll element into view using the new W3C compliant actions for android and ios and AI for image identification * * @param elementReferenceScreenshot relative path to the reference image from the local object repository * @param swipeDirection SwipeDirection.DOWN, UP, RIGHT, or LEFT * @return a self-reference to be used to chain actions */ public TouchActions swipeElementIntoView(String elementReferenceScreenshot, SwipeDirection swipeDirection) { return swipeElementIntoView(null, elementReferenceScreenshot, swipeDirection); } /** * Attempts to scroll element into view using the new W3C compliant actions for android and ios and AI for image identification * * @param scrollableElementLocator the locator of the container/view/scrollable webElement that the scroll action will be performed inside * @param elementReferenceScreenshot relative path to the reference image from the local object repository * @param swipeDirection SwipeDirection.DOWN, UP, RIGHT, or LEFT * @return a self-reference to be used to chain actions */ @SuppressWarnings("unchecked") public TouchActions swipeElementIntoView(By scrollableElementLocator, String elementReferenceScreenshot, SwipeDirection swipeDirection) { // Prepare attachments for reporting List> attachments = new LinkedList<>(); try { //noinspection CaughtExceptionImmediatelyRethrown try { if (DriverFactoryHelper.getDriver().get() instanceof AppiumDriver appiumDriver) { // appium native application var visualIdentificationObjects = attemptToSwipeElementIntoViewInNativeApp(scrollableElementLocator, elementReferenceScreenshot, swipeDirection); byte[] currentScreenImage = (byte[]) visualIdentificationObjects.get(0); byte[] referenceImage = (byte[]) visualIdentificationObjects.get(1); List coordinates = (List) visualIdentificationObjects.get(2); // prepare attachments var screenshot = ScreenshotManager.prepareImageForReport(currentScreenImage, "swipeElementIntoView - Current Screen Image"); var referenceScreenshot = ScreenshotManager.prepareImageForReport(referenceImage, "swipeElementIntoView - Reference Screenshot"); attachments = new LinkedList<>(); attachments.add(referenceScreenshot); attachments.add(screenshot); // If coordinates are empty then OpenCV couldn't find the element on screen if (Collections.emptyList().equals(coordinates)) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), "Couldn't find reference element on the current screen. If you can see it in the attached image then kindly consider cropping it and updating your reference image.", null, attachments); } } else { // Wait for element presence and get the needed data var objects = ElementActionsHelper.waitForElementPresence(DriverFactoryHelper.getDriver().get(), elementReferenceScreenshot); byte[] currentScreenImage = (byte[]) objects.get(0); byte[] referenceImage = (byte[]) objects.get(1); List coordinates = (List) objects.get(2); // prepare attachments var screenshot = ScreenshotManager.prepareImageForReport(currentScreenImage, "swipeElementIntoView - Current Screen Image"); var referenceScreenshot = ScreenshotManager.prepareImageForReport(referenceImage, "swipeElementIntoView - Reference Screenshot"); attachments = new LinkedList<>(); attachments.add(referenceScreenshot); attachments.add(screenshot); // If coordinates are empty then OpenCV couldn't find the element on screen if (Collections.emptyList().equals(coordinates)) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), "Couldn't find reference element on the current screen. If you can see it in the attached image then kindly consider cropping it and updating your reference image.", null, attachments); } else { new Actions(DriverFactoryHelper.getDriver().get()).scrollFromOrigin(WheelInput.ScrollOrigin.fromViewport(), coordinates.get(0), coordinates.get(1)).perform(); } } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), null, attachments, null); } catch (AssertionError assertionError) { //bubble up throw assertionError; } catch (Exception exception) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), "Couldn't find reference element on the current screen. If you can see it in the attached image then kindly consider cropping it and updating your reference image.", null, attachments, exception); } } catch (Throwable throwable) { // has to be throwable to catch assertion errors in case element was not found ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), scrollableElementLocator, throwable); } return this; } /** * Attempts to scroll element into view using the new W3C compliant actions for android and ios * * @param scrollableElementLocator the locator of the container/view/scrollable webElement that the scroll action will be performed inside * @param targetElementLocator the locator of the webElement that you want to scroll to under test (By xpath, id, * selector, name ...etc) * @param swipeDirection SwipeDirection.DOWN, UP, RIGHT, or LEFT * @return a self-reference to be used to chain actions */ public TouchActions swipeElementIntoView(By scrollableElementLocator, By targetElementLocator, SwipeDirection swipeDirection) { // Fix issue #641 Element locator is NULL by make internalScrollableElementLocator can be null and the condition become 'OR' not 'AND' try { try { if (DriverFactoryHelper.getDriver().get() instanceof AppiumDriver appiumDriver) { // appium native application boolean isElementFound = attemptToSwipeElementIntoViewInNativeApp(scrollableElementLocator, targetElementLocator, swipeDirection); if (Boolean.FALSE.equals(isElementFound)) { ElementActionsHelper.failAction(appiumDriver, targetElementLocator); } } else { // regular touch screen device if (scrollableElementLocator != null) { new Actions(DriverFactoryHelper.getDriver().get()).moveToElement(((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), scrollableElementLocator).get(1))).scrollToElement(((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), targetElementLocator).get(1))).perform(); } else { new Actions(DriverFactoryHelper.getDriver().get()).scrollToElement(((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), targetElementLocator).get(1))).perform(); } } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), targetElementLocator, Thread.currentThread().getStackTrace()[1].getMethodName(), null, null, null); } catch (Exception e) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), targetElementLocator, e); } } catch (Throwable throwable) { // has to be throwable to catch assertion errors in case element was not found ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), scrollableElementLocator, throwable); } return this; } /** * Attempts to scroll element into view using androidUIAutomator * * @param targetText element text to be used to swipe it into view * @return a self-reference to be used to chain actions */ public TouchActions swipeElementIntoView(String targetText) { DriverFactoryHelper.getDriver().get().findElement(AppiumBy.androidUIAutomator("new UiScrollable(new UiSelector().scrollable(true))" + ".scrollIntoView(new UiSelector().textContains(\"" + targetText + "\"))")); return this; } @SuppressWarnings("unchecked") private List attemptToSwipeElementIntoViewInNativeApp(By scrollableElementLocator, String targetElementImage, SwipeDirection swipeDirection) { boolean isElementFound = false; boolean canStillScroll = true; var isDiscrete = ReportManagerHelper.getDiscreteLogging(); ReportManagerHelper.setDiscreteLogging(true); List visualIdentificationObjects; // force SHAFT back into the loop even if canStillScroll is false, or ignore it completely for the first 5 scroll attempts int blindScrollingAttempts = 0; do { // appium native device // Wait for element presence and get the needed data visualIdentificationObjects = ElementActionsHelper.waitForElementPresence(DriverFactoryHelper.getDriver().get(), targetElementImage); List coordinates = (List) visualIdentificationObjects.get(2); if (!Collections.emptyList().equals(coordinates)) { // element is already on screen isElementFound = true; ReportManager.logDiscrete("Element found on screen."); } else { // for the animated GIF: ElementActionsHelper.takeScreenshot(DriverFactoryHelper.getDriver().get(), null, "swipeElementIntoView", null, true); canStillScroll = attemptW3cCompliantActionsScroll(swipeDirection, scrollableElementLocator, null); if (!canStillScroll){ // check if element can be found after scrolling to the end of the page visualIdentificationObjects = ElementActionsHelper.waitForElementPresence(DriverFactoryHelper.getDriver().get(), targetElementImage); coordinates = (List) visualIdentificationObjects.get(2); if(!Collections.emptyList().equals(coordinates)) { isElementFound = true; ReportManager.logDiscrete("Element found on screen."); } } } blindScrollingAttempts++; } while (Boolean.FALSE.equals(isElementFound) && (blindScrollingAttempts < DEFAULT_NUMBER_OF_ATTEMPTS_TO_SCROLL_TO_ELEMENT || Boolean.TRUE.equals(canStillScroll))); ReportManagerHelper.setDiscreteLogging(isDiscrete); return visualIdentificationObjects; } private boolean attemptToSwipeElementIntoViewInNativeApp(By scrollableElementLocator, By targetElementLocator, SwipeDirection swipeDirection) { boolean isElementFound = false; boolean canStillScroll = true; var isDiscrete = ReportManagerHelper.getDiscreteLogging(); ReportManagerHelper.setDiscreteLogging(true); // force SHAFT back into the loop even if canStillScroll is false, or ignore it completely for the first 5 scroll attempts int blindScrollingAttempts = 0; do { // appium native device if (ElementActionsHelper.waitForElementPresenceWithReducedTimeout(DriverFactoryHelper.getDriver().get(), targetElementLocator) > 0) { // element is already on screen isElementFound = true; ReportManager.logDiscrete("Element found on screen."); } else { // for the animated GIF: ElementActionsHelper.takeScreenshot(DriverFactoryHelper.getDriver().get(), null, "swipeElementIntoView", null, true); canStillScroll = attemptW3cCompliantActionsScroll(swipeDirection, scrollableElementLocator, targetElementLocator); if (!canStillScroll && ElementActionsHelper.waitForElementPresenceWithReducedTimeout(DriverFactoryHelper.getDriver().get(), targetElementLocator) > 0) { // element was found after scrolling to the end of the page isElementFound = true; ReportManager.logDiscrete("Element found on screen."); } } blindScrollingAttempts++; } while (Boolean.FALSE.equals(isElementFound) && (blindScrollingAttempts < DEFAULT_NUMBER_OF_ATTEMPTS_TO_SCROLL_TO_ELEMENT || Boolean.TRUE.equals(canStillScroll))); ReportManagerHelper.setDiscreteLogging(isDiscrete); return isElementFound; } private void attemptUISelectorScroll(SwipeDirection swipeDirection, int scrollableElementInstanceNumber) { ReportManager.logDiscrete("Swiping to find Element using UiSelector."); int scrollingSpeed = 100; String scrollDirection = "Forward"; ReportManager.logDiscrete("Swiping to find Element using UiSelector."); By androidUIAutomator = AppiumBy .androidUIAutomator("new UiScrollable(new UiSelector().scrollable(true).instance(" + scrollableElementInstanceNumber + ")).scroll" + scrollDirection + "(" + scrollingSpeed + ")"); ElementActionsHelper.getElementsCount(DriverFactoryHelper.getDriver().get(), androidUIAutomator); } private boolean attemptW3cCompliantActionsScroll(SwipeDirection swipeDirection, By scrollableElementLocator, By targetElementLocator) { var logMessage = "Swiping to find Element using W3C Compliant Actions. SwipeDirection \"" + swipeDirection + "\""; if (targetElementLocator != null) { logMessage += ", TargetElementLocator \"" + targetElementLocator + "\""; } if (scrollableElementLocator != null) { logMessage += ", inside ScrollableElement \"" + scrollableElementLocator + "\""; } logMessage += "."; ReportManager.logDiscrete(logMessage); Dimension screenSize = DriverFactoryHelper.getDriver().get().manage().window().getSize(); boolean canScrollMore = true; var scrollParameters = new HashMap<>(); if (scrollableElementLocator != null) { //scrolling inside an element Rectangle elementRectangle = ((WebElement) ElementActionsHelper.identifyUniqueElement(DriverFactoryHelper.getDriver().get(), scrollableElementLocator).get(1)).getRect(); scrollParameters.putAll(ImmutableMap.of( "height", elementRectangle.getHeight() * 90 / 100 )); //percent 0.5 works for UP/DOWN, optimized to 0.8 to scroll faster and introduced delay 1000ms after every scroll action to increase stability switch (swipeDirection) { case UP -> scrollParameters.putAll(ImmutableMap.of("percent", 0.8, "height", elementRectangle.getHeight() * 90 / 100, "width", elementRectangle.getWidth(), "left", elementRectangle.getX(), "top", elementRectangle.getHeight() - 100)); case DOWN -> scrollParameters.putAll(ImmutableMap.of("percent", 0.8, "height", elementRectangle.getHeight() * 90 / 100, "width", elementRectangle.getWidth(), "left", elementRectangle.getX(), "top", 100)); case RIGHT -> scrollParameters.putAll(ImmutableMap.of("percent", 1, "height", elementRectangle.getHeight(), "width", elementRectangle.getWidth() * 70 / 100, "left", 100, "top", elementRectangle.getY())); case LEFT -> scrollParameters.putAll(ImmutableMap.of("percent", 1, "height", elementRectangle.getHeight(), "width", elementRectangle.getWidth(), "left", elementRectangle.getX() + (elementRectangle.getWidth() * 50 / 100), "top", elementRectangle.getY())); } } else { //scrolling inside the screen scrollParameters.putAll(ImmutableMap.of( "width", screenSize.getWidth(), "height", screenSize.getHeight() * 90 / 100, "percent", 0.8 )); switch (swipeDirection) { case UP -> scrollParameters.putAll(ImmutableMap.of("left", 0, "top", screenSize.getHeight() - 100)); case DOWN -> scrollParameters.putAll(ImmutableMap.of("left", 0, "top", 100)); // expected issues with RIGHT and LEFT case RIGHT -> scrollParameters.putAll(ImmutableMap.of("left", 100, "top", 0)); case LEFT -> scrollParameters.putAll(ImmutableMap.of("left", screenSize.getWidth() - 100, "top", 0)); } } if (DriverFactoryHelper.getDriver().get() instanceof AndroidDriver androidDriver) { scrollParameters.putAll(ImmutableMap.of( "direction", swipeDirection.toString() )); canScrollMore = (Boolean) ((JavascriptExecutor) androidDriver).executeScript("mobile: scrollGesture", scrollParameters); } else if (DriverFactoryHelper.getDriver().get() instanceof IOSDriver iosDriver) { scrollParameters.putAll(ImmutableMap.of( "direction", swipeDirection.toString() )); //http://appium.github.io/appium-xcuitest-driver/4.16/execute-methods/#mobile-scroll var ret= ((JavascriptExecutor) iosDriver).executeScript("mobile: scroll", scrollParameters); canScrollMore = ret == null || (Boolean) ret; } var logMessageAfter = "Attempted to scroll using these parameters: \"" + scrollParameters + "\""; if (canScrollMore) { logMessageAfter += ", there is still more room to keep scrolling."; } else { logMessageAfter += ", there is no more room to keep scrolling."; } ReportManager.logDiscrete(logMessageAfter); return canScrollMore; } private void attemptPinchToZoomIn() { PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger"); PointerInput finger2 = new PointerInput(PointerInput.Kind.TOUCH, "finger2"); Dimension size = DriverFactoryHelper.getDriver().get().manage().window().getSize(); Point source = new Point(size.getWidth(), size.getHeight()); Sequence pinchAndZoom1 = new Sequence(finger, 0); pinchAndZoom1.addAction(finger.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), source.x / 2, source.y / 2)) .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg())) .addAction(new Pause(finger, Duration.ofMillis(110))) .addAction(finger.createPointerMove(Duration.ofMillis(600), PointerInput.Origin.viewport(), source.x / 3, source.y / 3)) .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg())); Sequence pinchAndZoom2 = new Sequence(finger2, 0); pinchAndZoom2.addAction(finger2.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), source.x / 2, source.y / 2)) .addAction(finger2.createPointerDown(PointerInput.MouseButton.LEFT.asArg())) .addAction(finger2.createPointerMove(Duration.ofMillis(600), PointerInput.Origin.viewport(), source.x * 3 / 4, source.y * 3 / 4)) .addAction(finger2.createPointerUp(PointerInput.MouseButton.LEFT.asArg())); ((RemoteWebDriver) DriverFactoryHelper.getDriver().get()).perform(asList(pinchAndZoom1, pinchAndZoom2)); } private void attemptPinchToZoomOut() { PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger"); PointerInput finger2 = new PointerInput(PointerInput.Kind.TOUCH, "finger2"); Dimension size = DriverFactoryHelper.getDriver().get().manage().window().getSize(); Point source = new Point(size.getWidth(), size.getHeight()); Sequence pinchAndZoom1 = new Sequence(finger, 0); pinchAndZoom1 .addAction(finger.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), source.x / 3, source.y / 3)) .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg())) .addAction(new Pause(finger, Duration.ofMillis(110))) .addAction(finger.createPointerMove(Duration.ofMillis(600), PointerInput.Origin.viewport(), source.x / 2, source.y / 2)) .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg())); Sequence pinchAndZoom2 = new Sequence(finger2, 0); pinchAndZoom2.addAction(finger2.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), source.x * 3 / 4, source.y * 3 / 4)) .addAction(finger2.createPointerDown(PointerInput.MouseButton.LEFT.asArg())) .addAction(new Pause(finger, Duration.ofMillis(100))) .addAction(finger2.createPointerMove(Duration.ofMillis(600), PointerInput.Origin.viewport(), source.x / 2, source.y / 2)) .addAction(finger2.createPointerUp(PointerInput.MouseButton.LEFT.asArg())); ((RemoteWebDriver) DriverFactoryHelper.getDriver().get()).perform(asList(pinchAndZoom1, pinchAndZoom2)); } /** * Attempts to zoom the current screen IN/ OUT in case of zoom enabled screen. * * @param zoomDirection ZoomDirection.IN or OUT * @return a self-reference to be used to chain actions */ public TouchActions pinchToZoom(ZoomDirection zoomDirection) { try { switch (zoomDirection) { case IN -> attemptPinchToZoomIn(); case OUT -> attemptPinchToZoomOut(); } } catch (Exception rootCauseException) { ElementActionsHelper.failAction(DriverFactoryHelper.getDriver().get(), null, rootCauseException); } ElementActionsHelper.passAction(DriverFactoryHelper.getDriver().get(), null, Thread.currentThread().getStackTrace()[1].getMethodName(), zoomDirection.name(), null, null); return this; } public enum ZoomDirection { IN, OUT } /** * SwipeDirection; swiping UP means the screen will move downwards */ public enum SwipeDirection { UP, DOWN, LEFT, RIGHT } public enum SwipeTechnique { W3C_ACTIONS, UI_SELECTOR } public enum KeyboardKeys { GO(ImmutableMap.of("action", "go")), DONE(ImmutableMap.of("action", "done")), SEARCH(ImmutableMap.of("action", "search")), SEND(ImmutableMap.of("action", "send")), NEXT(ImmutableMap.of("action", "next")), PREVIOUS(ImmutableMap.of("action", "previous")), NORMAL(ImmutableMap.of("action", "normal")), UNSPECIFIED(ImmutableMap.of("action", "unspecified")), NONE(ImmutableMap.of("action", "none")); private final ImmutableMap value; KeyboardKeys(ImmutableMap type) { this.value = type; } private ImmutableMap getValue() { return value; } } }