← Blog

"Selenium Series #6: Waits — Implicit, Explicit and Fluent — Eliminating Flaky Tests"

Timing issues cause 80% of Selenium test flakiness. Learn the difference between implicit, explicit and fluent waits, all ExpectedConditions, and how to build a robust wait utility.

reading now
views
comments

Series Navigation

Part 5: WebElement Interactions

Part 7: TestNG Integration — Annotations, Groups and Parallel Runs


Why Waits Exist

Modern web apps are asynchronous. When you click a button, the response might arrive 200ms later or 3 seconds later depending on network conditions. Selenium doesn't know to wait — it tries to find the next element immediately and throws NoSuchElementException.

User clicks Login
      │
      ▼
Browser sends request ──── network ────► Server
                                              │ processes
                                              ▼
Browser receives response ◄─── network ─── Response
      │
      ▼
Page updates (AJAX, React re-render, etc.)
      │
      ▼
Element is now available ← Selenium should wait until HERE

Without proper waits, Selenium checks for the element before the page updates.


The Wrong Way — Thread.sleep()

// ❌ NEVER do this in production tests
driver.findElement(By.id("login")).click();
Thread.sleep(3000);  // blindly wait 3 seconds
driver.findElement(By.id("dashboard")); // hope it's ready

// Problems:
// 1. Wastes time — if page loads in 0.5s, you still wait 3 full seconds
// 2. Still flaky — if page takes 4s, test still fails
// 3. Scales badly — 100 tests × 3s each = 5 minutes of pure sleeping

Implicit Wait — Set Once, Apply Everywhere

Implicit wait tells WebDriver to poll the DOM for a certain duration before throwing NoSuchElementException:

import java.util.concurrent.TimeUnit;

// Set once in BaseTest setUp() — applies to every findElement() call
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

// Now this will retry for up to 10 seconds before failing
WebElement element = driver.findElement(By.id("dynamicContent"));

How it works:

findElement("dynamicContent") called
      │
      ▼
Element found? → Yes → Return element immediately
      │
      No → Wait 500ms → Try again
      │
      No → Wait 500ms → Try again
      ...
      │
      No → 10 seconds elapsed → throw NoSuchElementException

Implicit wait problems:

  • Applies to EVERY findElement() call, including findElements() used to check if element exists
  • Makes negative assertions slow (checking element is NOT present waits full timeout)
  • Interacts badly with explicit waits — never mix them

Rule: If you use implicit wait, use it consistently and never mix with explicit waits.


Explicit Wait — Precise and Powerful

Explicit waits wait for a specific condition before proceeding. This is the recommended approach:

import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;

// Create wait with 10 second timeout
WebDriverWait wait = new WebDriverWait(driver, 10);

// Wait until element is visible
WebElement element = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.id("dynamicContent"))
);
element.click();

// Wait until element is clickable (visible + enabled)
WebElement button = wait.until(
    ExpectedConditions.elementToBeClickable(By.id("submitBtn"))
);
button.click();

The explicit wait polls every 500ms by default and throws TimeoutException if condition isn't met within the timeout.


All ExpectedConditions — Complete Reference

WebDriverWait wait = new WebDriverWait(driver, 15);

// ── ELEMENT PRESENCE ─────────────────────────────────────────────────────
// Checks DOM only — element may not be visible
WebElement el = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.id("myEl"))
);

// Multiple elements present
List<WebElement> els = wait.until(
    ExpectedConditions.presenceOfAllElementsLocatedBy(By.cssSelector(".item"))
);

// ── ELEMENT VISIBILITY ───────────────────────────────────────────────────
// Element in DOM AND visible (not hidden)
WebElement visEl = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.id("myEl"))
);

// Visibility of already-found element
WebElement found = driver.findElement(By.id("myEl"));
wait.until(ExpectedConditions.visibilityOf(found));

// All elements visible
List<WebElement> visEls = wait.until(
    ExpectedConditions.visibilityOfAllElementsLocatedBy(By.cssSelector(".row"))
);

