Blog

How to Handle Dynamic Elements in Selenium

Rishabh Kumar

Published on

Handle Dynamic Elements in Selenium

Summarise this article with AI:

ChatGPT

Perplexity

Claude

Grok

Google AI

Summarise this article with AI:

ChatGPT

Perplexity

Claude

Grok

Google AI

Summarise this article with AI:

ChatGPT

Perplexity

Claude

Grok

Google AI

Table of contents

Lorem ipsum

Lorem ipsum

Lorem ipsum

Lorem ipsum

Lorem ipsum

Modern web applications generate element attributes dynamically. IDs change with each page load, classes include generated hashes, content loads asynchronously, and lists grow and shrink as users act. These behaviours break Selenium locators that expect static, predictable elements.

Four kinds of dynamic elements



Understanding Dynamic Elements

What Makes Elements Dynamic

Dynamic elements have attributes or behaviors that change without explicit user action or between sessions:

Type

What Changes

Example

Dynamic IDs

The ID is regenerated on every page load

email_8f3k2 on one load, email_x91qa on the next

Dynamic classes

CSS Modules and CSS-in-JS libraries add hashes to class names at build time

Button_primary__a7Xk2 changes in the next build

Dynamic content

The element exists before its content arrives from an API

An empty results panel that fills a second later

Dynamic lists

The number, order and content of items change

Search results, notifications and live feeds

Here’s what dynamic IDs and classes look like in the DOM:

<!-- Page load 1 -->
<input id="email_8f3k2" class="Input_field__x7Kq2" type="email" name="email" placeholder="Email address">

<!-- Page load 2: same field, new values -->
<input id="email_x91qa" class="Input_field__p2Lm9" type="email" name="email" placeholder="Email address">
<!-- Page load 1 -->
<input id="email_8f3k2" class="Input_field__x7Kq2" type="email" name="email" placeholder="Email address">

<!-- Page load 2: same field, new values -->
<input id="email_x91qa" class="Input_field__p2Lm9" type="email" name="email" placeholder="Email address">
<!-- Page load 1 -->
<input id="email_8f3k2" class="Input_field__x7Kq2" type="email" name="email" placeholder="Email address">

<!-- Page load 2: same field, new values -->
<input id="email_x91qa" class="Input_field__p2Lm9" type="email" name="email" placeholder="Email address">
<!-- Page load 1 -->
<input id="email_8f3k2" class="Input_field__x7Kq2" type="email" name="email" placeholder="Email address">

<!-- Page load 2: same field, new values -->
<input id="email_x91qa" class="Input_field__p2Lm9" type="email" name="email" placeholder="Email address">

Why Applications Use Dynamic Elements

Dynamic attributes aren’t bugs. They serve real engineering purposes, and understanding them helps you choose the right strategy.

Reason

What It Does

Unique IDs

Frameworks generate IDs to prevent conflicts between components

Scoped CSS

CSS Modules and CSS-in-JS add hashes to stop styles clashing

Security

Some applications randomise IDs to discourage scraping or manipulation

State

Classes change to show the current state, such as active, disabled or selected

The Challenge for Test Automation

Static locators work when attribute values never change. Once an application generates attributes dynamically, every locator that depends on those values becomes a maintenance problem.

Both of these locators will fail the next time the page loads with new values:

driver.findElement(By.id("email_8f3k2")).sendKeys("jane@example.com");

driver.findElement(By.cssSelector(".Button_primary__a7Xk2")).click();
driver.findElement(By.id("email_8f3k2")).sendKeys("jane@example.com");

driver.findElement(By.cssSelector(".Button_primary__a7Xk2")).click();
driver.findElement(By.id("email_8f3k2")).sendKeys("jane@example.com");

driver.findElement(By.cssSelector(".Button_primary__a7Xk2")).click();
driver.findElement(By.id("email_8f3k2")).sendKeys("jane@example.com");

driver.findElement(By.cssSelector(".Button_primary__a7Xk2")).click();

Every locator that depends on dynamic values eventually breaks. The question is when, not whether.



Strategies for Dynamic IDs and Classes

None of these strategies removes dynamic attributes. They make locators resilient to the specific kind of change you’re dealing with, so the right choice depends on which part of the element stays stable.

Strategy

Use It When

Partial attribute matching

Part of the ID or class stays the same

Stable alternative attributes

The element has a name, data-testid, ARIA or form attribute

Text content

The element has a visible, stable label

Relative positioning

A nearby element has stable attributes

Combined attributes

A single condition matches more than one element

‍Strategy 1: Use Partial Attribute Matching

When an ID or class always contains a predictable piece, target that piece instead of the full value.

XPath contains()

// Matches id="email_8f3k2", id="email_x91qa" and so on
driver.findElement(By.xpath("//input[contains(@id, 'email_')]"));

// Matches a hashed class name by its stable part
driver.findElement(By.xpath("//button[contains(@class, 'Button_primary')]"));
// Matches id="email_8f3k2", id="email_x91qa" and so on
driver.findElement(By.xpath("//input[contains(@id, 'email_')]"));

