Explicit Waits and Expected Conditions — Precision Synchronisation

Explicit waits are the professional solution to Selenium synchronisation. Unlike implicit waits that apply the same behaviour to every element lookup, explicit waits let you specify exactly what condition to wait for, on exactly which element, with a custom timeout. “Wait up to 10 seconds for the login button to be clickable.” “Wait up to 5 seconds for the error message to become visible.” “Wait up to 15 seconds for the URL to contain ‘dashboard’.” This precision is what makes explicit waits both faster and more reliable than any other synchronisation strategy.

WebDriverWait and Expected Conditions — The Complete Reference

The explicit wait pattern has three components: a WebDriverWait object with a timeout, an expected condition, and the locator or value to check.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

# driver = webdriver.Chrome()  # assume driver is already created

# ── Expected Conditions — organised by category ──

EC_REFERENCE = {
    "Element Presence (in DOM)": [
        {
            "condition": "EC.presence_of_element_located(locator)",
            "returns": "WebElement",
            "use": "Element exists in DOM (may be hidden)",
            "example": 'WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "results")))',
        },
        {
            "condition": "EC.presence_of_all_elements_located(locator)",
            "returns": "List[WebElement]",
            "use": "At least one element matching locator exists in DOM",
            "example": 'WebDriverWait(driver, 10).until(EC.presence_of_all_elements_located((By.CLASS_NAME, "item")))',
        },
    ],
    "Element Visibility": [
        {
            "condition": "EC.visibility_of_element_located(locator)",
            "returns": "WebElement",
            "use": "Element is present AND visible (displayed, non-zero size)",
            "example": 'WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "welcome")))',
        },
        {
            "condition": "EC.invisibility_of_element_located(locator)",
            "returns": "Boolean",
            "use": "Element has disappeared or become hidden (loading spinner gone)",
            "example": 'WebDriverWait(driver, 10).until(EC.invisibility_of_element_located((By.ID, "spinner")))',
        },
    ],
    "Element Interactability": [
        {
            "condition": "EC.element_to_be_clickable(locator)",
            "returns": "WebElement",
            "use": "Element is visible AND enabled AND not covered",
            "example": 'WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "submit-btn")))',
        },
    ],
    "Text and Attribute": [
        {
            "condition": "EC.text_to_be_present_in_element(locator, text)",
            "returns": "Boolean",
            "use": "Element contains specific text (e.g. success message appeared)",
            "example": 'WebDriverWait(driver, 10).until(EC.text_to_be_present_in_element((By.ID, "status"), "Complete"))',
        },
        {
            "condition": "EC.text_to_be_present_in_element_attribute(locator, attr, text)",
            "returns": "Boolean",
            "use": "Element attribute contains specific text",
            "example": 'WebDriverWait(driver, 10).until(EC.text_to_be_present_in_element_attribute((By.ID, "input"), "class", "valid"))',
        },
    ],
    "URL and Title": [
        {
            "condition": "EC.url_contains(text)",
            "returns": "Boolean",
            "use": "Current URL contains substring (after navigation)",
            "example": 'WebDriverWait(driver, 10).until(EC.url_contains("dashboard"))',
        },
        {
            "condition": "EC.url_to_be(exact_url)",
            "returns": "Boolean",
            "use": "Current URL matches exactly",
            "example": 'WebDriverWait(driver, 10).until(EC.url_to_be("https://app.com/home"))',
        },
        {
            "condition": "EC.title_contains(text)",
            "returns": "Boolean",
            "use": "Page title contains substring",
            "example": 'WebDriverWait(driver, 10).until(EC.title_contains("Dashboard"))',
        },
    ],
    "Frames and Windows": [
        {
            "condition": "EC.frame_to_be_available_and_switch_to_it(locator)",
            "returns": "Boolean",
            "use": "Wait for iframe to load then switch into it",
            "example": 'WebDriverWait(driver, 10).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "payment-frame")))',
        },
        {
            "condition": "EC.number_of_windows_to_be(count)",
            "returns": "Boolean",
            "use": "Wait for a popup window to open",
            "example": 'WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2))',
        },
    ],
    "Staleness": [
        {
            "condition": "EC.staleness_of(element)",
            "returns": "Boolean",
            "use": "Wait for an element to be removed from DOM (page refresh/nav)",
            "example": 'old_element = driver.find_element(By.ID, "content")\nWebDriverWait(driver, 10).until(EC.staleness_of(old_element))',
        },
    ],
}

total_conditions = sum(len(v) for v in EC_REFERENCE.values())
print(f"Expected Conditions Reference — {total_conditions} conditions in {len(EC_REFERENCE)} categories\n")

for category, conditions in EC_REFERENCE.items():
    print(f"\n  [{category}]")
    for c in conditions:
        print(f"    {c['condition']}")
        print(f"      Returns: {c['returns']}")
        print(f"      Use: {c['use']}")
Note: The most commonly used expected conditions are visibility_of_element_located (for reading text or verifying display), element_to_be_clickable (for clicking buttons and links), and invisibility_of_element_located (for waiting for loading spinners to disappear). These three cover approximately 80% of synchronisation needs. Learn them first, then add others as specific scenarios arise.
Tip: When waiting for a page to finish loading after navigation, use EC.url_contains() or EC.visibility_of_element_located() on a key element of the destination page rather than time.sleep(). For example, after login: WebDriverWait(driver, 10).until(EC.url_contains("dashboard")). This returns instantly when the navigation completes and tolerates slow environments up to the timeout. It is both faster and more reliable than any fixed sleep duration.
Warning: EC.presence_of_element_located is NOT the same as EC.visibility_of_element_located. Presence means the element exists in the DOM — it could be hidden with display: none. Visibility means it exists AND is displayed with a non-zero size. If you wait for presence and then try to click, you may get ElementNotInteractableException because the element is present but hidden. Use presence only when you need to confirm DOM loading; use visibility or clickability for interaction.

Common Mistakes

Mistake 1 — Using presence when you need visibility or clickability

❌ Wrong: WebDriverWait(driver, 10).until(EC.presence_of_element_located(btn)).click() — the button might be present but hidden or covered.

✅ Correct: WebDriverWait(driver, 10).until(EC.element_to_be_clickable(btn)).click() — confirms the button is visible, enabled, and not covered before clicking.

Mistake 2 — Using the same timeout for every wait

❌ Wrong: Every WebDriverWait uses 30 seconds, even for elements that should appear in under 1 second.

✅ Correct: Calibrating timeouts to expected behaviour: 5 seconds for elements on the current page, 10 seconds for page navigations, 15 seconds for AJAX-heavy operations. Shorter timeouts give faster failure feedback for legitimate defects.

🧠 Test Yourself

After clicking a “Submit” button, a loading spinner appears and then disappears when the operation completes, revealing a success message. Which sequence of explicit waits correctly handles this?