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.
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 --><inputid="email_8f3k2"class="Input_field__x7Kq2"type="email"name="email"placeholder="Email address"><!-- Page load 2: same field, new values --><inputid="email_x91qa"class="Input_field__p2Lm9"type="email"name="email"placeholder="Email address">
<!-- Page load 1 --><inputid="email_8f3k2"class="Input_field__x7Kq2"type="email"name="email"placeholder="Email address"><!-- Page load 2: same field, new values --><inputid="email_x91qa"class="Input_field__p2Lm9"type="email"name="email"placeholder="Email address">
<!-- Page load 1 --><inputid="email_8f3k2"class="Input_field__x7Kq2"type="email"name="email"placeholder="Email address"><!-- Page load 2: same field, new values --><inputid="email_x91qa"class="Input_field__p2Lm9"type="email"name="email"placeholder="Email address">
<!-- Page load 1 --><inputid="email_8f3k2"class="Input_field__x7Kq2"type="email"name="email"placeholder="Email address"><!-- Page load 2: same field, new values --><inputid="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:
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 ondriver.findElement(By.xpath("//input[contains(@id, 'email_')]"));// Matches a hashed class name by its stable partdriver.findElement(By.xpath("//button[contains(@class, 'Button_primary')]"));
// Matches id="email_8f3k2", id="email_x91qa" and so ondriver.findElement(By.xpath("//input[contains(@id, 'email_')]"));// Matches a hashed class name by its stable partdriver.findElement(By.xpath("//button[contains(@class, 'Button_primary')]"));
// Matches id="email_8f3k2", id="email_x91qa" and so ondriver.findElement(By.xpath("//input[contains(@id, 'email_')]"));// Matches a hashed class name by its stable partdriver.findElement(By.xpath("//button[contains(@class, 'Button_primary')]"));
// Matches id="email_8f3k2", id="email_x91qa" and so ondriver.findElement(By.xpath("//input[contains(@id, 'email_')]"));// Matches a hashed class name by its stable partdriver.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():
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" anywheredriver.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" anywheredriver.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" anywheredriver.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" anywheredriver.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:
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 textdriver.findElement(By.linkText("Forgot password?"));// Part of the visible textdriver.findElement(By.partialLinkText("Forgot"));
// Exact visible textdriver.findElement(By.linkText("Forgot password?"));// Part of the visible textdriver.findElement(By.partialLinkText("Forgot"));
// Exact visible textdriver.findElement(By.linkText("Forgot password?"));// Part of the visible textdriver.findElement(By.partialLinkText("Forgot"));
// Exact visible textdriver.findElement(By.linkText("Forgot password?"));// Part of the visible textdriver.findElement(By.partialLinkText("Forgot"));
XPath Text Matching
// Exact textdriver.findElement(By.xpath("//button[text()='Place order']"));// Text that contains a phrasedriver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));// Ignores extra spaces and line breaks, and matches text in child elementsdriver.findElement(By.xpath("//button[normalize-space()='Place order']"));
// Exact textdriver.findElement(By.xpath("//button[text()='Place order']"));// Text that contains a phrasedriver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));// Ignores extra spaces and line breaks, and matches text in child elementsdriver.findElement(By.xpath("//button[normalize-space()='Place order']"));
// Exact textdriver.findElement(By.xpath("//button[text()='Place order']"));// Text that contains a phrasedriver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));// Ignores extra spaces and line breaks, and matches text in child elementsdriver.findElement(By.xpath("//button[normalize-space()='Place order']"));
// Exact textdriver.findElement(By.xpath("//button[text()='Place order']"));// Text that contains a phrasedriver.findElement(By.xpath("//h2[contains(text(), 'Order summary')]"));// Ignores extra spaces and line breaks, and matches text in child elementsdriver.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" labeldriver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));// The input inside the same form group as the labeldriver.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" labeldriver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));// The input inside the same form group as the labeldriver.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" labeldriver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));// The input inside the same form group as the labeldriver.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" labeldriver.findElement(By.xpath("//label[normalize-space()='Email address']/following-sibling::input"));// The input inside the same form group as the labeldriver.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 childdriver.findElement(By.cssSelector("form#checkout > button[type='submit']"));// Any descendantdriver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));// The input immediately after a labeldriver.findElement(By.cssSelector("label[for='email'] + input"));
// A direct childdriver.findElement(By.cssSelector("form#checkout > button[type='submit']"));// Any descendantdriver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));// The input immediately after a labeldriver.findElement(By.cssSelector("label[for='email'] + input"));
// A direct childdriver.findElement(By.cssSelector("form#checkout > button[type='submit']"));// Any descendantdriver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));// The input immediately after a labeldriver.findElement(By.cssSelector("label[for='email'] + input"));
// A direct childdriver.findElement(By.cssSelector("form#checkout > button[type='submit']"));// Any descendantdriver.findElement(By.cssSelector("[data-testid='billing-section'] input[name='postcode']"));// The input immediately after a labeldriver.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:
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 textdriver.findElement(By.xpath("//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"));// CSS: an email input inside the billing formdriver.findElement(By.cssSelector("form[data-testid='billing-form'] input[type='email'][name='email']"));
// XPath: a submit button with a stable class fragment and specific textdriver.findElement(By.xpath("//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"));// CSS: an email input inside the billing formdriver.findElement(By.cssSelector("form[data-testid='billing-form'] input[type='email'][name='email']"));
// XPath: a submit button with a stable class fragment and specific textdriver.findElement(By.xpath("//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"));// CSS: an email input inside the billing formdriver.findElement(By.cssSelector("form[data-testid='billing-form'] input[type='email'][name='email']"));
// XPath: a submit button with a stable class fragment and specific textdriver.findElement(By.xpath("//button[@type='submit' and contains(@class, 'Button_primary') and normalize-space()='Place order']"));// CSS: an email input inside the billing formdriver.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.
importjava.time.Duration;importorg.openqa.selenium.support.ui.ExpectedConditions;importorg.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']")));
importjava.time.Duration;importorg.openqa.selenium.support.ui.ExpectedConditions;importorg.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']")));
importjava.time.Duration;importorg.openqa.selenium.support.ui.ExpectedConditions;importorg.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']")));
importjava.time.Duration;importorg.openqa.selenium.support.ui.ExpectedConditions;importorg.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.
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.
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 textwait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector("[data-testid='order-status']"),"Confirmed"));// Wait for any text at allwait.until(driver -> !driver.findElement(By.cssSelector("[data-testid='results-count']"))
.getText().trim().isEmpty());
// Wait for specific textwait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector("[data-testid='order-status']"),"Confirmed"));// Wait for any text at allwait.until(driver -> !driver.findElement(By.cssSelector("[data-testid='results-count']"))
.getText().trim().isEmpty());
// Wait for specific textwait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector("[data-testid='order-status']"),"Confirmed"));// Wait for any text at allwait.until(driver -> !driver.findElement(By.cssSelector("[data-testid='results-count']"))
.getText().trim().isEmpty());
// Wait for specific textwait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector("[data-testid='order-status']"),"Confirmed"));// Wait for any text at allwait.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.
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.
importorg.openqa.selenium.JavascriptExecutor;// The document has finished loadingwait.until(d -> ((JavascriptExecutor)d)
.executeScript("return document.readyState").equals("complete"));// jQuery has no active requestswait.until(d -> (Boolean)((JavascriptExecutor)d)
.executeScript("return window.jQuery !== undefined && jQuery.active === 0"));// Angular reports that it's stablewait.until(d -> (Boolean)((JavascriptExecutor)d).executeScript("return window.getAllAngularTestabilities !== undefined && " +
"window.getAllAngularTestabilities().every(t => t.isStable())"));
importorg.openqa.selenium.JavascriptExecutor;// The document has finished loadingwait.until(d -> ((JavascriptExecutor)d)
.executeScript("return document.readyState").equals("complete"));// jQuery has no active requestswait.until(d -> (Boolean)((JavascriptExecutor)d)
.executeScript("return window.jQuery !== undefined && jQuery.active === 0"));// Angular reports that it's stablewait.until(d -> (Boolean)((JavascriptExecutor)d).executeScript("return window.getAllAngularTestabilities !== undefined && " +
"window.getAllAngularTestabilities().every(t => t.isStable())"));
importorg.openqa.selenium.JavascriptExecutor;// The document has finished loadingwait.until(d -> ((JavascriptExecutor)d)
.executeScript("return document.readyState").equals("complete"));// jQuery has no active requestswait.until(d -> (Boolean)((JavascriptExecutor)d)
.executeScript("return window.jQuery !== undefined && jQuery.active === 0"));// Angular reports that it's stablewait.until(d -> (Boolean)((JavascriptExecutor)d).executeScript("return window.getAllAngularTestabilities !== undefined && " +
"window.getAllAngularTestabilities().every(t => t.isStable())"));
importorg.openqa.selenium.JavascriptExecutor;// The document has finished loadingwait.until(d -> ((JavascriptExecutor)d)
.executeScript("return document.readyState").equals("complete"));// jQuery has no active requestswait.until(d -> (Boolean)((JavascriptExecutor)d)
.executeScript("return window.jQuery !== undefined && jQuery.active === 0"));// Angular reports that it's stablewait.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 loadedwait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems,0));// Or wait for an exact count when you know itwait.until(ExpectedConditions.numberOfElementsToBe(resultItems,10));
By resultItems = By.cssSelector("[data-testid='search-result']");// Wait until at least one result has loadedwait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems,0));// Or wait for an exact count when you know itwait.until(ExpectedConditions.numberOfElementsToBe(resultItems,10));
By resultItems = By.cssSelector("[data-testid='search-result']");// Wait until at least one result has loadedwait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems,0));// Or wait for an exact count when you know itwait.until(ExpectedConditions.numberOfElementsToBe(resultItems,10));
By resultItems = By.cssSelector("[data-testid='search-result']");// Wait until at least one result has loadedwait.until(ExpectedConditions.numberOfElementsToBeMoreThan(resultItems,0));// Or wait for an exact count when you know itwait.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){thrownew AssertionError("Expected at least 3 results but found " + results.size());}results.get(2).click();
List<WebElement> results = driver.findElements(resultItems);if(results.size() < 3){thrownew AssertionError("Expected at least 3 results but found " + results.size());}results.get(2).click();
List<WebElement> results = driver.findElements(resultItems);if(results.size() < 3){thrownew AssertionError("Expected at least 3 results but found " + results.size());}results.get(2).click();
List<WebElement> results = driver.findElements(resultItems);if(results.size() < 3){thrownew 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 kindList<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 kindList<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 kindList<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 kindList<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:
// 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 enabledwait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();// Or check the enabled state directlywait.until(d -> d.findElement(submitButton).isEnabled());
By submitButton = By.cssSelector("[data-testid='checkout-submit']");// Visible and enabledwait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();// Or check the enabled state directlywait.until(d -> d.findElement(submitButton).isEnabled());
By submitButton = By.cssSelector("[data-testid='checkout-submit']");// Visible and enabledwait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();// Or check the enabled state directlywait.until(d -> d.findElement(submitButton).isEnabled());
By submitButton = By.cssSelector("[data-testid='checkout-submit']");// Visible and enabledwait.until(ExpectedConditions.elementToBeClickable(submitButton)).click();// Or check the enabled state directlywait.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 appearwait.until(ExpectedConditions.visibilityOfElementLocated(toast));// Wait for it to disappearwait.until(ExpectedConditions.invisibilityOfElementLocated(toast));
By toast = By.cssSelector("[data-testid='success-toast']");// Wait for the message to appearwait.until(ExpectedConditions.visibilityOfElementLocated(toast));// Wait for it to disappearwait.until(ExpectedConditions.invisibilityOfElementLocated(toast));
By toast = By.cssSelector("[data-testid='success-toast']");// Wait for the message to appearwait.until(ExpectedConditions.visibilityOfElementLocated(toast));// Wait for it to disappearwait.until(ExpectedConditions.invisibilityOfElementLocated(toast));
By toast = By.cssSelector("[data-testid='success-toast']");// Wait for the message to appearwait.until(ExpectedConditions.visibilityOfElementLocated(toast));// Wait for it to disappearwait.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 nameswait.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 nameswait.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 nameswait.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 nameswait.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 = newWebDriverWait(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 = newWebDriverWait(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 = newWebDriverWait(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 = newWebDriverWait(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"infield "Email address"
Write "jane@example.com"infield "Email address"
Write "jane@example.com"infield "Email address"
Write "jane@example.com"infield "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.
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.
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.
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.
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.