// Matches a hashed class name by its stable part
driver.findElement(By.xpath("//button[contains(@class, 'Button_primary')]"));
// Matches id="email_8f3k2", id="email_x91qa" and so on
driver.findElement(By.xpath("//input[contains(@id, 'email_')]"));

// Matches a hashed class name by its stable part
driver.findElement(By.xpath("//button[contains(@class, 'Button_primary')]"));
// Matches id="email_8f3k2", id="email_x91qa" and so on
driver.findElement(By.xpath("//input[contains(@id, 'email_')]"));

// Matches a hashed class name by its stable part
driver.findElement(By.xpath("//button[contains(@class, 'Button_primary')]"));

XPath starts-with()

When the dynamic part is always at the end, anchoring to the start is more precise than contains():

driver.findElement(By.xpath("//input[starts-with(@id, 'email_')]"));
driver.findElement(By.xpath("//input[starts-with(@id, 'email_')]"));
driver.findElement(By.xpath("//input[starts-with(@id, 'email_')]"));
driver.findElement(By.xpath("//input[starts-with(@id, 'email_')]"));

Browsers support XPath 1.0, which has no ends-with() function. When the stable part is at the end of the value, use a CSS selector instead.

CSS Partial Matching

// Value starts with "email_"
driver.findElement(By.cssSelector("input[id^='email_']"));

// Value ends with "_submit"
driver.findElement(By.cssSelector("button[id$='_submit']"));

// Value contains "Button_primary" anywhere
driver.findElement(By.cssSelector("button[class*='Button_primary']"));
// Value starts with "email_"
driver.findElement(By.cssSelector("input[id^='email_']"));

// Value ends with "_submit"
driver.findElement(By.cssSelector("button[id$='_submit']"));

// Value contains "Button_primary" anywhere
driver.findElement(By.cssSelector("button[class*='Button_primary']"));
// Value starts with "email_"
driver.findElement(By.cssSelector("input[id^='email_']"));

// Value ends with "_submit"
driver.findElement(By.cssSelector("button[id$='_submit']"));

// Value contains "Button_primary" anywhere
driver.findElement(By.cssSelector("button[class*='Button_primary']"));
// Value starts with "email_"
driver.findElement(By.cssSelector("input[id^='email_']"));

// Value ends with "_submit"
driver.findElement(By.cssSelector("button[id$='_submit']"));

// Value contains "Button_primary" anywhere
driver.findElement(By.cssSelector("button[class*='Button_primary']"));

Choose the operator by where the stable part sits: ^= for the start, $= for the end and *= for anywhere.


Strategy 2: Use Stable Alternative Attributes

When IDs and classes are fully dynamic, use attributes developers don’t regenerate. These usually serve functional or accessibility purposes, so they stay stable across page loads and releases.

Name Attribute

Form fields almost always have a name that reflects their purpose and rarely changes:

driver.findElement(By.name("email"));
driver.findElement(By.name("email"));
driver.findElement(By.name("email"));
driver.findElement(By.name("email"));

Data Attributes

data-* attributes exist for non-presentational metadata, and the data-testid convention gives automation a stable hook that’s independent of styling:

driver.findElement(By.cssSelector("[data-testid='checkout-submit']"));
driver.findElement(By.cssSelector("[data-testid='checkout-submit']"));
driver.findElement(By.cssSelector("[data-testid='checkout-submit']"));
driver.findElement(By.cssSelector("[data-testid='checkout-submit']"));

ARIA Attributes

Accessibility attributes describe what an element does, so they change only when its purpose changes:

driver.findElement(By.cssSelector("button[aria-label='Close dialog']"));
driver.findElement(By.cssSelector("[role='tab'][aria-controls='billing-panel']"));
driver.findElement(By.cssSelector("button[aria-label='Close dialog']"));
driver.findElement(By.cssSelector("[role='tab'][aria-controls='billing-panel']"));
driver.findElement(By.cssSelector("button[aria-label='Close dialog']"));
driver.findElement(By.cssSelector("[role='tab'][aria-controls='billing-panel']"));
driver.findElement(By.cssSelector("button[aria-label='Close dialog']"));
driver.findElement(By.cssSelector("[role='tab'][aria-controls='billing-panel']"));

Form Attributes

Input types and placeholders communicate purpose to users and browsers, so they tend to stay stable:

driver.findElement(By.cssSelector("input[type='email']"));
driver.findElement(By.cssSelector("input[placeholder='Email address']"));
driver.findElement(By.cssSelector("input[type='email']"));
driver.findElement(By.cssSelector("input[placeholder='Email address']"));
driver.findElement(By.cssSelector("input[type='email']"));
driver.findElement(By.cssSelector("input[placeholder='Email address']"));
driver.findElement(By.cssSelector("input[type='email']"));
driver.findElement(By.cssSelector("input[placeholder='Email address']"));


Strategy 3: Use Text Content

Visible text is often the most stable thing on a page. Button labels, link text and headings change when the product’s wording changes, which happens far less often than IDs and class names.

Link Text

// Exact visible text
driver.findElement(By.linkText("Forgot password?"));

// Part of the visible text
driver.findElement(By.partialLinkText("Forgot"));
// Exact visible text
driver.findElement(By.linkText("Forgot password?"));

