🔥 0
0
Lesson 5 of 10 25 min +200 XP

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
Cons:
  • 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
Cons:
  • 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

DoDon't
Use explicit waitsUse Thread.sleep()
Wait for specific conditionsWait arbitrary times
Use shortest reasonable timeoutUse very long timeouts (60s+)
Combine with good locatorsHope timing works out
Handle StaleElementReferenceIgnore 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.