Stable Locators and Smart Waits

Locator Stability Ladder

Prefer locators that developers have no reason to change. Agree with the developers on data-testid attributes for important elements.

<button id="add-to-cart" data-testid="add-to-cart" class="btn btn-primary mt-2 css-1x8k2">
  Add to Cart
</button>
LocatorStability
By.cssSelector("[data-testid='add-to-cart']")best: exists only for tests
By.id("add-to-cart")very good
By.cssSelector("button.btn-primary")medium: styling classes change
By.xpath("//button[text()='Add to Cart']")medium: text changes, translations
By.xpath("/html/body/div[3]/div[2]/button")worst: breaks on any layout change
.css-1x8k2never: generated class names change every build

Wait for the Right Condition

// Bad: fixed sleep, slow and still flaky
Thread.sleep(3000);
driver.findElement(By.id("checkout")).click();

// Good: wait for the spinner to go away, then for the button to be clickable
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".loading-spinner")));
wait.until(ExpectedConditions.elementToBeClickable(By.id("checkout"))).click();

// Good: wait for the data, not just the element
wait.until(ExpectedConditions.textToBe(By.id("cart-count"), "2"));

Tip: Wait for what the user waits for: the spinner gone, the count updated, the URL changed (urlContains("/orders/")). Waiting only for "element present" is the cause of many flaky clicks.

Handling Stale Elements

// The product list re-renders after sorting; old WebElement references become stale.
public List<String> productNamesAfterSort(String option) {
    new Select(driver.findElement(By.id("sort"))).selectByVisibleText(option);
    wait.until(ExpectedConditions.stalenessOf(driver.findElement(By.cssSelector(".product-card"))));
    return driver.findElements(By.cssSelector(".product-card .name"))   // find again
            .stream().map(WebElement::getText).toList();
}