You instinctively wait for the spinner to finish before checking the result. You don't even think about it.
Code has no instincts. Tell it exactly what to wait for, or it checks too early and fails randomly. Waits are the #1 difference between suites people trust and suites people ignore.
Modern pages load asynchronously: the HTML arrives, then JavaScript fetches data and builds the rest. Your code runs at machine speed and will happily assert on a page that isn't finished. The result is the flaky test — passes locally, fails in CI, passes on re-run — and it erodes the team's trust in automation faster than anything else.
Thread.sleep(5000); // wait 5 seconds. always. no matter what.
Thread.sleep() waits a fixed time: too short on a slow day (still flaky), too long on a fast day (a 200-test suite × 5s = 16 wasted minutes per run). It treats the symptom while making the suite slow. You'll see it in legacy suites; now you know why it's there and why to remove it.
WebDriverWait polls a condition every half-second and proceeds the moment it's true — failing only if a deadline passes. Fast when the app is fast, patient when it's slow:
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 until the Products header is visible, then use it — one motion: WebElement header = wait.until( ExpectedConditions.visibilityOfElementLocated(By.className("title"))); System.out.println("Header appeared: " + header.getText());
Header appeared: Products
The conditions you'll use weekly, all from ExpectedConditions:
visibilityOfElementLocated(by) — present and visible. Your default.elementToBeClickable(by) — before clicking things that enable late.textToBePresentInElementLocated(by, "text") — before asserting on text that updates.invisibilityOfElementLocated(by) — wait for a spinner to disappear.urlContains("inventory") — after navigation.org.openqa.selenium.TimeoutException: Expected condition failed: waiting for visibility of element located by By.className: totally-wrong-class (tried for 10 second(s) with 500 milliseconds interval)
Read it like a pro: the condition, the locator, how long it tried. Either the locator is wrong (check $$()), or the app genuinely never showed it (a real bug — congratulations).
Fix: Two possibilities, in order: the locator is wrong (verify in $$() — here it's a typo, 'finsh'), or the app genuinely never showed it — which is a real bug. The timeout message hands you both the condition and the locator.
Fix: You've mixed implicit and explicit waits; the official docs warn the combination causes unpredictable wait times. Delete the implicit wait. Convention: explicit only.
Fix: presenceOfElementLocated fires when the node exists in the DOM — possibly before it's visible or populated. You wanted visibilityOfElementLocated or textToBePresentInElementLocated.
Saucedemo is fast, so use the practice site built for this: the-internet.herokuapp.com/dynamic_loading/1 — a Start button reveals "Hello World!" after a 5-second spinner.
#start button, immediately getText() on #finish h4. Run it — you'll get an exception. Which one, and is it a Failure or an Error?visibilityOfElementLocated and assert the text equals "Hello World!".invisibilityOfElementLocated(By.id("loading")) first — the spinner pattern you'll use on every real app.