Common Synchronisation Issues and Solutions

8 min read

You now have the tools — WebDriverWait, ExpectedConditions, FluentWait. This lesson is the map of failures you'll see in real codebases and the wait pattern that solves each. Six categories cover ~95% of timing-related test failures: StaleElementReferenceException, ElementClickInterceptedException, "exists but not interactable," dynamic content, SPA navigation, and animations. Each has a specific shape, and once you recognise the shape, the fix is mechanical.

1. StaleElementReferenceException

You found an element. The page re-rendered. Your reference now points to an element that no longer exists in the DOM:

// Problem
WebElement saveButton = driver.findElement(By.id("save"));
// ... AJAX call updates the form ...
saveButton.click();   // → StaleElementReferenceException

The fix: don't hold WebElement references across DOM mutations. Find immediately before use:

// Solution — find fresh, click immediately
wait.until(ExpectedConditions.elementToBeClickable(By.id("save"))).click();

If the element is in a list you're iterating, reload the list on each iteration rather than caching the loop variable:

// Bad — list captured once, references go stale on re-render
List<WebElement> rows = driver.findElements(By.cssSelector("tr"));
for (WebElement row : rows) {
    row.findElement(By.cssSelector(".delete-btn")).click();
    // re-render after delete → next iteration references a stale row
}
 
// Good — count first, re-find each iteration
int rowCount = driver.findElements(By.cssSelector("tr")).size();
for (int i = 0; i < rowCount; i++) {
    driver.findElements(By.cssSelector("tr"))
          .get(0)
          .findElement(By.cssSelector(".delete-btn"))
          .click();
    wait.until(ExpectedConditions.numberOfElementsToBe(By.cssSelector("tr"), rowCount - i - 1));
}

2. ElementClickInterceptedException

The element exists, is visible, should be clickable — but another element is on top of it. A modal, a sticky header, a loading spinner that lingers half a beat too long:

ElementClickInterceptedException: Other element would receive the click:
<div class="loading-overlay">

The fix: wait for the obstructing element to go away first.

// Wait for the spinner/overlay to vanish, then click
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loading-overlay")));
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit"))).click();

The interception message tells you which element is blocking the click — read it. The fix is almost always a wait for that specific overlay's invisibility, not a wait for the click target's clickability (which is already true).

3. "Exists but not interactable"

ElementNotInteractableException — present in DOM but disabled, hidden by CSS (display: none), or rendered with zero size. presenceOfElementLocated happily returns it; the click then fails:

// Anti-pattern — presence isn't enough
wait.until(ExpectedConditions.presenceOfElementLocated(By.id("submit"))).click();
 
// Fix — elementToBeClickable confirms visible AND enabled
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit"))).click();

The rule: whenever the next call is a .click(), wait for elementToBeClickable, not visibilityOfElementLocated (and definitely not presenceOfElementLocated).

4. Dynamic content loading

An AJAX call updates a part of the page — the URL doesn't change, no full page navigation, but the element you care about isn't there yet:

// Click triggers a server call that fills in the order status
driver.findElement(By.id("place-order")).click();
 
// Wait for the SPECIFIC text to appear — not just "the page is loaded"
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.id("order-status"), "Confirmed"
));

textToBePresentInElementLocated is the right tool when the element exists immediately but its content fills in asynchronously. The number of suites that incorrectly wait for the element (which is already present) and then read empty text is large.

5. SPA page transitions

Single-page apps change the URL via history.pushState without a full reload. The URL flips immediately; the new page's content arrives milliseconds later. Wait for both:

// Click triggers SPA navigation to /dashboard
driver.findElement(By.id("dashboard-link")).click();
 
// Wait for URL change AND for the destination's main content
wait.until(ExpectedConditions.and(
    ExpectedConditions.urlContains("/dashboard"),
    ExpectedConditions.visibilityOfElementLocated(By.id("dashboard-widget"))
));

Waiting only for the URL is the classic SPA pitfall — the URL changes a beat before React has rendered the new page. Pair the URL check with a content check, every time.

6. Animations

The element is "visible" by every measure, but it's still moving — sliding in from the side, fading from 50% to 100% opacity. Click during the animation and the click can land in the wrong place:

// Wait for the modal to be visible AND in its final geometric position
wait.until(driver -> {
    WebElement modal = driver.findElement(By.id("modal"));
    return modal.isDisplayed() && modal.getLocation().getY() > 100;
});
 
// Better — wait for the CSS transition to be over
wait.until(driver -> {
    WebElement modal = driver.findElement(By.id("modal"));
    String opacity = modal.getCssValue("opacity");
    return modal.isDisplayed() && "1".equals(opacity);
});

A generic alternative that works almost everywhere: use the reduced-motion CSS preference by setting browser options to disable animations during tests. Cleaner than waiting for them to finish.

The decision tree

The cheat-sheet rule: read the exception name, walk the tree to the wait that fixes it.

A complete sync-aware test

Putting every pattern from this lesson into one test against Sauce Demo:

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
import java.time.Duration;
 
public class SyncIssuesTest {
 
    WebDriver driver;
    WebDriverWait wait;
 
