Fluent Waits and Custom Expected Conditions — Advanced Synchronisation

Standard expected conditions cover most scenarios, but real-world applications sometimes need custom synchronisation logic. A dropdown that populates after an AJAX call, a table that finishes sorting, an animation that must complete before interaction — these situations require custom expected conditions. Fluent waits add additional control over polling intervals and exception handling, giving you fine-grained control over the wait behaviour.

Custom Expected Conditions and Fluent Wait Configuration

A custom expected condition is simply a callable that returns a truthy value when the condition is met, or a falsy value (or raises an exception) when it is not. WebDriverWait calls your callable repeatedly until it succeeds or the timeout expires.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import StaleElementReferenceException, NoSuchElementException


# ── Custom Expected Condition: element has specific attribute value ──
class element_attribute_to_be:
    def __init__(self, locator, attribute, value):
        self.locator = locator
        self.attribute = attribute
        self.value = value

    def __call__(self, driver):
        try:
            element = driver.find_element(*self.locator)
            current = element.get_attribute(self.attribute)
            if current == self.value:
                return element
            return False
        except (NoSuchElementException, StaleElementReferenceException):
            return False


# ── Custom Expected Condition: table has finished loading rows ──
class table_row_count_at_least:
    def __init__(self, table_locator, row_selector, min_count):
        self.table_locator = table_locator
        self.row_selector = row_selector
        self.min_count = min_count

    def __call__(self, driver):
        try:
            table = driver.find_element(*self.table_locator)
            rows = table.find_elements(By.CSS_SELECTOR, self.row_selector)
            if len(rows) >= self.min_count:
                return rows
            return False
        except (NoSuchElementException, StaleElementReferenceException):
            return False


# ── Custom Expected Condition using a lambda ──
# Wait for the page to have no active AJAX requests (jQuery)
def no_active_ajax(driver):
    return driver.execute_script("return jQuery.active === 0")


# ── Usage examples ──
# Wait for a dropdown to have the value "completed"
# WebDriverWait(driver, 10).until(
#     element_attribute_to_be(
#         (By.ID, "order-status"), "data-status", "completed"
#     )
# )

# Wait for a table to have at least 5 rows
# rows = WebDriverWait(driver, 15).until(
#     table_row_count_at_least(
#         (By.ID, "results-table"), "tbody tr", 5
#     )
# )

# Wait for jQuery AJAX to finish
# WebDriverWait(driver, 10).until(no_active_ajax)


# ── Fluent Wait — custom polling and ignored exceptions ──
# Python's WebDriverWait already supports these via constructor parameters:

# Fluent wait with 0.25s polling and ignoring stale element exceptions
# wait = WebDriverWait(
#     driver,
#     timeout=15,
#     poll_frequency=0.25,               # Check every 250ms (default is 500ms)
#     ignored_exceptions=[
#         StaleElementReferenceException,  # Ignore stale during polling
#         NoSuchElementException,          # Ignore not-found during polling
#     ]
# )
# element = wait.until(EC.element_to_be_clickable((By.ID, "dynamic-btn")))

print("Custom Expected Conditions")
print("=" * 55)
print("  element_attribute_to_be  — wait for attribute to have specific value")
print("  table_row_count_at_least — wait for table to populate with N rows")
print("  no_active_ajax           — wait for jQuery AJAX to complete")
print()
print("Fluent Wait Parameters:")
print("  timeout          — maximum wait duration")
print("  poll_frequency   — how often to check (default 500ms)")
print("  ignored_exceptions — exceptions to suppress during polling")
Note: A custom expected condition can return any truthy value — not just True. Returning the element itself (as in element_attribute_to_be) is a best practice because it lets you chain: element = wait.until(my_condition); element.click(). Returning True means you have to find the element again after the wait completes. Returning the element directly saves a redundant lookup and avoids a potential race condition between the wait and the subsequent interaction.
Tip: For applications that use jQuery, the jQuery.active === 0 check is a powerful way to wait for all AJAX requests to complete. For modern apps using the Fetch API instead of jQuery, use: driver.execute_script("return window.performance.getEntriesByType('resource').filter(r => !r.responseEnd).length === 0"). These JavaScript-level waits are more reliable than waiting for specific DOM changes because they target the root cause (network requests) rather than the symptom (element updates).
Warning: Setting poll_frequency too low (e.g., 10ms) creates excessive CPU usage as Selenium hammers the browser with requests. The default 500ms is appropriate for most scenarios. Lower it to 100-250ms only for time-critical interactions where the 500ms granularity causes noticeable test slowness. Never set it below 50ms — the overhead of each poll request makes sub-50ms polling counterproductive.

Common Mistakes

Mistake 1 — Writing a custom condition when a built-in one exists

❌ Wrong: Writing a custom callable to check if an element is visible when EC.visibility_of_element_located already does exactly that.

✅ Correct: Checking the built-in expected conditions list first. Custom conditions are only needed when no built-in condition matches your scenario — attribute value checks, row count checks, AJAX completion, and other application-specific states.

Mistake 2 — Not handling StaleElementReferenceException in custom conditions

❌ Wrong: A custom condition finds an element and reads its text, but does not catch StaleElementReferenceException. If the DOM re-renders between the find and the read, the condition crashes instead of retrying.

✅ Correct: Wrapping element interactions in try/except (StaleElementReferenceException, NoSuchElementException): return False. This tells the wait to retry on the next poll instead of aborting.

🧠 Test Yourself

You need to wait for a table to populate with at least 10 rows after an AJAX call. No built-in expected condition handles this. What is the correct approach?