// Part of the visible text
driver.findElement(By.partialLinkText("Forgot"));
// Exact visible text
driver.findElement(By.linkText("Forgot password?"));

// Part of the visible text
driver.findElement(By.partialLinkText("Forgot"));
// Exact visible text
driver.findElement(By.linkText("Forgot password?"));

// Part of the visible text
driver.findElement(By.partialLinkText("Forgot"));

XPath Text Matching

// Exact text
driver.findElement(By.xpath("//button[text()='Place order']"));

// Text that contains a phrase
driver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));

// Ignores extra spaces and line breaks, and matches text in child elements
driver.findElement(By.xpath("//button[normalize-space()='Place order']"));
// Exact text
driver.findElement(By.xpath("//button[text()='Place order']"));

// Text that contains a phrase
driver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));

// Ignores extra spaces and line breaks, and matches text in child elements
driver.findElement(By.xpath("//button[normalize-space()='Place order']"));
// Exact text
driver.findElement(By.xpath("//button[text()='Place order']"));

// Text that contains a phrase
driver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));

// Ignores extra spaces and line breaks, and matches text in child elements
driver.findElement(By.xpath("//button[normalize-space()='Place order']"));
// Exact text
driver.findElement(By.xpath("//button[text()='Place order']"));

// Text that contains a phrase
driver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));

// Ignores extra spaces and line breaks, and matches text in child elements
driver.findElement(By.xpath("//button[normalize-space()='Place order']"));

normalize-space() is usually the safest choice, because it handles wrapped text and inconsistent spacing.


Strategy 4: Use Relative Positioning

When an element has no stable attributes, locate it from a nearby element that does. This works well for fields next to labels, buttons inside known containers and elements in well-defined page regions.

XPath Axes

// The input that follows the "Email address" label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));

// The input inside the same form group as the label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/ancestor::div[contains(@class, 'form-group')]//input"));

// The "Remove" button in the row that contains "Wireless Keyboard"
driver.findElement(By.xpath("//tr[td[normalize-space()='Wireless Keyboard']]//button[normalize-space()='Remove']"));
// The input that follows the "Email address" label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));

// The input inside the same form group as the label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/ancestor::div[contains(@class, 'form-group')]//input"));

// The "Remove" button in the row that contains "Wireless Keyboard"
driver.findElement(By.xpath("//tr[td[normalize-space()='Wireless Keyboard']]//button[normalize-space()='Remove']"));
// The input that follows the "Email address" label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));

// The input inside the same form group as the label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/ancestor::div[contains(@class, 'form-group')]//input"));

// The "Remove" button in the row that contains "Wireless Keyboard"
driver.findElement(By.xpath("//tr[td[normalize-space()='Wireless Keyboard']]//button[normalize-space()='Remove']"));
// The input that follows the "Email address" label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));

// The input inside the same form group as the label
driver.findElement(By.xpath("//label[normalize-space()='Email address']/ancestor::div[contains(@class, 'form-group')]//input"));

// The "Remove" button in the row that contains "Wireless Keyboard"
driver.findElement(By.xpath("//tr[td[normalize-space()='Wireless Keyboard']]//button[normalize-space()='Remove']"));

CSS Combinators

// A direct child
driver.findElement(By.cssSelector("form#checkout > button[type='submit']"));

// Any descendant
driver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));

// The input immediately after a label
driver.findElement(By.cssSelector("label[for='email'] + input"));
// A direct child
driver.findElement(By.cssSelector("form#checkout > button[type='submit']"));

// Any descendant
driver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));

// The input immediately after a label
driver.findElement(By.cssSelector("label[for='email'] + input"));
// A direct child
driver.findElement(By.cssSelector("form#checkout > button[type='submit']"));

// Any descendant
driver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));

// The input immediately after a label
driver.findElement(By.cssSelector("label[for='email'] + input"));
// A direct child
driver.findElement(By.cssSelector("form#checkout > button[type='submit']"));

// Any descendant
driver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));

// The input immediately after a label
driver.findElement(By.cssSelector("label[for='email'] + input"));

Selenium 4 Relative Locators

Selenium 4 also lets you find elements by their position on screen relative to another element:

import static org.openqa.selenium.support.locators.RelativeLocator.with;

WebElement emailLabel = driver.findElement(By.xpath("//label[normalize-space()='Email address']"));
WebElement emailInput = driver.findElement(with(By.tagName("input")).below(emailLabel));
import static org.openqa.selenium.support.locators.RelativeLocator.with;

WebElement emailLabel = driver.findElement(By.xpath("//label[normalize-space()='Email address']"));
WebElement emailInput = driver.findElement(with(By.tagName("input")).below(emailLabel));
import static org.openqa.selenium.support.locators.RelativeLocator.with;

WebElement emailLabel = driver.findElement(By.xpath("//label[normalize-space()='Email address']"));
WebElement emailInput = driver.findElement(with(By.tagName("input")).below(emailLabel));
import static org.openqa.selenium.support.locators.RelativeLocator.with;

