Scrolling to find elements, swiping through carousels, and pulling to refresh are everyday interactions in mobile test suites. Platform differences mean Android and iOS need different approaches.
Android — scroll to element with UIAutomator
The most reliable Android scroll strategy uses UIAutomator's UiScrollable:
from appium.webdriver.common.appiumby import AppiumBy
def scroll_to_text(driver, text: str):
"""Scrolls until an element with the given text is visible, then returns it."""
return driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
f'new UiScrollable(new UiSelector().scrollable(true))'
f'.scrollIntoView(new UiSelector().text("{text}"))'
)
# Usage
terms_item = scroll_to_text(driver, "Terms of Service")
terms_item.click()For resource-id:
def scroll_to_id(driver, resource_id: str):
return driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
f'new UiScrollable(new UiSelector().scrollable(true))'
f'.scrollIntoView(new UiSelector().resourceId("{resource_id}"))'
)iOS — scroll loop with predicate
iOS doesn't have UIAutomator. Combine a swipe loop with element presence checks:
from selenium.common.exceptions import NoSuchElementException
from appium.webdriver.common.appiumby import AppiumBy
def scroll_to_accessibility_id(driver, accessibility_id: str, max_scrolls: int = 10):
size = driver.get_window_size()
x = size["width"] // 2
start_y = int(size["height"] * 0.7)
end_y = int(size["height"] * 0.3)
for _ in range(max_scrolls):
try:
element = driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id)
if element.is_displayed():
return element
except NoSuchElementException:
pass
_swipe(driver, x, start_y, x, end_y, duration=0.5)
raise NoSuchElementException(
f"Element '{accessibility_id}' not found after {max_scrolls} scrolls"
)
def _swipe(driver, sx, sy, ex, ey, duration):
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput
from selenium.webdriver.common.actions import interaction
finger = PointerInput(interaction.POINTER_TOUCH, "finger")
actions = ActionBuilder(driver, mouse=finger)
actions.pointer_action\
.move_to_location(sx, sy)\
.pointer_down()\
.pause(duration)\
.move_to_location(ex, ey)\
.pointer_up()
actions.perform()Horizontal carousel swipe
Swipe left to advance a carousel, right to go back:
def swipe_carousel_left(driver, carousel_element):
"""Swipe left within the carousel bounds (advances to next slide)."""
loc = carousel_element.location
size = carousel_element.size
start_x = loc["x"] + int(size["width"] * 0.8)
end_x = loc["x"] + int(size["width"] * 0.2)
mid_y = loc["y"] + size["height"] // 2
_swipe(driver, start_x, mid_y, end_x, mid_y, duration=0.4)
def swipe_carousel_right(driver, carousel_element):
loc = carousel_element.location
size = carousel_element.size
start_x = loc["x"] + int(size["width"] * 0.2)
end_x = loc["x"] + int(size["width"] * 0.8)
mid_y = loc["y"] + size["height"] // 2
_swipe(driver, start_x, mid_y, end_x, mid_y, duration=0.4)Constraining to the carousel's bounds prevents triggering iOS back-swipe or Android edge navigation.
Pull to refresh
def pull_to_refresh(driver):
size = driver.get_window_size()
x = size["width"] // 2
# Slow downward drag — 1.5s makes it a pull gesture, not a scroll
_swipe(driver,
sx=x, sy=int(size["height"] * 0.25),
ex=x, ey=int(size["height"] * 0.75),
duration=1.5)After calling pull_to_refresh(), wait for the refresh indicator to disappear:
LOADING_SPINNER = (AppiumBy.ACCESSIBILITY_ID, "loadingIndicator")
def refresh_and_wait(driver, page):
pull_to_refresh(driver)
page.wait_for_invisible(LOADING_SPINNER) # waits up to 30s by defaultDetecting end of list (loop guard)
When scrolling in a loop to find an element, guard against infinite scrolling by detecting that the page hasn't changed:
def scroll_to_element_or_end(driver, accessibility_id: str) -> bool:
"""Returns True if element found, False if end of list reached."""
size = driver.get_window_size()
x = size["width"] // 2
for _ in range(15):
try:
el = driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id)
if el.is_displayed():
return True
except NoSuchElementException:
pass
before = driver.page_source
_swipe(driver, x, int(size["height"] * 0.7), x, int(size["height"] * 0.3), 0.5)
after = driver.page_source
if before == after:
return False # page didn't change — we're at the bottom
return Falsepage_source comparison is reliable but slow on complex screens — each call dumps the full element tree. Use sparingly.
Scroll to top (Android)
def scroll_to_top_android(driver):
"""Uses UIAutomator to scroll to the very top of a scrollable list."""
driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiScrollable(new UiSelector().scrollable(true)).scrollToBeginning(5)'
)scrollToBeginning(5) performs up to 5 swipes toward the top.
In GestureUtils
class GestureUtils:
def __init__(self, driver):
self.driver = driver
def scroll_to_text(self, text: str):
return scroll_to_text(self.driver, text)
def scroll_to_id(self, resource_id: str):
return scroll_to_id(self.driver, resource_id)
def swipe_carousel_left(self, element):
swipe_carousel_left(self.driver, element)
def pull_to_refresh(self):
pull_to_refresh(self.driver)
def scroll_to_top(self):
scroll_to_top_android(self.driver)