WebDriverWait and ExpectedConditions

9 min read

WebDriverWait is the workhorse you'll type a hundred times a day. It pairs with the ExpectedConditions static helper to express exactly what your test should wait for: visibility, clickability, URL change, text appearance, count of elements, alert presence. This lesson is the working catalogue — the conditions you'll use, the patterns that compose them, and the small handful of helpers that turn those incantations into one-liners. By the end you'll write tests that don't race the page.

The two classes that do everything

WebDriverWait is the polling engine. ExpectedConditions is a library of pre-built conditions to feed it. Together:

import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
 
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
 
// Wait for the element to be visible, then return it
WebElement productList = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.id("product-list"))
);

Behind that one line, wait.until(...) is doing this:

  1. Evaluate the condition.
  2. If true (the condition returns a non-null, non-false value) → return that value.
  3. If false → sleep 500ms.
  4. If 10 seconds have passed → throw TimeoutException with a message naming the condition.
  5. Otherwise → loop back to step 1.

The 500ms is the default polling interval. The 10 seconds is the timeout you set. Both are tuneable (next lesson covers FluentWait for that). The crucial property: wait.until(...) returns as soon as the condition is met, not after the full timeout. A condition that becomes true at 800ms returns at 800ms, not at 10 seconds.

The polling loop, visualised

Step 1 of 5

Check

Evaluate the ExpectedCondition (e.g., visibilityOfElementLocated). The function returns a WebElement if found and visible, null otherwise.

The ExpectedConditions you'll use 90% of the time

There are dozens of methods on ExpectedConditions. Eight cover most of what you'll write:

// 1. Element is in the DOM AND visible (display != none, opacity > 0, height/width > 0)
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("product-list")));
 
// 2. Element is visible AND enabled — safe to click
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("[data-testid='submit']")));
 
// 3. Element is in the DOM (may be hidden — useful for elements that exist but won't be clicked)
wait.until(ExpectedConditions.presenceOfElementLocated(By.id("hidden-token")));
 
// 4. Element has gone away (hidden or removed from DOM)
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loading-spinner")));
 
// 5. The element's visible text contains a string
wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("status"), "Complete"));
 
// 6. The browser URL has updated
wait.until(ExpectedConditions.urlContains("/inventory.html"));
 
// 7. Title check
wait.until(ExpectedConditions.titleIs("Swag Labs"));
 
// 8. There are at least N matching elements
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(
    By.cssSelector(".product-card"), 5
));

Each one expresses a precise question: "is the element visible yet?", "has the spinner disappeared?", "did the URL change?". Pick the one that matches what your test actually depends on. Don't reach for presenceOfElementLocated if you're about to click — elementToBeClickable is the stricter, correct check.

Three more worth knowing

// Wait for an element to be selected (checkbox/radio/option)
wait.until(ExpectedConditions.elementToBeSelected(By.id("terms")));
 
// Wait for an alert to appear
wait.until(ExpectedConditions.alertIsPresent());
 
// Compose two conditions with AND logic — both must be true
wait.until(ExpectedConditions.and(
    ExpectedConditions.urlContains("/dashboard"),
    ExpectedConditions.visibilityOfElementLocated(By.id("welcome-banner"))
));

ExpectedConditions.and(...) (and its sibling or(...)) compose conditions when one alone doesn't capture what you need. SPAs in particular often need "URL changed and the new page's main content has rendered."

The wait-then-interact pattern

Most calls fold the wait and the interaction into a single line, because wait.until(...) returns the element it just found:

// Verbose
WebElement button = wait.until(
    ExpectedConditions.elementToBeClickable(By.id("submit"))
);
button.click();
 
// Idiomatic
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit"))).click();

The second form is what you'll see in production code. It reads as: "wait for submit to be clickable, then click it."

Custom conditions with lambdas

When ExpectedConditions doesn't cover what you need, pass any function Function<WebDriver, T> — usually a lambda:

// Wait until at least 10 product cards are loaded
wait.until(driver -> driver.findElements(By.cssSelector(".product")).size() >= 10);
 
// Wait until a specific data attribute changes
wait.until(driver -> {
    WebElement modal = driver.findElement(By.cssSelector(".modal"));
    return "ready".equals(modal.getAttribute("data-state"));
});
 
// Wait for jQuery's queue to be empty (legacy apps)
wait.until(driver -> ((JavascriptExecutor) driver)
    .executeScript("return jQuery.active == 0").equals(true));

The lambda returns truthy → wait succeeds. Returns null/false → wait keeps polling. The single rule: don't throw inside the lambda for transient conditions; that's what FluentWait's ignoring(...) is for (next lesson).

Catching TimeoutException

Sometimes you genuinely want to handle the failure rather than let the test crash:

import org.openqa.selenium.TimeoutException;
 
try {
    wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("optional-banner")));
    System.out.println("Banner appeared — handle it");
} catch (TimeoutException e) {
    System.out.println("Banner didn't appear — that's fine for this test");
}