WebElement emailLabel = driver.findElement(By.xpath("//label[normalize-space()='Email address']"));
WebElement emailInput = driver.findElement(with(By.tagName("input")).below(emailLabel));


Strategy 5: Combine Multiple Attributes

When one condition matches several elements, combine conditions to narrow the result to one. This is the most precise manual strategy, and the least likely to match the wrong element.

// XPath: a submit button with a stable class fragment and specific text
driver.findElement(By.xpath(
    "//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"
));

// CSS: an email input inside the billing form
driver.findElement(By.cssSelector("form[data-testid='billing-form'] input[type='email'][name='email']"));
// XPath: a submit button with a stable class fragment and specific text
driver.findElement(By.xpath(
    "//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"
));

// CSS: an email input inside the billing form
driver.findElement(By.cssSelector("form[data-testid='billing-form'] input[type='email'][name='email']"));
// XPath: a submit button with a stable class fragment and specific text
driver.findElement(By.xpath(
    "//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"
));

// CSS: an email input inside the billing form
driver.findElement(By.cssSelector("form[data-testid='billing-form'] input[type='email'][name='email']"));
// XPath: a submit button with a stable class fragment and specific text
driver.findElement(By.xpath(
    "//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"
));

// CSS: an email input inside the billing form
driver.findElement(By.cssSelector("form[data-testid='billing-form'] input[type='email'][name='email']"));



Handling AJAX and Asynchronously Loaded Elements‍

AJAX content adds a timing problem that locator strategies can’t solve on their own. An element may be easy to identify once it exists, but if the test looks for it before the API responds, it isn’t there yet.

The Timing Challenge

The page load event fires when the initial HTML has been parsed. AJAX requests fire afterwards, so any element that depends on an API response is missing from the DOM between page load and the response arriving.

Here’s what that failure looks like:

driver.get("https://shop.example.com/search?q=keyboard");

// The results haven't loaded yet, so this throws NoSuchElementException
WebElement firstResult = driver.findElement(By.cssSelector("[data-testid='search-result']"));
driver.get("https://shop.example.com/search?q=keyboard");

// The results haven't loaded yet, so this throws NoSuchElementException
WebElement firstResult = driver.findElement(By.cssSelector("[data-testid='search-result']"));
driver.get("https://shop.example.com/search?q=keyboard");

// The results haven't loaded yet, so this throws NoSuchElementException
WebElement firstResult = driver.findElement(By.cssSelector("[data-testid='search-result']"));
driver.get("https://shop.example.com/search?q=keyboard");

// The results haven't loaded yet, so this throws NoSuchElementException
WebElement firstResult = driver.findElement(By.cssSelector("[data-testid='search-result']"));

Wait for the Element to Be Present

Instead of assuming the element exists, tell Selenium to wait until it appears. Explicit waits check repeatedly until the condition is met or the timeout runs out.

import java.time.Duration;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement firstResult = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.cssSelector("[data-testid='search-result']"))
);
import java.time.Duration;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement firstResult = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.cssSelector("[data-testid='search-result']"))
);
import java.time.Duration;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement firstResult = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.cssSelector("[data-testid='search-result']"))
);
import java.time.Duration;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement firstResult = wait.until(
    ExpectedConditions.presenceOfElementLocated(By.cssSelector("[data-testid='search-result']"))
);

Wait for the Element to Be Visible

An element in the DOM isn’t necessarily visible. Some elements load hidden and appear only after further processing.

WebElement banner = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-testid='promo-banner']"))
);
WebElement banner = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-testid='promo-banner']"))
);
WebElement banner = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-testid='promo-banner']"))
);
WebElement banner = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-testid='promo-banner']"))
);

Wait for the Element to Be Clickable

Buttons and links can be visible but disabled while a form validates or a request processes. A clickability wait checks the element is visible and enabled.

wait.until(ExpectedConditions.elementToBeClickable(
    By.cssSelector("[data-testid='checkout-submit']")
)).click();
wait.until(ExpectedConditions.elementToBeClickable(
    By.cssSelector("[data-testid='checkout-submit']")
)).click();
wait.until(ExpectedConditions.elementToBeClickable(
    By.cssSelector("[data-testid='checkout-submit']")
)).click();
wait.until(ExpectedConditions.elementToBeClickable(
    By.cssSelector("[data-testid='checkout-submit']")
)).click();

Wait for Text Content

Some containers appear straight away, empty, and fill when data arrives. Waiting for the element isn’t enough here, so wait for its content.

// Wait for specific text
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.cssSelector("[data-testid='order-status']"), "Confirmed"
));

// Wait for any text at all
wait.until(driver -> !driver.findElement(By.cssSelector("[data-testid='results-count']"))
    .getText().trim().isEmpty());
// Wait for specific text
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.cssSelector("[data-testid='order-status']"), "Confirmed"
));

// Wait for any text at all
wait.until(driver -> !driver.findElement(By.cssSelector("[data-testid='results-count']"))
    .getText().trim().isEmpty());
// Wait for specific text
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.cssSelector("[data-testid='order-status']"), "Confirmed"
));

// Wait for any text at all
wait.until(driver -> !driver.findElement(By.cssSelector("[data-testid='results-count']"))
    .getText().trim().isEmpty());
