Waits: Implicit, Explicit & Fluent
If there is one topic that separates a reliable automation suite from a flaky one, it's waits. A browser page is not instant — JavaScript runs, AJAX calls fire, spinners spin, and elements attach to the DOM asynchronously. Selenium, on the other hand, executes commands as fast as your code lets it. If you ask for an element before the page has actually produced it, you get a NoSuchElementException or a click that lands on nothing. This lesson covers the three ways Selenium lets you wait for the browser to catch up: implicit wait, explicit wait, and fluent wait.
Why Waits Matter: The RecipeBox Spinner Problem
On RecipeBox's recipe list page, the recipe cards don't exist in the HTML when the page first loads. A spinner shows while an AJAX call fetches the recipes, and only once that call resolves do the .recipe-card elements get injected into the DOM. If your test calls driver.findElements(By.className("recipe-card")) immediately after driver.get(...), you'll usually get an empty list — not because there are no recipes, but because you asked too early.
1. Implicit Wait
An implicit wait is a global setting on the WebDriver instance. Once set, every findElement() / findElements() call will poll the DOM for up to the given duration before throwing NoSuchElementException.
WebDriver driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driver.get("https://recipebox.example.com/recipes");
// Selenium will silently retry finding this for up to 10 seconds
List<WebElement> cards = driver.findElements(By.className("recipe-card"));
- It only helps with presence in the DOM — not visibility, not "clickable", not text content.
- It applies globally to the driver, so you can't wait longer for one slow element without slowing down every other lookup too.
- It says nothing about the spinner disappearing — the cards could exist in the DOM while still hidden behind a loading overlay.
2. Explicit Wait (WebDriverWait + ExpectedConditions)
An explicit wait targets one specific condition on one specific element, and it's the tool you should reach for almost every time. It's built from a WebDriverWait object and a condition from ExpectedConditions.
WebDriver driver = new ChromeDriver();
driver.get("https://recipebox.example.com/recipes");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
// 1. Wait for the loading spinner to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loadingSpinner")));
// 2. Then wait for at least one recipe card to actually be visible
WebElement firstCard = wait.until(
ExpectedConditions.visibilityOfElementLocated(By.className("recipe-card"))
);
// 3. Assert once we know the list has really finished loading
List<WebElement> cards = driver.findElements(By.className("recipe-card"));
Assert.assertTrue(cards.size() > 0, "Expected at least one recipe card to load");
Common ExpectedConditions you'll use constantly:
| Condition | When to use it |
|---|---|
| visibilityOf(element) | Element exists and is displayed — e.g. a recipe card after the spinner clears |
| elementToBeClickable(locator) | Element is visible and enabled — e.g. the "Save Recipe" button before clicking it |
| invisibilityOfElementLocated(locator) | Waiting for a spinner/overlay to go away |
| textToBePresentInElement(el, text) | Waiting for a status message, e.g. "Recipe saved!" |
| presenceOfElementLocated(locator) | Element just needs to exist in the DOM (rare — usually you want visibility) |
3. Fluent Wait
FluentWait is WebDriverWait's more configurable cousin. It lets you set a custom polling interval (how often Selenium re-checks the condition) and tell it which exceptions to swallow while it's still polling — useful when an element is being re-rendered and briefly throws StaleElementReferenceException mid-poll.
Wait<WebDriver> fluentWait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(20))
.pollingEvery(Duration.ofMillis(500))
.ignoring(NoSuchElementException.class)
.ignoring(StaleElementReferenceException.class);
WebElement firstCard = fluentWait.until(driver1 ->
driver1.findElement(By.className("recipe-card"))
);
Never Mix Implicit and Explicit Waits
Selenium's own documentation warns against combining implicit and explicit waits on the same driver. When both are configured, the effective wait time becomes unpredictable — the implicit wait's polling can interact badly with an explicit wait's own polling, sometimes summing delays, sometimes causing a wait to appear to take far longer than either timeout alone. This is a well-known source of hard-to-diagnose flaky/slow tests.
| Recommendation | Why |
|---|---|
| Prefer explicit waits everywhere | Targets the exact condition you actually care about, on the exact element you care about |
| Avoid implicitlyWait() entirely in new frameworks | Global, coarse, and dangerous when mixed with explicit waits |
| Never use Thread.sleep() as your primary wait strategy | It's either too short (still flaky) or too long (slow suite) — it never actually reacts to the app's real state |
Worked Example: Waiting on RecipeBox's Recipe List
public class RecipeListWaitTest {
private WebDriver driver;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
driver.get("https://recipebox.example.com/recipes");
}
@Test
public void recipeListLoadsAfterSpinner() {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
// Wait for the AJAX spinner to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loadingSpinner")));
// Wait until at least one recipe card is visible before asserting
wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("recipe-card")));
List<WebElement> cards = driver.findElements(By.className("recipe-card"));
Assert.assertTrue(cards.size() > 0, "Recipe list should not be empty after loading");
}
@AfterMethod
public void tearDown() {
if (driver != null) driver.quit();
}
}