Theory is great, but let's look at the exact wait patterns you'll use in real projects. These are the scenarios that come up every day.
// Login → wait for spinner to disappear → read dashboard
driver.findElement(By.id("login-btn")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
// Step 1: Wait for spinner to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(
By.cssSelector(".loading-spinner")));
// Step 2: Now safely read the dashboard
WebElement balance = wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.id("account-balance")));
System.out.println("Balance: " + balance.getText());// Click login → wait for URL to change → verify dashboard
driver.findElement(By.id("login-btn")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// Wait for URL to change to dashboard
wait.until(ExpectedConditions.urlContains("/dashboard"));
// Now verify the page loaded
WebElement title = wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.id("dashboard-title")));
System.out.println("Page: " + title.getText());// Click "View Transactions" → wait for rows to appear
driver.findElement(By.id("view-transactions")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// Wait until at least one transaction row appears
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(
By.cssSelector(".transaction-row"), 0));
List<WebElement> rows = driver.findElements(
By.cssSelector(".transaction-row"));
System.out.println("Transactions: " + rows.size());// Problem: page re-renders after action, element reference is stale
WebElement price = driver.findElement(By.id("total-price"));
driver.findElement(By.id("apply-coupon")).click();
// price.getText(); // STALE! Page re-rendered, old reference is dead.
// Solution: Re-find the element after the action
driver.findElement(By.id("apply-coupon")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement updatedPrice = wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.id("total-price")));
String newPrice = updatedPrice.getText();
// Solution 2: FluentWait that ignores StaleElementReferenceException
Wait<WebDriver> fluentWait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(10))
.pollingEvery(Duration.ofMillis(500))
.ignoring(StaleElementReferenceException.class)
.ignoring(NoSuchElementException.class);
String price2 = fluentWait.until(d ->
d.findElement(By.id("total-price")).getText()
);// A promo popup might appear — close it if it does, ignore if it doesn't
try {
WebDriverWait shortWait = new WebDriverWait(driver,
Duration.ofSeconds(3));
WebElement popup = shortWait.until(
ExpectedConditions.visibilityOfElementLocated(
By.id("promo-popup")));
popup.findElement(By.cssSelector(".close-btn")).click();
} catch (TimeoutException e) {
// Popup didn't appear — that's fine, continue
}Q: How do you handle StaleElementReferenceException?
A: It happens when an element I previously found is no longer attached to the DOM — usually because the page re-rendered. Three solutions: (1) Re-find the element after the page changes instead of reusing old references. (2) Use FluentWait configured to ignore StaleElementReferenceException. (3) Find the element within the wait condition itself, so each poll gets a fresh reference. The best practice is approach 1 — simply re-find the element after the action that causes the re-render.
Key Point: Spinners: invisibilityOfElementLocated. Navigation: urlContains. AJAX: numberOfElementsToBeMoreThan. Stale elements: re-find after re-render.