// Wait for specific text
wait.until(ExpectedConditions.textToBePresentInElementLocated(
    By.cssSelector("[data-testid='order-status']"), "Confirmed"
));

// Wait for any text at all
wait.until(driver -> !driver.findElement(By.cssSelector("[data-testid='results-count']"))
    .getText().trim().isEmpty());

Wait for Loading Indicators to Disappear

When an application shows a spinner or overlay during AJAX calls, its disappearance tells you the data has arrived.

wait.until(ExpectedConditions.invisibilityOfElementLocated(
    By.cssSelector(".loading-spinner")
));
wait.until(ExpectedConditions.invisibilityOfElementLocated(
    By.cssSelector(".loading-spinner")
));
wait.until(ExpectedConditions.invisibilityOfElementLocated(
    By.cssSelector(".loading-spinner")
));
wait.until(ExpectedConditions.invisibilityOfElementLocated(
    By.cssSelector(".loading-spinner")
));

Custom Wait Conditions

Standard conditions cover common situations, but not application-specific ones. When you know what your application uses to signal readiness, wait on that signal directly.

import org.openqa.selenium.JavascriptExecutor;

// The document has finished loading
wait.until(d -> ((JavascriptExecutor) d)
    .executeScript("return document.readyState").equals("complete"));

// jQuery has no active requests
wait.until(d -> (Boolean) ((JavascriptExecutor) d)
    .executeScript("return window.jQuery !== undefined && jQuery.active === 0"));

// Angular reports that it's stable
wait.until(d -> (Boolean) ((JavascriptExecutor) d).executeScript(
    "return window.getAllAngularTestabilities !== undefined && " +
    "window.getAllAngularTestabilities().every(t => t.isStable())"));
import org.openqa.selenium.JavascriptExecutor;

// The document has finished loading
wait.until(d -> ((JavascriptExecutor) d)
    .executeScript("return document.readyState").equals("complete"));

// jQuery has no active requests
wait.until(d -> (Boolean) ((JavascriptExecutor) d)
    .executeScript("return window.jQuery !== undefined && jQuery.active === 0"));

// Angular reports that it's stable
wait.until(d -> (Boolean) ((JavascriptExecutor) d).executeScript(
    "return window.getAllAngularTestabilities !== undefined && " +
    "window.getAllAngularTestabilities().every(t => t.isStable())"));
import org.openqa.selenium.JavascriptExecutor;

// The document has finished loading
wait.until(d -> ((JavascriptExecutor) d)
    .executeScript("return document.readyState").equals("complete"));

// jQuery has no active requests
wait.until(d -> (Boolean) ((JavascriptExecutor) d)
    .executeScript("return window.jQuery !== undefined && jQuery.active === 0"));

// Angular reports that it's stable
wait.until(d -> (Boolean) ((JavascriptExecutor) d).executeScript(
    "return window.getAllAngularTestabilities !== undefined && " +
    "window.getAllAngularTestabilities().every(t => t.isStable())"));
import org.openqa.selenium.JavascriptExecutor;

// The document has finished loading
wait.until(d -> ((JavascriptExecutor) d)
    .executeScript("return document.readyState").equals("complete"));

// jQuery has no active requests
wait.until(d -> (Boolean) ((JavascriptExecutor) d)
    .executeScript("return window.jQuery !== undefined && jQuery.active === 0"));

// Angular reports that it's stable
wait.until(d -> (Boolean) ((JavascriptExecutor) d).executeScript(
    "return window.getAllAngularTestabilities !== undefined && " +
    "window.getAllAngularTestabilities().every(t => t.isStable())"));

Custom conditions are more reliable than generic waits, because they check the application’s own readiness signals rather than guessing from element state.



Handling Dynamic Lists and Tables

Lists and tables bring a different challenge: the structure itself changes. Rows appear and disappear, item counts change, and positions move with filters, sorting and live updates.

Lists That Change Size

When list length is unpredictable, wait for enough items before interacting, so your test doesn’t run against an incomplete list.

By resultItems = By.cssSelector("[data-testid='search-result']");

// Wait until at least one result has loaded
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems, 0));

// Or wait for an exact count when you know it
wait.until(ExpectedConditions.numberOfElementsToBe(resultItems, 10));
By resultItems = By.cssSelector("[data-testid='search-result']");

// Wait until at least one result has loaded
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems, 0));

// Or wait for an exact count when you know it
wait.until(ExpectedConditions.numberOfElementsToBe(resultItems, 10));
By resultItems = By.cssSelector("[data-testid='search-result']");

// Wait until at least one result has loaded
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems, 0));

// Or wait for an exact count when you know it
wait.until(ExpectedConditions.numberOfElementsToBe(resultItems, 10));
By resultItems = By.cssSelector("[data-testid='search-result']");

// Wait until at least one result has loaded
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems, 0));

// Or wait for an exact count when you know it
wait.until(ExpectedConditions.numberOfElementsToBe(resultItems, 10));

Find Items by Content, Not Position

Finding list items by their content makes your test independent of sort order and page state.

