Implementing Swipe and Scroll with mobile: Commands

7 min read

Scrolling and swiping are the most common interactions in mobile testing — finding list items, navigating carousels, pulling to refresh. Each platform handles scroll differently, and the right approach depends on whether you need to find an element or just move the viewport.

Vertical scroll to find an element — Android

On Android, UIAutomator's UiScrollable is the most reliable way to scroll to an element by text or attribute. It handles both ListView and RecyclerView:

public WebElement scrollToText(String text) {
    return driver.findElement(AppiumBy.androidUIAutomator(
        "new UiScrollable(new UiSelector().scrollable(true))" +
        ".scrollIntoView(new UiSelector().text(\"" + text + "\"))"
    ));
}
 
// Usage
WebElement termsItem = scrollToText("Terms of Service");
termsItem.click();

scrollIntoView scrolls until the element with the given text is visible, then returns it. If the element doesn't exist in the list, it throws UiObjectNotFoundException (wrapped in a WebDriverException by Appium).

For resource-id instead of text:

public WebElement scrollToId(String resourceId) {
    return driver.findElement(AppiumBy.androidUIAutomator(
        "new UiScrollable(new UiSelector().scrollable(true))" +
        ".scrollIntoView(new UiSelector().resourceId(\"" + resourceId + "\"))"
    ));
}

Vertical scroll to find an element — iOS

iOS doesn't have UIAutomator. Use predicate strings combined with a scroll loop:

public WebElement scrollToAccessibilityId(String accessibilityId) {
    Dimension screen = driver.manage().window().getSize();
    int maxScrolls = 10;
 
    for (int i = 0; i < maxScrolls; i++) {
        try {
            WebElement element = driver.findElement(
                AppiumBy.accessibilityId(accessibilityId)
            );
            if (element.isDisplayed()) {
                return element;
            }
        } catch (NoSuchElementException ignored) {}
 
        // Swipe up to scroll down
        swipeVertical(
            (int)(screen.getWidth() * 0.5),
            (int)(screen.getHeight() * 0.7),
            (int)(screen.getHeight() * 0.3)
        );
    }
    throw new NoSuchElementException("Element not found after " + maxScrolls + " scrolls: " + accessibilityId);
}
 
private void swipeVertical(int x, int startY, int endY) {
    PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
    Sequence scroll = new Sequence(finger, 0)
        .addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), x, startY))
        .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
        .addAction(finger.createPointerMove(Duration.ofMillis(500), PointerInput.Origin.viewport(), x, endY))
        .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
    driver.perform(List.of(scroll));
}

Horizontal swipe for carousels

Carousels require horizontal swipes. Swipe left to advance, right to go back:

public void swipeCarouselLeft(WebElement carousel) {
    Point location = carousel.getLocation();
    Dimension size = carousel.getSize();
 
    int startX = location.getX() + (int)(size.getWidth() * 0.8);
    int endX   = location.getX() + (int)(size.getWidth() * 0.2);
    int midY   = location.getY() + size.getHeight() / 2;
 
    PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
    Sequence swipe = new Sequence(finger, 0)
        .addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), startX, midY))
        .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
        .addAction(finger.createPointerMove(Duration.ofMillis(400), PointerInput.Origin.viewport(), endX, midY))
        .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
 
    driver.perform(List.of(swipe));
}

Constrain the swipe to within the carousel's bounds — swiping across the full screen may trigger system gestures (iOS back swipe, Android edge nav).

Pull to refresh

Pull-to-refresh requires a slow downward drag from the top of the list:

public void pullToRefresh() {
    Dimension screen = driver.manage().window().getSize();
    int x = screen.getWidth() / 2;
    int startY = (int)(screen.getHeight() * 0.3);
    int endY = (int)(screen.getHeight() * 0.7);
 
    PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
    Sequence pull = new Sequence(finger, 0)
        .addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), x, startY))
        .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
        // Slow drag — 1200ms makes it readable as a pull gesture, not a flick
        .addAction(finger.createPointerMove(Duration.ofMillis(1200), PointerInput.Origin.viewport(), x, endY))
        .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
 
    driver.perform(List.of(pull));
}

After pullToRefresh(), add an explicit wait for the refresh indicator to disappear before asserting on refreshed content.

Scroll to top / bottom

public void scrollToTop() {
    // Swipe down quickly (fling)
    Dimension screen = driver.manage().window().getSize();
    swipeVertical(screen.getWidth() / 2,
        (int)(screen.getHeight() * 0.2),
        (int)(screen.getHeight() * 0.9));
    // Repeat 3 times to reach the top of very long lists
    for (int i = 0; i < 2; i++) {
        swipeVertical(screen.getWidth() / 2,
            (int)(screen.getHeight() * 0.2),
            (int)(screen.getHeight() * 0.9));
    }
}

For Android, UiScrollable provides scrollToBeginning(maxSwipes) and scrollToEnd(maxSwipes) which are more reliable than coordinate-based flings.

Detecting end of list

When scrolling in a loop, detect that you've reached the bottom to avoid infinite loops:

public boolean isAtBottomOfList() {
    // Save page source before and after a scroll
    String before = driver.getPageSource();
    swipeVertical(screen.getWidth() / 2,
        (int)(screen.getHeight() * 0.7),
        (int)(screen.getHeight() * 0.3));
    String after = driver.getPageSource();
    return before.equals(after); // no change means we're at the bottom
}

Comparing page source is a common but slow technique. A faster alternative on Android: check if the scrollable container's child count stopped changing between scrolls.

// tip to track lessons you complete and pick up where you left off across devices.