    @BeforeMethod
    public void setup() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        driver.get("https://www.saucedemo.com");
 
        // Robust login — wait for clickable on every interactive element
        wait.until(ExpectedConditions.elementToBeClickable(By.id("user-name")))
            .sendKeys("standard_user");
        driver.findElement(By.id("password")).sendKeys("secret_sauce");
        driver.findElement(By.id("login-button")).click();
    }
 
    @Test
    public void shouldHandleSpaNavigationToCart() {
        // Add an item — page re-renders the badge
        wait.until(ExpectedConditions.elementToBeClickable(
            By.id("add-to-cart-sauce-labs-backpack")
        )).click();
 
        // Wait for the badge to update — content change without URL change
        wait.until(ExpectedConditions.textToBePresentInElementLocated(
            By.cssSelector(".shopping_cart_badge"), "1"
        ));
 
        // Navigate to cart — URL changes, then content renders
        driver.findElement(By.cssSelector(".shopping_cart_link")).click();
 
        wait.until(ExpectedConditions.and(
            ExpectedConditions.urlContains("/cart.html"),
            ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".cart_item"))
        ));
 
        Assert.assertEquals(
            driver.findElements(By.cssSelector(".cart_item")).size(),
            1
        );
    }
 
    @Test
    public void shouldRefindAcrossReRenders() {
        // Add to cart, remove, add again — the button text/locator changes between Add and Remove
        // Find fresh each time to avoid staleness
        wait.until(ExpectedConditions.elementToBeClickable(
            By.id("add-to-cart-sauce-labs-bike-light")
        )).click();
 
        wait.until(ExpectedConditions.elementToBeClickable(
            By.id("remove-sauce-labs-bike-light")    // re-fetch — new id after add
        )).click();
 
        wait.until(ExpectedConditions.elementToBeClickable(
            By.id("add-to-cart-sauce-labs-bike-light")    // re-fetch — original id is back
        )).click();
 
        wait.until(ExpectedConditions.textToBePresentInElementLocated(
            By.cssSelector(".shopping_cart_badge"), "1"
        ));
    }
 
    @AfterMethod
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

Two tests, four sync patterns: SPA navigation with and(...), content-only update with textToBePresentInElementLocated, repeated re-finding to avoid staleness, and elementToBeClickable for safe clicks.

The full reference for every condition lives on the Selenium tool entry, and the TestNG cheat sheet covers the assertion side.

⚠️ Common mistakes

  • Catching StaleElementReferenceException and retrying. A try/catch with retry hides a design problem — the reference shouldn't be held across DOM mutations in the first place. Find immediately before use; the issue disappears, no retry needed.
  • Using Thread.sleep to "give animations time to finish." It works locally and fails on CI under load. Either disable animations via browser options (--disable-animations in some chrome flag set, or set * CSS animations to 0s), or wait on a stable property like opacity or final geometric position.
  • Treating every flake as "needs more wait time." Bumping every timeout to 30 seconds slows the suite without fixing the root cause. The right fix is almost always a different condition (clickable instead of visible, text-present instead of element-present), not a longer timeout on the wrong condition.

🎯 Practice task

Hunt and fix sync issues. 35–45 minutes.

  1. Add SyncIssuesTest from this lesson to your project. Run both tests; both should pass.
  2. Cause each exception on purpose. Write four short failing tests:
    • causeStaleElement — find the cart link before login, log in, then click the cached reference.
    • causeClickIntercepted — open Sauce Demo's burger menu and click an underlying inventory button while the menu overlay is open.
    • causeNotInteractable — find a hidden element (any element with display: none via CSS) and click it.
    • causeNoSuchElement — find an element by an ID that doesn't exist. Read each exception. Then fix each one with the wait pattern from the lesson.
  3. Refactor for reuse. Notice you used elementToBeClickable(...).click() four times in your sync tests. Add a helper to WaitHelpers:
    public static void clickWhenReady(WebDriver driver, By locator) {
        new WebDriverWait(driver, Duration.ofSeconds(10))
            .until(ExpectedConditions.elementToBeClickable(locator))
            .click();
    }
    Use it across your test classes. Test code shrinks; reliability stays the same.
  4. Time the savings. Measure how long SyncIssuesTest takes with your wait patterns vs the same logic with Thread.sleep(2000) everywhere. The wait version finishes in ~80% of the time, on average — and never flakes.
  5. Stretch — animation handling. Add Chrome options that disable animations:
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--disable-animations");
    options.addArguments("--no-default-browser-check");
    // or via prefs:
    options.addArguments("--force-prefers-reduced-motion");
    Run a test against any animation-heavy page (Material Design demos, GitHub PRs). Tests should be measurably faster and never race a transition.

Chapter 3 is done. Synchronisation is the single biggest source of Selenium flake; if you've built the muscle memory in this chapter, your suites are already in the top 20% by reliability. Chapter 4 covers the advanced interactions — hover, drag-and-drop, alerts, iframes, multiple windows — every one of which depends on the wait patterns you've just learned.

// tip to track lessons you complete and pick up where you left off across devices.