// XPath: the result that contains "Wireless Keyboard"
WebElement keyboard = driver.findElement(By.xpath(
    "//*[@data-testid='search-result'][.//h3[normalize-space()='Wireless Keyboard']]"
));

// Java: filter the list by text
WebElement keyboardResult = driver.findElements(resultItems).stream()
    .filter(item -> item.getText().contains("Wireless Keyboard"))
    .findFirst()
    .orElseThrow(() -> new AssertionError("Wireless Keyboard not found in results"));
// XPath: the result that contains "Wireless Keyboard"
WebElement keyboard = driver.findElement(By.xpath(
    "//*[@data-testid='search-result'][.//h3[normalize-space()='Wireless Keyboard']]"
));

// Java: filter the list by text
WebElement keyboardResult = driver.findElements(resultItems).stream()
    .filter(item -> item.getText().contains("Wireless Keyboard"))
    .findFirst()
    .orElseThrow(() -> new AssertionError("Wireless Keyboard not found in results"));
// XPath: the result that contains "Wireless Keyboard"
WebElement keyboard = driver.findElement(By.xpath(
    "//*[@data-testid='search-result'][.//h3[normalize-space()='Wireless Keyboard']]"
));

// Java: filter the list by text
WebElement keyboardResult = driver.findElements(resultItems).stream()
    .filter(item -> item.getText().contains("Wireless Keyboard"))
    .findFirst()
    .orElseThrow(() -> new AssertionError("Wireless Keyboard not found in results"));
// XPath: the result that contains "Wireless Keyboard"
WebElement keyboard = driver.findElement(By.xpath(
    "//*[@data-testid='search-result'][.//h3[normalize-space()='Wireless Keyboard']]"
));

// Java: filter the list by text
WebElement keyboardResult = driver.findElements(resultItems).stream()
    .filter(item -> item.getText().contains("Wireless Keyboard"))
    .findFirst()
    .orElseThrow(() -> new AssertionError("Wireless Keyboard not found in results"));

Use Indices Carefully

Selecting by position is the most fragile approach to dynamic lists, so use content-based selection where you can. When an index is genuinely necessary, check the list length first.

List<WebElement> results = driver.findElements(resultItems);

if (results.size() < 3) {
    throw new AssertionError("Expected at least 3 results but found " + results.size());
}
results.get(2).click();
List<WebElement> results = driver.findElements(resultItems);

if (results.size() < 3) {
    throw new AssertionError("Expected at least 3 results but found " + results.size());
}
results.get(2).click();
List<WebElement> results = driver.findElements(resultItems);

if (results.size() < 3) {
    throw new AssertionError("Expected at least 3 results but found " + results.size());
}
results.get(2).click();
List<WebElement> results = driver.findElements(resultItems);

if (results.size() < 3) {
    throw new AssertionError("Expected at least 3 results but found " + results.size());
}
results.get(2).click();

Narrow the set first, so the index refers to a meaningful position within a defined group:

// The first in-stock result, rather than the first result of any kind
List<WebElement> inStock = driver.findElements(By.cssSelector(
    "[data-testid='search-result'][data-stock='in-stock']"
));
inStock.get(0).click();
// The first in-stock result, rather than the first result of any kind
List<WebElement> inStock = driver.findElements(By.cssSelector(
    "[data-testid='search-result'][data-stock='in-stock']"
));
inStock.get(0).click();
// The first in-stock result, rather than the first result of any kind
List<WebElement> inStock = driver.findElements(By.cssSelector(
    "[data-testid='search-result'][data-stock='in-stock']"
));
inStock.get(0).click();
// The first in-stock result, rather than the first result of any kind
List<WebElement> inStock = driver.findElements(By.cssSelector(
    "[data-testid='search-result'][data-stock='in-stock']"
));
inStock.get(0).click();

Tables with Dynamic Rows

Tables combine variable row counts with the need to match rows against column headers. Hard-coding column positions breaks when columns are reordered. This method finds the column from its header text at runtime:

import java.util.List;
import org.openqa.selenium.NoSuchElementException;

public String getCellValue(WebElement table, String rowKey, String columnHeader) {
    List<WebElement> headers = table.findElements(By.cssSelector("thead th"));

    int columnIndex = -1;
    for (int i = 0; i < headers.size(); i++) {
        if (headers.get(i).getText().trim().equalsIgnoreCase(columnHeader)) {
            columnIndex = i + 1; // XPath positions start at 1
            break;
        }
    }
    if (columnIndex == -1) {
        throw new NoSuchElementException("Column not found: " + columnHeader);
    }

    WebElement row = table.findElement(By.xpath(
        ".//tbody/tr[td[normalize-space()='" + rowKey + "']]"
    ));
    return row.findElement(By.xpath("./td[" + columnIndex + "]")).getText();
}
import java.util.List;
import org.openqa.selenium.NoSuchElementException;

