Waiting & Synchronization
The #1 Cause of Flaky Tests
> "I've spent hours debugging timing issues and fighting StaleElementReferenceException like it was a final boss in a bad video game."
Flakiness happens when tests sometimes pass and sometimes fail with no code changes. The main cause? Timing issues.Your test runs faster than the browser. By the time Selenium looks for an element, the page hasn't loaded it yet.
The Wrong Way: Thread.sleep()
# DON'T DO THIS
import time
driver.get("https://shop.example.com")
time.sleep(5) # Wait 5 seconds and pray
driver.find_element(By.ID, "product-list").click()
Why it's bad:
- Wastes time (waits even when element is ready in 1 second)
- Still fails if element takes 6 seconds
- Makes test suite unbearably slow
- Doesn't actually solve the problem
Types of Waits in Selenium
1. Implicit Wait
Set once, applies to all find_element calls.
# Set implicit wait (once, typically in setup)
driver.implicitly_wait(10) # Wait up to 10 seconds
# Now this will wait up to 10 seconds for element to appear
element = driver.find_element(By.ID, "product-list")
Pros:
- Simple to implement
- Applies globally
- Can't customize per element
- Applies to ALL elements (even those that should fail fast)
- Hides actual timing issues
2. Explicit Wait (Recommended)
Wait for a specific condition on a specific element.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
# Wait up to 20 seconds for element to be clickable
wait = WebDriverWait(driver, 20)
add_to_cart = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='add-to-cart']"))
)
add_to_cart.click()
Pros:
- Precise control
- Different waits for different elements
- Clear about what you're waiting for
- More verbose
- Need to import additional modules
3. Fluent Wait
Explicit wait with custom polling and ignored exceptions.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
wait = WebDriverWait(
driver,
timeout=30,
poll_frequency=0.5, # Check every 0.5 seconds
ignored_exceptions=[NoSuchElementException, StaleElementReferenceException]
)
element = wait.until(
EC.presence_of_element_located((By.ID, "dynamic-content"))
)
Expected Conditions for E-Commerce
Element Visibility
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
# Wait for element to be present in DOM
wait.until(EC.presence_of_element_located((By.ID, "cart")))
# Wait for element to be visible on page
wait.until(EC.visibility_of_element_located((By.ID, "cart")))
# Wait for element to be clickable (visible + enabled)
wait.until(EC.element_to_be_clickable((By.ID, "checkout-btn")))
Text and Values
# Wait for specific text
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, "[data-testid='cart-count']"),
"3"
))
# Wait for element text to contain substring
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, ".order-status"),
"Confirmed"
))
# Wait for input value
wait.until(EC.text_to_be_present_in_element_value(
(By.ID, "quantity"),
"2"
))
Page and URL
# Wait for URL to contain string
wait.until(EC.url_contains("/checkout"))
# Wait for URL to match exactly
wait.until(EC.url_to_be("https://shop.example.com/order-confirmed"))
# Wait for title
wait.until(EC.title_contains("Order Confirmed"))
Element State Changes
# Wait for element to disappear (loading spinner)
wait.until(EC.invisibility_of_element_located(
(By.CSS_SELECTOR, ".loading-spinner")
))
# Wait for element to become stale (page refresh)
old_element = driver.find_element(By.ID, "product-list")
# ... trigger refresh ...
wait.until(EC.staleness_of(old_element))
# Wait for number of elements
wait.until(EC.number_of_windows_to_be(2))
E-Commerce Wait Patterns
Adding to Cart
def add_to_cart(driver, product_id):
wait = WebDriverWait(driver, 10)
# Click add to cart
add_btn = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, f"[data-testid='add-to-cart-{product_id}']")
))
add_btn.click()
# Wait for cart count to update (AJAX)
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, "[data-testid='cart-count']"),
"1" # or use lambda for dynamic count
))
# Wait for "Added!" confirmation to appear
wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR, ".add-confirmation")
))
# Wait for confirmation to disappear
wait.until(EC.invisibility_of_element_located(
(By.CSS_SELECTOR, ".add-confirmation")
))
Checkout Flow
def complete_checkout(driver):
wait = WebDriverWait(driver, 20)
# Wait for checkout page to load
wait.until(EC.url_contains("/checkout"))
# Fill shipping form
email = wait.until(EC.element_to_be_clickable((By.ID, "email")))
email.send_keys("test@example.com")
# Wait for address autocomplete to load
address = wait.until(EC.element_to_be_clickable((By.ID, "address")))
address.send_keys("123 Test St")
# Wait for shipping options to load (often AJAX)
wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, ".shipping-options")
))
# Click continue
continue_btn = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, "[data-testid='continue-to-payment']")
))
continue_btn.click()
# Wait for payment section
wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR, "[data-testid='payment-form']")
))
Product Search with Filters
def search_and_filter(driver, query, category):
wait = WebDriverWait(driver, 15)
# Search
search = wait.until(EC.element_to_be_clickable((By.ID, "search")))
search.send_keys(query)
search.submit()
# Wait for loading spinner to disappear
wait.until(EC.invisibility_of_element_located(
(By.CSS_SELECTOR, ".search-loading")
))
# Wait for results to load
wait.until(EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, "[data-testid='product-card']")
))
# Apply filter
filter_btn = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, f"[data-testid='filter-{category}']")
))
filter_btn.click()
# Wait for filtered results (count changes)
# Store original count
original_count = len(driver.find_elements(By.CSS_SELECTOR, "[data-testid='product-card']"))
# Wait for count to potentially change
time.sleep(0.5) # Brief pause for filter to apply
# Wait for loading to finish
wait.until(EC.invisibility_of_element_located(
(By.CSS_SELECTOR, ".filter-loading")
))
Custom Wait Conditions
def wait_for_cart_count(driver, expected_count, timeout=10):
"""Wait for cart to have specific item count"""
def check_cart_count(driver):
try:
count_element = driver.find_element(
By.CSS_SELECTOR, "[data-testid='cart-count']"
)
current_count = int(count_element.text)
return current_count == expected_count
except (NoSuchElementException, ValueError):
return False
wait = WebDriverWait(driver, timeout)
wait.until(check_cart_count)
# Usage
add_to_cart(driver, product_id)
wait_for_cart_count(driver, 1)
The StaleElementReferenceException
This happens when an element you found is no longer attached to the DOM (page refreshed, AJAX updated content).
# Problem
products = driver.find_elements(By.CSS_SELECTOR, ".product")
driver.find_element(By.ID, "refresh").click() # Page updates
products[0].click() # BOOM! StaleElementReferenceException
# Solution: Re-find elements after DOM changes
driver.find_element(By.ID, "refresh").click()
wait.until(EC.staleness_of(products[0])) # Wait for old element to go stale
products = driver.find_elements(By.CSS_SELECTOR, ".product") # Re-find
products[0].click() # Works!
Best Practices
| Do | Don't |
|---|---|
| Use explicit waits | Use Thread.sleep() |
| Wait for specific conditions | Wait arbitrary times |
| Use shortest reasonable timeout | Use very long timeouts (60s+) |
| Combine with good locators | Hope timing works out |
| Handle StaleElementReference | Ignore intermittent failures |
Key Takeaways
- Never use Thread.sleep() - It's slow and unreliable
- Explicit waits > Implicit waits - More control, clearer intent
- Wait for conditions, not time -
element_to_be_clickable, not "5 seconds" - Re-find elements after DOM changes - Avoid StaleElementReference
- Test your waits - Run tests multiple times to catch flakiness
Next Lesson
Now that we can find elements and wait for them, let's organize our code properly with Page Object Model for E-Commerce.