Mobile apps are asynchronous. Network calls return late, animations complete over time, view transitions don't happen instantly. Explicit waits — polling until a condition is true — beat time.sleep() on both speed and reliability.
Why not time.sleep()
time.sleep(3) always waits 3 seconds, even when the element appears in 300ms. Across 150 tests, that's 7.5 minutes of guaranteed waste. Explicit waits return as soon as the condition is met.
WebDriverWait basics
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
wait = WebDriverWait(driver, 15) # 15-second timeout
# Wait for visibility
element = wait.until(EC.visibility_of_element_located(
(AppiumBy.ACCESSIBILITY_ID, "submitButton")
))
# Wait for clickable (visible + enabled)
button = wait.until(EC.element_to_be_clickable(
(AppiumBy.ACCESSIBILITY_ID, "loginButton")
))
# Wait for text
wait.until(EC.text_to_be_present_in_element(
(AppiumBy.ACCESSIBILITY_ID, "statusLabel"),
"Order Placed"
))
# Wait for invisibility (loading spinners)
wait.until(EC.invisibility_of_element_located(
(AppiumBy.ACCESSIBILITY_ID, "loadingSpinner")
))Common expected conditions
| Condition | Use case |
|---|---|
visibility_of_element_located | Element exists and has non-zero size |
element_to_be_clickable | Visible + enabled (handles grayed-out buttons) |
invisibility_of_element_located | Wait for spinner to disappear |
presence_of_element_located | In DOM but may be invisible |
text_to_be_present_in_element | Value updates after async load |
number_of_elements_to_be_more_than | List has populated |
Custom wait conditions
Lambda-based conditions for cases expected_conditions doesn't cover:
# Wait for element count to stabilise
def wait_for_list_to_load(driver, locator, min_count=1, timeout=15):
return WebDriverWait(driver, timeout).until(
lambda d: len(d.find_elements(*locator)) >= min_count
)
items = wait_for_list_to_load(driver, (AppiumBy.ACCESSIBILITY_ID, "listItem"))
# Wait for attribute value
def wait_for_checked(driver, locator, timeout=10):
return WebDriverWait(driver, timeout).until(
lambda d: d.find_element(*locator).get_attribute("checked") == "true"
)
# Wait for specific activity (Android)
def wait_for_activity(driver, expected_activity, timeout=10):
WebDriverWait(driver, timeout).until(
lambda d: expected_activity in d.current_activity
)Return None or False to keep polling; return any other value to stop and return it.
FluentWait equivalent in Python
Python's WebDriverWait accepts poll_frequency and ignored_exceptions:
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
wait = WebDriverWait(
driver,
timeout=20,
poll_frequency=0.5, # check every 500ms
ignored_exceptions=[
NoSuchElementException,
StaleElementReferenceException,
]
)
element = wait.until(
EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "result"))
)ignored_exceptions prevents polling from stopping when the element hasn't appeared yet but find_element throws. Without it, a NoSuchElementException during polling terminates the wait immediately.
Waiting for animations
After a tap triggers an animation, wait for the element to reach its stable size:
import time
def wait_for_stable_element(driver, locator, timeout=5):
"""Wait for element dimensions to stop changing (animation complete)."""
end_time = time.time() + timeout
last_height = None
while time.time() < end_time:
try:
el = driver.find_element(*locator)
current_height = el.size["height"]
if current_height == last_height and last_height is not None:
return el
last_height = current_height
except Exception:
pass
time.sleep(0.2)
raise TimeoutException(f"Element at {locator} did not stabilise within {timeout}s")Loading spinner pattern
After triggering an async operation, wait for the spinner to appear and then disappear:
SPINNER = (AppiumBy.ACCESSIBILITY_ID, "loadingIndicator")
def wait_for_load_complete(driver, trigger_spinner_timeout=2, load_timeout=30):
"""
First waits for the spinner to appear (up to trigger_spinner_timeout),
then waits for it to disappear (up to load_timeout).
"""
try:
# Wait for spinner to appear
WebDriverWait(driver, trigger_spinner_timeout).until(
EC.visibility_of_element_located(SPINNER)
)
# Then wait for it to go away
WebDriverWait(driver, load_timeout).until(
EC.invisibility_of_element_located(SPINNER)
)
except TimeoutException:
# Spinner appeared and disappeared faster than we checked — OK
passCentralised wait utils
Put common patterns in utils/wait_utils.py:
# utils/wait_utils.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
class WaitUtils:
def __init__(self, driver, default_timeout=15, short_timeout=5, long_timeout=30):
self.driver = driver
self.default = default_timeout
self.short = short_timeout
self.long = long_timeout
def for_visible(self, locator, timeout=None):
t = timeout or self.default
return WebDriverWait(self.driver, t).until(
EC.visibility_of_element_located(locator)
)
def for_clickable(self, locator, timeout=None):
t = timeout or self.default
return WebDriverWait(self.driver, t).until(
EC.element_to_be_clickable(locator)
)
def for_invisible(self, locator, timeout=None):
t = timeout or self.long
WebDriverWait(self.driver, t).until(
EC.invisibility_of_element_located(locator)
)
def is_present(self, locator, timeout=None):
t = timeout or self.short
try:
WebDriverWait(self.driver, t).until(
EC.presence_of_element_located(locator)
)
return True
except TimeoutException:
return False