public String getCellValue(WebElement table, String rowKey, String columnHeader) {
    List<WebElement> headers = table.findElements(By.cssSelector("thead th"));

    int columnIndex = -1;
    for (int i = 0; i < headers.size(); i++) {
        if (headers.get(i).getText().trim().equalsIgnoreCase(columnHeader)) {
            columnIndex = i + 1; // XPath positions start at 1
            break;
        }
    }
    if (columnIndex == -1) {
        throw new NoSuchElementException("Column not found: " + columnHeader);
    }

    WebElement row = table.findElement(By.xpath(
        ".//tbody/tr[td[normalize-space()='" + rowKey + "']]"
    ));
    return row.findElement(By.xpath("./td[" + columnIndex + "]")).getText();
}
import java.util.List;
import org.openqa.selenium.NoSuchElementException;

public String getCellValue(WebElement table, String rowKey, String columnHeader) {
    List<WebElement> headers = table.findElements(By.cssSelector("thead th"));

    int columnIndex = -1;
    for (int i = 0; i < headers.size(); i++) {
        if (headers.get(i).getText().trim().equalsIgnoreCase(columnHeader)) {
            columnIndex = i + 1; // XPath positions start at 1
            break;
        }
    }
    if (columnIndex == -1) {
        throw new NoSuchElementException("Column not found: " + columnHeader);
    }

    WebElement row = table.findElement(By.xpath(
        ".//tbody/tr[td[normalize-space()='" + rowKey + "']]"
    ));
    return row.findElement(By.xpath("./td[" + columnIndex + "]")).getText();
}
import java.util.List;
import org.openqa.selenium.NoSuchElementException;

public String getCellValue(WebElement table, String rowKey, String columnHeader) {
    List<WebElement> headers = table.findElements(By.cssSelector("thead th"));

    int columnIndex = -1;
    for (int i = 0; i < headers.size(); i++) {
        if (headers.get(i).getText().trim().equalsIgnoreCase(columnHeader)) {
            columnIndex = i + 1; // XPath positions start at 1
            break;
        }
    }
    if (columnIndex == -1) {
        throw new NoSuchElementException("Column not found: " + columnHeader);
    }

    WebElement row = table.findElement(By.xpath(
        ".//tbody/tr[td[normalize-space()='" + rowKey + "']]"
    ));
    return row.findElement(By.xpath("./td[" + columnIndex + "]")).getText();
}
// In the test
WebElement ordersTable = driver.findElement(By.cssSelector("[data-testid='orders-table']"));
String status = getCellValue(ordersTable, "ORD-88231", "Status");
// In the test
WebElement ordersTable = driver.findElement(By.cssSelector("[data-testid='orders-table']"));
String status = getCellValue(ordersTable, "ORD-88231", "Status");
// In the test
WebElement ordersTable = driver.findElement(By.cssSelector("[data-testid='orders-table']"));
String status = getCellValue(ordersTable, "ORD-88231", "Status");
// In the test
WebElement ordersTable = driver.findElement(By.cssSelector("[data-testid='orders-table']"));
String status = getCellValue(ordersTable, "ORD-88231", "Status");



Handling Elements That Change State

State changes add a time dimension: the same element is usable at some moments and not at others. Tests that ignore state often fail falsely, because they act on elements at the wrong moment.

Elements That Enable and Disable

Buttons that stay disabled until a form is complete, or until an earlier action finishes, need a wait that checks they’re enabled, not just present.

By submitButton = By.cssSelector("[data-testid='checkout-submit']");

// Visible and enabled
wait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();

// Or check the enabled state directly
wait.until(d -> d.findElement(submitButton).isEnabled());
By submitButton = By.cssSelector("[data-testid='checkout-submit']");

// Visible and enabled
wait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();

// Or check the enabled state directly
wait.until(d -> d.findElement(submitButton).isEnabled());
By submitButton = By.cssSelector("[data-testid='checkout-submit']");

// Visible and enabled
wait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();

// Or check the enabled state directly
wait.until(d -> d.findElement(submitButton).isEnabled());
By submitButton = By.cssSelector("[data-testid='checkout-submit']");

// Visible and enabled
wait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();

// Or check the enabled state directly
wait.until(d -> d.findElement(submitButton).isEnabled());

Elements That Show and Hide

Elements often toggle between visible and hidden. Handle both directions: wait for them to appear before interacting, and wait for them to disappear before asserting they’re gone.

By toast = By.cssSelector("[data-testid='success-toast']");

// Wait for the message to appear
wait.until(ExpectedConditions.visibilityOfElementLocated(toast));

// Wait for it to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(toast));
By toast = By.cssSelector("[data-testid='success-toast']");

// Wait for the message to appear
wait.until(ExpectedConditions.visibilityOfElementLocated(toast));

// Wait for it to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(toast));
By toast = By.cssSelector("[data-testid='success-toast']");

// Wait for the message to appear
wait.until(ExpectedConditions.visibilityOfElementLocated(toast));

// Wait for it to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(toast));
By toast = By.cssSelector("[data-testid='success-toast']");

// Wait for the message to appear
wait.until(ExpectedConditions.visibilityOfElementLocated(toast));

// Wait for it to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(toast));

Elements with Changing Classes

Class changes often show a state transition, such as a tab becoming active or a step being completed. Wait for the class instead of checking repeatedly in your test code.

By billingTab = By.cssSelector("[role='tab'][aria-controls='billing-panel']");