// ── ELEMENT CLICKABILITY ─────────────────────────────────────────────────
// Visible AND enabled (not disabled)
wait.until(ExpectedConditions.elementToBeClickable(By.id("submitBtn")));

// ── ELEMENT INVISIBILITY ─────────────────────────────────────────────────
// Wait for element to disappear (loading spinner, modal overlay)
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loadingSpinner")));
wait.until(ExpectedConditions.invisibilityOf(spinnerElement));

// ── TEXT CONDITIONS ──────────────────────────────────────────────────────
// Wait for exact text
wait.until(ExpectedConditions.textToBe(By.id("status"), "Complete"));

// Wait for text to contain substring
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.id("message"), "Success"
));

// Wait for input value
wait.until(ExpectedConditions.textToBePresentInElementValue(
    By.id("emailField"), "user@"
));

// ── URL CONDITIONS ───────────────────────────────────────────────────────
wait.until(ExpectedConditions.urlContains("/dashboard"));
wait.until(ExpectedConditions.urlMatches(".*\\/dashboard\\?tab=overview.*"));
wait.until(ExpectedConditions.urlToBe("https://app.example.com/home"));

// ── TITLE CONDITIONS ─────────────────────────────────────────────────────
wait.until(ExpectedConditions.titleContains("Dashboard"));
wait.until(ExpectedConditions.titleIs("My App - Dashboard"));

// ── FRAME CONDITIONS ─────────────────────────────────────────────────────
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframeName"));
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.id("paymentFrame")));

// ── ALERT CONDITIONS ─────────────────────────────────────────────────────
wait.until(ExpectedConditions.alertIsPresent());

// ── ATTRIBUTE CONDITIONS ─────────────────────────────────────────────────
wait.until(ExpectedConditions.attributeContains(
    By.id("statusLabel"), "class", "success"
));
wait.until(ExpectedConditions.attributeToBe(
    By.id("progressBar"), "aria-valuenow", "100"
));

// ── SELECTION CONDITIONS ─────────────────────────────────────────────────
wait.until(ExpectedConditions.elementToBeSelected(By.id("checkbox")));
wait.until(ExpectedConditions.elementSelectionStateToBe(
    By.id("checkbox"), true
));

// ── STALENESS ────────────────────────────────────────────────────────────
// Wait for old element reference to go stale (page refreshed/re-rendered)
WebElement oldRef = driver.findElement(By.id("content"));
wait.until(ExpectedConditions.stalenessOf(oldRef));
// Then find the fresh element
WebElement newRef = driver.findElement(By.id("content"));

// ── NUMBER OF ELEMENTS ───────────────────────────────────────────────────
wait.until(ExpectedConditions.numberOfElementsToBe(By.cssSelector(".item"), 5));
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.cssSelector(".row"), 0));
wait.until(ExpectedConditions.numberOfElementsToBeLessThan(By.cssSelector(".error"), 1));

Fluent Wait — Full Control

Fluent wait gives you complete control over polling interval, timeout, and which exceptions to ignore:

import org.openqa.selenium.support.ui.FluentWait;
import java.time.Duration;
import java.util.NoSuchElementException;

FluentWait<WebDriver> fluentWait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(30))       // max wait time
    .pollingEvery(Duration.ofMillis(500))       // check every 500ms
    .ignoring(NoSuchElementException.class)     // ignore until timeout
    .ignoring(StaleElementReferenceException.class); // also ignore stale refs

// Use with lambda for custom conditions
WebElement element = fluentWait.until(driver -> {
    WebElement el = driver.findElement(By.id("dynamicData"));
    return el.getText().length() > 0 ? el : null;
    // Return null to keep waiting, return non-null to stop
});

// More complex custom condition
String result = fluentWait.until(driver -> {
    WebElement counter = driver.findElement(By.id("loadCount"));
    String text = counter.getText();
    return text.equals("100") ? text : null;
});

Custom Wait Utility

Create a reusable wait utility class:

// src/test/java/com/yourname/selenium/utils/WaitUtils.java
package com.yourname.selenium.utils;