This pattern is rare. In real code, a missing element almost always means the test should fail. Use the catch only for genuinely optional UI (an "Are you sure?" dialog that may or may not appear depending on the user's previous choices, for example).

A reusable helper that everyone writes

Every Selenium codebase ends up with a tiny utility class to shorten the wait calls. Put it in src/test/java/com/mycompany/tests/base/:

package com.mycompany.tests.base;
 
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
 
import java.time.Duration;
 
public class WaitHelpers {
 
    public static final Duration DEFAULT = Duration.ofSeconds(10);
 
    public static WebElement visible(WebDriver driver, By locator) {
        return new WebDriverWait(driver, DEFAULT)
            .until(ExpectedConditions.visibilityOfElementLocated(locator));
    }
 
    public static WebElement clickable(WebDriver driver, By locator) {
        return new WebDriverWait(driver, DEFAULT)
            .until(ExpectedConditions.elementToBeClickable(locator));
    }
 
    public static void invisible(WebDriver driver, By locator) {
        new WebDriverWait(driver, DEFAULT)
            .until(ExpectedConditions.invisibilityOfElementLocated(locator));
    }
}

Now your tests read more cleanly:

visible(driver, By.id("product-list")).getText();
clickable(driver, By.id("submit")).click();
invisible(driver, By.id("loading-spinner"));

Chapter 6 moves these onto a BasePage superclass, so every page object inherits them automatically. For now, a static utility is fine.

A complete real-world test

A loading-spinner pattern you'll automate hundreds of times:

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
import java.time.Duration;
 
public class WebDriverWaitTest {
 
    WebDriver driver;
    WebDriverWait wait;
 
    @BeforeMethod
    public void setup() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        driver.get("https://www.saucedemo.com");
    }
 
    @Test
    public void shouldChainWaitsAfterLogin() {
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("user-name")))
            .sendKeys("standard_user");
        driver.findElement(By.id("password")).sendKeys("secret_sauce");
        driver.findElement(By.id("login-button")).click();
 
        // Wait for URL change AND the inventory page to render
        wait.until(ExpectedConditions.and(
            ExpectedConditions.urlContains("/inventory.html"),
            ExpectedConditions.numberOfElementsToBeMoreThan(
                By.cssSelector("[data-test='inventory-item']"), 0
            )
        ));
 
        Assert.assertEquals(
            driver.findElements(By.cssSelector("[data-test='inventory-item']")).size(),
            6
        );
    }
 
    @Test
    public void shouldWaitForCustomCondition() {
        // Custom condition — at least 6 product cards visible
        wait.until(driver -> driver
            .findElements(By.cssSelector("[data-test='inventory-item']"))
            .size() >= 6 || driver.getCurrentUrl().contains("saucedemo"));
 
        Assert.assertTrue(driver.getCurrentUrl().contains("saucedemo"));
    }
 
    @AfterMethod
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

Two tests, three patterns: chained wait-and-interact, composed and(...) conditions, and a custom lambda condition.

The Selenium tool entry on qa.codes lists every ExpectedConditions method, and the JUnit + TestNG cheat sheet has the assertion side.

⚠️ Common mistakes

  • Using presenceOfElementLocated before clicking. Presence only checks the element is in the DOM — it might still be hidden, disabled, or covered. Click on a "present-but-not-clickable" element and you get ElementClickInterceptedException or ElementNotInteractableException. Use elementToBeClickable whenever the next step is .click().
  • Reusing one WebDriverWait for the whole class with too short a timeout. A single WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(2)); saves typing but makes every wait 2 seconds. The login that genuinely takes 8 seconds will fail every CI run. Either use a generous default (10–15s) or instantiate per-call when you need a longer one — new WebDriverWait(driver, Duration.ofSeconds(30)) for the slow operation.
  • Catching TimeoutException to suppress a real failure. If a test legitimately depends on an element appearing, catching the timeout and printing "didn't appear" hides the failure from CI. The test passes when the element is missing — exactly the bug you're paid to catch. Only use the catch for truly optional UI.

🎯 Practice task

Build wait fluency on Sauce Demo. 30–40 minutes.

  1. Add WebDriverWaitTest from this lesson to your project. Run both tests; both should pass.
  2. Cover every condition. Add tests that demonstrate each of the eight conditions from the catalogue (visibility, clickable, presence, invisibility, text, urlContains, titleIs, numberOfElementsToBeMoreThan). Sauce Demo has all the surface you need — login, inventory, cart, checkout. One method per condition.
  3. Build the helpers. Create WaitHelpers.java from the lesson under src/test/java/com/mycompany/tests/base/. Refactor at least two of your existing tests to use it. Notice how much shorter the test code becomes.
  4. Compose with and(...). Write a test that waits for the inventory page using only ExpectedConditions.and(...): urlContains("/inventory.html") AND visibilityOfElementLocated(...) for the first product card. Both conditions matter — the URL changes a beat before the DOM renders.
  5. Custom condition. Write a wait that polls until the cart badge shows a specific number. Use a lambda:
    wait.until(driver -> {
        List<WebElement> badges = driver.findElements(By.cssSelector(".shopping_cart_badge"));
        return !badges.isEmpty() && badges.get(0).getText().equals("3");
    });
    Add three items to the cart, then wait for the badge to read "3". Confirm the test passes.
  6. Stretch — measure the saving. Take any test that currently uses Thread.sleep(5000). Time it before and after replacing the sleep with wait.until(...) for the actual condition. The wall-clock difference (often 4+ seconds per test) is real money on a 200-test suite.

Next lesson: FluentWait — when WebDriverWait's defaults aren't quite right, and you need custom polling intervals, multiple ignored exceptions, or a tailored failure message.

// tip to track lessons you complete and pick up where you left off across devices.