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 itWebElement productList = wait.until( ExpectedConditions.visibilityOfElementLocated(By.id("product-list")));
Behind that one line, wait.until(...) is doing this:
Evaluate the condition.
If true (the condition returns a non-null, non-false value) → return that value.
If false → sleep 500ms.
If 10 seconds have passed → throw TimeoutException with a message naming the condition.
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 clickwait.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 stringwait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("status"), "Complete"));// 6. The browser URL has updatedwait.until(ExpectedConditions.urlContains("/inventory.html"));// 7. Title checkwait.until(ExpectedConditions.titleIs("Swag Labs"));// 8. There are at least N matching elementswait.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 appearwait.until(ExpectedConditions.alertIsPresent());// Compose two conditions with AND logic — both must be truewait.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:
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 loadedwait.until(driver -> driver.findElements(By.cssSelector(".product")).size() >= 10);// Wait until a specific data attribute changeswait.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)); }}
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.
Add WebDriverWaitTest from this lesson to your project. Run both tests; both should pass.
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.
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.
Compose with and(...). Write a test that waits for the inventory page using onlyExpectedConditions.and(...): urlContains("/inventory.html") AND visibilityOfElementLocated(...) for the first product card. Both conditions matter — the URL changes a beat before the DOM renders.
Custom condition. Write a wait that polls until the cart badge shows a specific number. Use a lambda:
Add three items to the cart, then wait for the badge to read "3". Confirm the test passes.
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.