import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;

public class WaitUtils {

    private static final int DEFAULT_TIMEOUT = 15;
    private static final int SHORT_TIMEOUT   = 5;
    private static final int LONG_TIMEOUT    = 30;

    private final WebDriver driver;
    private final WebDriverWait wait;
    private final WebDriverWait shortWait;
    private final WebDriverWait longWait;

    public WaitUtils(WebDriver driver) {
        this.driver    = driver;
        this.wait      = new WebDriverWait(driver, DEFAULT_TIMEOUT);
        this.shortWait = new WebDriverWait(driver, SHORT_TIMEOUT);
        this.longWait  = new WebDriverWait(driver, LONG_TIMEOUT);
    }

    public WebElement waitForVisible(By locator) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    public WebElement waitForClickable(By locator) {
        return wait.until(ExpectedConditions.elementToBeClickable(locator));
    }

    public void waitForInvisible(By locator) {
        wait.until(ExpectedConditions.invisibilityOfElementLocated(locator));
    }

    public void waitForUrl(String urlFragment) {
        wait.until(ExpectedConditions.urlContains(urlFragment));
    }

    public void waitForText(By locator, String text) {
        wait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
    }

    public void waitForPageLoad() {
        longWait.until(driver ->
            ((JavascriptExecutor) driver)
                .executeScript("return document.readyState")
                .equals("complete")
        );
    }

    public void waitForAjax() {
        longWait.until(driver ->
            (Boolean) ((JavascriptExecutor) driver)
                .executeScript("return jQuery.active == 0")
        );
    }

    public boolean isElementPresent(By locator) {
        try {
            shortWait.until(ExpectedConditions.presenceOfElementLocated(locator));
            return true;
        } catch (TimeoutException e) {
            return false;
        }
    }

    public void clickWhenReady(By locator) {
        waitForClickable(locator).click();
    }

    public void waitForLoadingSpinner(By spinnerLocator) {
        // First wait for spinner to appear (it might not appear immediately)
        try {
            shortWait.until(ExpectedConditions.visibilityOfElementLocated(spinnerLocator));
        } catch (TimeoutException ignored) {
            // Spinner appeared and disappeared too fast — that's fine
        }
        // Wait for spinner to disappear
        longWait.until(ExpectedConditions.invisibilityOfElementLocated(spinnerLocator));
    }
}

Use in tests:

public class LoginTest extends BaseTest {

    private WaitUtils waitUtils;

    @BeforeMethod
    @Override
    public void setUp() {
        super.setUp();
        waitUtils = new WaitUtils(getDriver());
    }

    @Test
    public void testLoginWithWaits() {
        WebDriver driver = getDriver();
        driver.get("https://app.example.com/login");

        // Type credentials
        waitUtils.waitForVisible(By.id("email")).sendKeys("user@example.com");
        driver.findElement(By.id("password")).sendKeys("password123");
        waitUtils.clickWhenReady(By.id("loginBtn"));

        // Wait for loading spinner to disappear
        waitUtils.waitForLoadingSpinner(By.cssSelector(".spinner"));

        // Wait for redirect
        waitUtils.waitForUrl("/dashboard");

        // Verify dashboard element
        WebElement greeting = waitUtils.waitForVisible(By.cssSelector(".user-greeting"));
        Assert.assertTrue(greeting.getText().contains("Welcome"));
    }
}

Wait Strategy Cheat Sheet

Situation Use
Simple pages, consistent timing Implicit wait (set once)
AJAX content loading Explicit wait + visibilityOfElementLocated
Button enabling after validation Explicit wait + elementToBeClickable
Loading spinner Explicit wait + invisibilityOfElementLocated
Page redirect Explicit wait + urlContains
Custom conditions Fluent wait with lambda
Never use Thread.sleep()

What's Next

In Part 7 we integrate TestNG fully — annotations, test groups, listeners, parallel execution configuration, and building a complete test suite runner with HTML reports.

Discussion

Loading...

Leave a Comment

All comments are reviewed before appearing. No links please.

0 / 1000