wait.until(ExpectedConditions.attributeContains(billingTab, "class", "active"));

// Where ARIA state is available, it's often more reliable than class names
wait.until(ExpectedConditions.attributeToBe(billingTab, "aria-selected", "true"));
By billingTab = By.cssSelector("[role='tab'][aria-controls='billing-panel']");

wait.until(ExpectedConditions.attributeContains(billingTab, "class", "active"));

// Where ARIA state is available, it's often more reliable than class names
wait.until(ExpectedConditions.attributeToBe(billingTab, "aria-selected", "true"));
By billingTab = By.cssSelector("[role='tab'][aria-controls='billing-panel']");

wait.until(ExpectedConditions.attributeContains(billingTab, "class", "active"));

// Where ARIA state is available, it's often more reliable than class names
wait.until(ExpectedConditions.attributeToBe(billingTab, "aria-selected", "true"));
By billingTab = By.cssSelector("[role='tab'][aria-controls='billing-panel']");

wait.until(ExpectedConditions.attributeContains(billingTab, "class", "active"));

// Where ARIA state is available, it's often more reliable than class names
wait.until(ExpectedConditions.attributeToBe(billingTab, "aria-selected", "true"));



Why Manual Strategies Add Up

Each strategy above adds code that engineers have to write, review and maintain:

  • Partial matches need careful patterns to avoid matching the wrong element

  • Waits need custom conditions for application-specific situations

  • Relative locators need detailed knowledge of the current DOM

  • List handling needs defensive code for unpredictable sizes

Across a large suite, that complexity builds up. Tests get harder to read, maintain and debug, because every dynamic element adds another layer of handling logic.

To follow how teams move from locator-heavy scripts to tests that keep pace with application changes, read our guide to self-healing in continuous testing.



How Virtuoso QA Handles Dynamic Elements

AI-native platforms such as Virtuoso QA identify elements the way people do: by what they look like and what they’re for, not by the implementation details underneath. You describe the element in plain English, and the platform works out how to find it.

The Same Step, Two Ways

Traditional Selenium needs the engineer to know the ID is dynamic, design a partial match around the stable prefix, wrap it in an explicit wait, and maintain all of that if the prefix changes:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement email = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.xpath("//input[starts-with(@id, 'email_') and @type='email']")
));
email.sendKeys("jane@example.com");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement email = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.xpath("//input[starts-with(@id, 'email_') and @type='email']")
));
email.sendKeys("jane@example.com");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement email = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.xpath("//input[starts-with(@id, 'email_') and @type='email']")
));
email.sendKeys("jane@example.com");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement email = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.xpath("//input[starts-with(@id, 'email_') and @type='email']")
));
email.sendKeys("jane@example.com");

In Virtuoso QA, the same step is:

Write "jane@example.com" in field "Email address"
Write "jane@example.com" in field "Email address"
Write "jane@example.com" in field "Email address"
Write "jane@example.com" in field "Email address"

Virtuoso QA identifies the field from its label, placeholder text, input type, position and appearance together, so the dynamic ID doesn’t matter. There’s no locator for you to write or maintain.

To learn how plain English steps let more of the team build and maintain tests, read our guide to natural language programming in testing.

Identification from Many Signals

Signal Type

Examples

Meaning

Labels, ARIA attributes, placeholder text, field names, roles and element types

Appearance

Position on screen, size and relationship to other elements

Structure

Container, neighbouring elements and page region

When one attribute changes, such as a regenerated ID or a new class hash, the element can still be identified from the others.

Repairs You Review

When the page changes more significantly and an element can’t be found the usual way, Virtuoso QA proposes a repair, and your team accepts or rejects it. Tests keep up with UI changes, and every change leaves a record, so a passing run never hides an edit nobody agreed to.

Waiting Built In

Each step waits for the page to be ready before acting, so you don’t write explicit waits or custom conditions for AJAX content and loading states.

Live Authoring and Clear Failures

Each step runs against the application as it’s written, so you see straight away whether an element was found. When a run fails, the failed step appears beside the last passing run, with screenshots, the DOM snapshot, network data and a likely cause.

Moving Existing Selenium Suites

GENerator converts existing Selenium tests into plain English journeys, so teams keep their coverage and leave locator maintenance behind.



Related Reads‍

ElementNotInteractableException in Selenium: Complete Fix Guide‍

From Selenium Scripts to Self-Healing Tests: The Complete Journey of Test Automation‍‍

Headless Browser Testing with Selenium: Complete Guide‍

Selenium Timeouts Explained: Implicit, Explicit, Fluent Waits

‍‍Dynamic XPath in Selenium Explained With Practical Examples‍

Selenium ChromeDriver Setup Guide: Install, Configure & Run Chrome Tests‍

Selenium NotFoundException: Causes, Fixes & Solutions

Dynamic Elements in Selenium FAQs

What are dynamic elements in Selenium?

How do I handle dynamic IDs in Selenium?

How do I wait for AJAX content in Selenium?

Is XPath or CSS better for dynamic elements?

How do I select an item from a dynamic list?

Can AI-native tools handle dynamic elements without locators?

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA