When it comes to UI test automation, one of the most important but most neglected parts will be how we locate web elements and how we interact with those web elements. It does not matter if you use Selenium, Playwright, Cypress, or Appium; stable and maintainable test scripts rely on reliable locators. A change to the DOM structure or a poorly engaged locator can yield flaky tests, wasted time, and useless reports.
Good locator strategies allow QA (Quality Assurance) engineers and automation testers to write strong, adaptable tests, which keep maintenance to a minimum and allow for efficient scaling from the ground up. Good locator strategies are important not only for choosing which locator type to use, but making great locators and using them to engage dynamic elements, dealing with more complex UIs (tables, iframes, modals), etc.
In the upcoming blogs, we will deliver to you relevant locator strategies, realistic situations, some tool-based perspectives, and some avoidable pitfalls and best practices of text factors when applying your UI tests so that you can have reliable, meaningful, scalable UI tests.
- Why Web Element Locators Are Key to Automation Success
- Common Issues Caused by Poor Locators
- Understanding Web Element Locators
- Choosing the Right Locator for the Right Situation
- Handling Dynamic Web Elements
- Advanced XPath Techniques and Axes
- Locating Elements Inside Tables
- Navigating Complex UI Structures in Test Automation
- Locator Discovery Tools and Techniques
- Best Practices for Writing and Maintaining Locators
- Framework-Specific Locator Examples in UI Automation
- Common Mistakes to Avoid in Locator Strategy
- Organizing Locators in Automation Projects
- Real-World Example
- Checklist: Writing Stable Locators
- Conclusion
Why Web Element Locators Are Key to Automation Success
Accurate and reliable locators are the foundation of stable and maintainable test automation. Whether using Selenium, Cypress, Playwright, or Appium, the effectiveness of your tests depends on how efficiently your scripts can find and interact with page elements.
Well-defined locators result in:
- Reduced test flakiness
- Lower maintenance effort when UI changes occur
- Faster debugging and issue resolution
- More consistent and trustworthy test outcomes
A strategic approach to writing locators ensures your automation suite is scalable, robust, and future-proof.
Common Issues Caused by Poor Locators
Many automation failures can be traced back to poor locator strategies. Some common issues include:
- Use of non-unique or auto-generated element IDs
- Overreliance on deeply nested or brittle XPath expressions
- Ignoring accessibility attributes such as
aria-label
,role
, ordata-testid
- Failing to handle dynamic or localized content properly
By addressing these issues early, teams can avoid unnecessary maintenance, improve test reliability, and build a stronger automation framework.
Understanding Web Element Locators
What is a Locator?
A locator is a reference used by automation tools to identify and interact with elements on a web page. It tells the testing framework exactly where an element is located in the DOM (Document Object Model). Without a reliable locator, automation scripts cannot function correctly.
How UI Testing Frameworks Use Locators
Automation frameworks like Selenium, Cypress, Playwright, and Appium use locators to find elements before performing actions like clicking buttons, entering text, selecting options, or verifying element states. These locators serve as the bridge between the test scripts and the application’s user interface.
When a test script executes, the framework queries the DOM using the provided locator and attempts to find the matching element. If the locator is incorrect, outdated, or too generic, the test may fail or behave unpredictably.
Common Locator Types
Different frameworks support a variety of locator strategies, including:
- ID – Fast and reliable when the ID is unique.
- Name – Useful when form elements have consistent name attributes.
- Class Name – Suitable when class values are stable and specific.
- Tag Name – Rarely used alone, but helpful in combination with other selectors.
- CSS Selector – Powerful and clean, allowing precise targeting based on attributes, hierarchy, and more.
- XPath – Flexible for navigating complex or dynamic DOM structures, especially when elements lack good identifiers.
- Custom Attributes – Such as
data-testid
,aria-label
, or other developer-defined attributes, often added for testing purposes.
Choosing the right locator type depends on the application structure and the stability of the DOM elements.
Choosing the Right Locator for the Right Situation
When to Use Specific Locators
1 ID
Use when the element has a unique and stable id attribute. IDs are the most efficient and least likely to change.
Example:
<input id="username" type="text"\>
Locator:
- Selenium: driver.findElement(By.id(“username”))
- Playwright: page.locator(“#username”)
2 CSS Selector
Ideal for elements with consistent class names or attribute patterns. CSS selectors are clean and flexible.
Example:
<button class="btn primary login-button"\>Login\</button>
Locator
- CSS Selector: .login-button
- Hierarchical: div.container > button.login-button
3 XPath
Useful for navigating complex or nested DOM structures where no IDs or classes are available.
Example:
<table>
<tr>
<td>Username</td>
<td><input type="text" name="user"></td>
</tr>
</table>
Locator:
- XPath:
//tr[td[text()='Username']]/td[2]/input
4 Custom Attributes (data-testid
, aria-*
)
Best for automation as they’re designed to remain stable, even when the UI changes.
Example:
<button data-testid="submit-btn">Submit</button>`
Locator:
- CSS:
[data-testid='submit-btn']
- Playwright:
page.getByTestId("submit-btn")
Mini decision matrix: Strategy vs. Scenario
Choosing the right locator isn’t just about what’s technically possible it’s about what makes your test reliable, readable, and easy to maintain over time. Here’s how to make informed choices based on different real-world scenarios:
Simplicity and Uniqueness
Use: id, data-testid, or other custom attributes
When: The element has a unique, stable attribute that clearly identifies it.
Why: These locators are short, efficient, and less prone to change as UI layout evolves. Custom attributes like data-testid are especially helpful in test environments.
Example Scenario:
A login form input with id="email"
or data-testid="login-email"
- Preferred locator:
#email
or[data-testid="login-email"]
Readability and Maintainability
Use: Semantic class names or CSS selectors
When: Working with reusable UI components or when id
is not available
Why: Well-structured CSS selectors using meaningful class names help testers quickly understand the purpose of an element. They’re easy to read and modify as the UI grows.
Example Scenario:
A button with class btn-submit primary
inside a form
- Locator: .btn-submit
Avoid using overly complex chained selectors like:
form > div:nth-child(2) > button.btn-submit
They are harder to maintain if the layout changes.
Use: XPath
When: You need to locate elements based on their relationship to other elements (e.g., nearest sibling, parent-child)
Why: XPath excels in complex, nested structures, especially when there’s no direct identifier on the target element.
Example Scenario:
You need to click the delete icon in a table row that contains a specific username.
- XPath:
//tr[td[text()='john.doe']]/td/button[@class='delete']
This allows precision when element relationships matter more than attributes.
Consistency with Framework and Project Standards
Use: Whatever locator format your framework or team recommends
When: Your team follows a structured automation strategy or uses specific libraries/tools
Why: Adhering to consistent patterns ensures uniformity across the test suite, easier onboarding for new team members, and better maintainability.
Example Scenario:
- Playwright projects may prefer
getByTestId()
- Cypress teams might use custom
data-cy
attributes - Selenium users might stick with POM (Page Object Model) structure and centralized locators
Handling Dynamic Web Elements
Modern web applications often rely on dynamic content that is rendered or modified at runtime. This poses a unique challenge for automation engineers, as such elements typically have unstable or changing attributes.
To interact with these elements reliably, we must use dynamic locator strategies that adapt to changes in the DOM. This section covers the core techniques used to handle dynamic elements in test automation.
What Are Dynamic Locators?
Dynamic locators are used to identify web elements whose attributes (like id, class, or text) change every time the page is loaded or based on user interactions. These elements cannot be located using static locators like fixed IDs or names because those attributes are either auto-generated or inconsistent.
Example:
HTML:
<button id="btn-submit-78121">Submit</button>
The id here is dynamic and changes with each session.
Incorrect (static locator):
driver.FindElement(By.Id("btn-submit-78121")); // Will break next time
Correct (dynamic locator):
driver.FindElement(By.XPath("//button\[contains(@id, 'btn-submit')\]")).Click();
Dynamic locators help maintain stability by matching predictable parts of the attribute or structure.
XPath with contains(), starts-with(), normalize-space()
XPath functions are powerful tools when dealing with dynamic elements. These functions allow partial matching and whitespace normalization, which helps when values are generated or inconsistent.
1 contains()
Use when part of the attribute or text is consistent.
Example :
<input id="user_email_9182">
Selenium C# :
driver.FindElement(By.XPath("//input[contains(@id, 'user_email')]")).SendKeys("test@example.com");
2 starts-with()
Use when the beginning of the attribute value is stable.
Example:
<div class="message-error-xyz">Error</div>
Selenium C# :
driver.FindElement(By.XPath("//div[starts-with(@class, 'message-error')]")).Click();
3 normalize-space()
Useful when the text has inconsistent spacing.
Example :
<span> Sign Out </span>
Selenium C# :
driver.FindElement(By.XPath("//span[normalize-space()='Sign Out']")).Click();
CSS Attribute Match Techniques
CSS selectors can also be used to locate elements with partially dynamic attributes. These selectors support different types of attribute matches:
[attr*='value']
– contains
Selects elements where the attribute contains the specified substring anywhere within its value.
Use case: Useful when the dynamic value can be anywhere in the attribute.
Example :
<input name="customer_email_field_9876">
Selenium C# :
driver.FindElement(By.CssSelector("input[name*='email']")).SendKeys("test@example.com");
- `[attr^=’value’]` – starts with
Selects elements where the attribute starts with the given substring.
Use case: Ideal when the dynamic part is at the end of a predictable prefix.
Example :
<input id="user_input_1234">
Selenium C#:
driver.FindElement(By.CssSelector("input[id^='user_input']")).SendKeys("demoUser");
[attr$='value']
– ends with
Selects elements where the attribute ends with the specified substring.
Use case: Useful when the stable part is at the end and the beginning is dynamic.
Example :
<input name="form_token_abc123xyz">
Selenium C# :
driver.FindElement(By.CssSelector("input[name$='token_abc123xyz']")).SendKeys("value");
This technique is useful when using CSS over XPath in frameworks like Cypress or Playwright, but it works equally well in Selenium.
When to Use CSS Match Techniques
These selectors are generally:
- Faster than XPath in most browsers
- More readable and concise
- Compatible with Selenium, Cypress, Playwright, and other tools
Use them whenever possible to improve performance and avoid brittle test code caused by highly specific DOM paths.
Parameterizing Locators with Variables
Instead of hardcoding locators, create methods that accept dynamic input. This allows your tests to scale and adapt to different values at runtime.
Example:
Locating a product card by product name:
HTML:
<div class="product-card">Apple iPhone 14</div>
C# Reusable Method:
public IWebElement GetProductCard(IWebDriver driver, string productName)
{
string xpath = $"//div[contains(@class, 'product-card') and contains(text(), '{productName}')]";
return driver.FindElement(By.XPath(xpath));
}
Usage in Test:
var product = GetProductCard(driver, "Apple iPhone 14");
product.Click();
This approach is cleaner and helps avoid repetitive code when working with a list of items.
Example use cases
Here are common scenarios where dynamic locators are necessary, along with how to handle them.
- Dynamic Buttons
HTML:
<button id="save_btn_458">Save</button>
Locator:
driver.FindElement(By.XPath("//button[contains(@id, 'save_btn')]")).Click();
- Alerts and Toast Messages
HTML:
<div class="alert-success">Successfully saved!</div>
Locator:
driver.FindElement(By.XPath("//div[starts-with(@class, 'alert-')]")).Click();
- Product Cards or Listings
HTML:
<div class="product-card">MacBook Air M2</div>
Locator:
driver.FindElement(By.XPath("//div[contains(@class, 'product-card') and text()='MacBook Air M2']")).Click();
- Calendar or Date Pickers
HTML:
<td class="calendar-day">22</td>
Locator:
driver.FindElement(By.XPath("//td[contains(@class, 'calendar-day') and text()='22']")).Click();
Advanced XPath Techniques and Axes
As web UIs become more complex, it’s often not enough to use simple attributes like id or class to locate elements. That’s where XPath axes come in powerful tools that allow you to navigate between related nodes in the DOM based on their hierarchy or relationship to each other.
This section covers important XPath axes like preceding-sibling, following-sibling, ancestor, descendant, parent, and self along with practical examples using Selenium in C#.
What Are XPath Axes?
XPath axes are keywords that help traverse the DOM in relation to a current node. Instead of only finding an element directly, you can locate it by navigating from a known reference like a parent, child, or sibling.
This is extremely helpful in dynamic or structured layouts like tables, lists, or nested components, where target elements lack unique attributes but have a predictable relationship with surrounding elements.
Using preceding-sibling
and following-sibling
These axes are used when you want to locate an element based on another element that appears before or after it at the same hierarchical level.
- following-sibling
Use Case: Select an element that appears after another element in the same container.
HTML:
<td>Username</td>
<td><input type="text" name="username"></td>
Selenium C#:
driver.FindElement(By.XPath("//td[text()='Username']/following-sibling::td/input")).SendKeys("admin");
- preceding-sibling
Use Case: Select an element that appears before another element.
HTML:
<td>admin</td>
<td><button>Delete</button></td>
SeleniumC#:
driver.FindElement(By.XPath("//button[text()='Delete']/preceding-sibling::td[text()='admin']")).Click();
Other Useful Axes
- ancestor
Use Case: Selects any parent or grandparent of the current node.
HTML:
<table>
<tbody>
<tr>
<td><span>Delete</span></td>
</tr>
</tbody>
</table>
SeleniumC#:
driver.FindElement(By.XPath("//span[text()='Delete']/ancestor::tr")).Click(); // Clicks the entire row
- descendant
Use Case: Selects any child, grandchild, or deeper-level nested node.
HTML:
<div class="product">
<div>
<span class="name">MacBook</span>
</div>
</div>
SeleniumC#:
driver.FindElement(By.XPath("//div[@class='product']/descendant::span[@class='name']")).Click();
- parent
Use Case: Selects the direct parent of the current node.
HTML:
<div class="card">
<span>Product Name</span>
</div>
SeleniumC#:
driver.FindElement(By.XPath("//span[text()='Product Name']/parent::div")).Click();
- self
Use Case: Refers to the current node itself, useful when applying filters or conditions.
SeleniumC#:
driver.FindElement(By.XPath("//div[@class='card']/self::div")).Click(); // Highlights intent clearly
Real-World Use Cases
XPath axes are incredibly useful in navigating complex UI components. Let’s explore practical, real-world examples for tables, lists, modals, and forms.
- Tables
You often need to click Edit/Delete in a row where a specific value exists (e.g., a username or product name).
HTML:
<table>
<tr>
<td>john.doe</td>
<td><button>Edit</button></td>
</tr>
<tr>
<td>jane.smith</td>
<td><button>Edit</button></td>
</tr>
</table>
Selenium C#:
driver.FindElement(By.XPath("//td[text()='jane.smith']/following-sibling::td/button[text()='Edit']")).Click();
- Multi-Level Lists:
You may need to interact with child list items under a specific section header.
HTML:
<div class="menu">
<h3>Electronics</h3>
<ul>
<li>Mobile Phones</li>
<li>Laptops</li>
</ul>
<h3>Clothing</h3>
<ul>
<li>Men</li>
<li>Women</li>
</ul>
</div>
Selenium C#:
driver.FindElement(By.XPath("//h3[text()='Clothing']/following-sibling::ul/li[text()='Women']")).Click();
- Modals
Many modern apps use modals (popups), which are deeply nested in overlay containers.
HTML:
<div class="modal">
<div class="modal-content">
<h2>Delete Confirmation</h2>
<button>Yes, Delete</button>
<button>Cancel</button>
</div>
</div>
Selenium C#:
driver.FindElement(By.XPath("//h2[text()='Delete Confirmation']/following-sibling::button[text()='Yes, Delete']")).Click();
Better Approach (relative to the modal container):
driver.FindElement(By.XPath("//div[@class='modal-content']/descendant::button[text()='Yes, Delete']")).Click();
We use the descendant
axis to find a deeply nested button inside the modal container.
- Forms
Sometimes, the<label>
and<input>
are not connected viafor
andid
. You must locate the input field based on its label.
HTML:
<div class="form-group">
<label>Username</label>
<input type="text" name="username">
</div>
Selenium C#:
driver.FindElement(By.XPath("//label[text()='Username']/following-sibling::input")).SendKeys("admin");
We navigate from the label “Username” to the input field using the following-sibling
axis.
Summary
These real-world examples show how XPath axes help in locating elements that:
- Do not have unique identifiers
- Are only identifiable based on the surrounding context
- Are dynamically generated or deeply nested
Using XPath axes like following-sibling
, descendant
, and ancestor
gives you more control and precision when automating interactions in structured or dynamic UI layouts.
Best Practices and Limitations
Best Practices:
- Use axes only when simple locators (id, class, data attributes) are not available.
- Combine axes with functions like contains() or text() for reliability.
- Keep expressions readable avoid chaining too many axes in a single path.
Limitations:
- Axes-based XPaths can be more fragile if the DOM structure changes frequently.
- Performance may be slightly slower than simple selectors.
- Not supported in CSS selectors XPath is required.
Locating Elements Inside Tables
Working with HTML tables is a common challenge in test automation, especially in admin dashboards or data grids. Often, actions like clicking Edit, Delete, or validating cell values depend on locating rows and columns dynamically. This section explores multiple techniques to locate and interact with table data using XPath and Selenium with C#, with practical examples.
Finding Rows and Columns Dynamically
When automating tables, we often don’t know in advance which row will contain the data we need. Instead of relying on row numbers or static positions, we can dynamically search for rows based on the content of their cells.
Use Case: Find the row where the “Name” column contains “jane.smith”.
HTML:
<table>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>john.doe</td>
<td>Admin</td>
<td><button>Edit</button></td>
</tr>
<tr>
<td>jane.smith</td>
<td>User</td>
<td><button>Edit</button></td>
</tr>
</tbody>
</table>
Selenium C# Example:
var userRow = driver.FindElement(By.XPath("//td[text()='jane.smith']/parent::tr"));
Explanation:
We locate the <td> with text jane.smith, then move up to its parent <tr> using the parent::tr
axis to target the full row.
XPath Examples: Find a Row with a Specific Cell Value
If you want to work only with rows that match a specific value in a given column, use XPath expressions that directly target those cell contents.
Example: Get the “Role” for user john.doe
XPath:
var role = driver.FindElement(By.XPath("//td[text()='john.doe']/following-sibling::td[1]")).Text;
Console.WriteLine(role); // Output: Admin
Explanation:
We locate the \<td> containing john.doe, then move to the first following sibling \<td> which contains the “Role”.
Click/Edit/Delete Operations in Rows
Interacting with action buttons inside a table (e.g., Edit, Delete) is a frequent task. This usually involves locating a row based on a specific value, then navigating to a sibling \<td> to find the button.
Use Case: Click “Edit” for user jane.smith
XPath:
driver.FindElement(By.XPath("//td[text()='jane.smith']/following-sibling::td/button[text()='Edit']")).Click();
Use Case: Click “Delete” for email user@example.com
HTML:
<tr>
<td>user@example.com</td>
<td><a href="#" class="delete">Delete</a></td>
</tr>
XPath:
driver.FindElement(By.XPath("//td[text()='user@example.com']/following-sibling::td/a[text()='Delete']")).Click();
Explanation:
The XPath finds the specific row with the email and navigates to its sibling where the “Delete” link is located.
Working with thead
, tbody
, and Nested Tables
Tables often include a <thead>
for headers and <tbody>
for actual data. In automation, it’s important to scope your XPath to avoid accidentally selecting header rows.
Example: Count the number of rows (excluding headers)
var rows = driver.FindElements(By.XPath("//table/tbody/tr"));
Console.WriteLine("Row count: " + rows.Count);
Explanation:
We target only <tr>
inside <tbody>
to avoid counting <thead>
rows.
Working with Nested Tables
If a table is nested inside another table (e.g., inside a \<td>), we must carefully scope the XPath.
HTML:
<table id="main-table">
<tr>
<td>
<table class="inner-table">
<tr><td>Nested Row</td></tr>
</table>
</td>
</tr>
</table>
XPath:
var nested = driver.FindElement(By.XPath("//table[@id='main-table']//table[@class='inner-table']//td[text()='Nested Row']"));
nested.Click();
Explanation:
We navigate through both levels of tables using //table
with proper scoping.
Summary
Locating elements inside tables is essential for working with data-driven interfaces. With the right XPath strategies, you can:
- Dynamically locate rows and columns based on text
- Interact with specific actions (edit/delete) in the correct row
- Handle nested tables and avoid header mismatches
These techniques will help you write stable and reusable automation scripts for complex table structures.
Navigating Complex UI Structures in Test Automation
Modern web applications often feature sophisticated user interfaces that go beyond simple HTML. Components like iframes, shadow DOM, modals, and hidden elements can make UI automation challenging. Without proper techniques, your test scripts may become flaky or even completely fail.
In this post, we’ll explore how to reliably navigate complex UI structures using Selenium with C#, with practical examples you can use in your automation suite.
Iframes: Switching Contexts and Locating Inner Elements
Iframes are HTML documents embedded within another document. Selenium cannot access elements inside an iframe unless you explicitly switch to it.
Use Case: Fill a form inside an iframe
HTML:
<iframe id="paymentFrame"></iframe>
<!-- inside iframe -->
<input type="text" name="cardNumber">
Selenium C# Example:
// Switch to iframe first
driver.SwitchTo().Frame("paymentFrame");
// Interact with element inside iframe
driver.FindElement(By.Name("cardNumber")).SendKeys("1234 5678 9012 3456");
// Switch back to main content
driver.SwitchTo().DefaultContent();
Best Practice:
Always switch back to the default content after interacting with the iframe.
Shadow DOM: Challenges and Locator Techniques
Shadow DOM is used in modern front-end frameworks (like Lit, Stencil, or Web Components) to encapsulate HTML and CSS, hiding it from the regular DOM tree. Selenium cannot access shadow DOM elements using traditional locators.
Solution: Use JavaScript execution to pierce the shadow DOM.
Example:
HTML (simplified):
<custom-element>
#shadow-root
<input id="email">
</custom-element>
Selenium C# Example:
var shadowHost = driver.FindElement(By.CssSelector("custom-element"));
IJavaScriptExecutor js = (IJavaScriptExecutor)driver;
var shadowRoot = (IWebElement)js.ExecuteScript("return arguments[0].shadowRoot", shadowHost);
var input = shadowRoot.FindElement(By.CssSelector("#email"));
input.SendKeys("test@example.com");
Note: Not all browsers support full shadow DOM access via WebDriver, so check compatibility before implementing.
Modals, Popups, and Dropdowns
Modals and popups are common UI components, but since they are often dynamically loaded or overlayed, timing and visibility issues can arise.
Example: Handling a confirmation modal
HTML:
<div class="modal">
<p>Are you sure?</p>
<button id="confirm">Yes</button>
</div>
Selenium C# Example:
// Wait until modal is visible
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
wait.Until(ExpectedConditions.ElementIsVisible(By.Id("confirm")));
// Interact with modal
driver.FindElement(By.Id("confirm")).Click();
Tip: Use WebDriverWait
to ensure the modal is ready before interacting.
Hidden or Off-Screen Elements
Some elements might be hidden using CSS (display: none or visibility: hidden) or rendered off-screen. Attempting to interact with them directly can result in exceptions.
Techniques:
- Scroll into view
- Use JavaScript click
- Wait for visibility
Example: Scroll and click a hidden button
Selenium C# Example:
var button = driver.FindElement(By.Id("loadMore"));
((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].scrollIntoView(true);", button);
button.Click();
Or use JavaScript click if Selenium fails:
((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].click();", button);
Summary
Navigating complex UI components like iframes, shadow DOM, modals, and hidden elements requires specific techniques beyond basic locators. Here’s a quick recap:
Component | Solution |
---|---|
Iframes | Use driver.SwitchTo().Frame() |
Shadow DOM | Access via JavaScriptExecutor |
Modals | Wait for visibility |
Hidden Elements | Scroll or JS click |
By using the right approach for each situation, you’ll make your automation framework more resilient and reliable even for advanced, modern UIs.
Locator Discovery Tools and Techniques
Efficient and accurate locator strategies begin with the right discovery tools. Whether you’re building locators for Selenium, Playwright, or Cypress, using the right tools will significantly reduce debugging time and help ensure your automation scripts are stable and maintainable.
Here are some widely used tools and techniques for discovering and verifying web element locators.
Chrome DevTools
Use Case: Built-in tool in Chrome for inspecting, testing, and copying locators.
How to Use:
- Right-click any element in Chrome → Inspect.
- Right-click the selected HTML element → Copy → Choose Selector or XPath.
Example: If you inspect a button:
HTML: <button id="submitBtn">Submit</button>
You can copy: //button[@id='submitBtn']
Tip: Use $x("your_xpath")
or document.querySelector()
directly in the Console tab to test locators.
Playwright Inspector
Use Case: A live UI debugger for identifying elements and recording interactions in Playwright.
Features:
- Automatically highlights elements on hover.
- Generates code snippets in JavaScript, TypeScript, Python, etc.
- Lets you validate selectors in real time.
Example Output: await page.getByRole('button', { name: 'Submit' }).click();
Bonus: Playwright Inspector opens automatically when you run tests in debug mode.
Cypress Selector Playground
Use Case: Helps Cypress users locate elements with optimal selectors.
How to Use:
- Launch your Cypress test runner.
- Open the app in the test browser.
- Hover and click on elements in the Playground to view recommended selectors.
Example Output: cy.get('[data-testid="login-button"]').click();
Note: Cypress prefers using custom data-*
attributes for testability and best practices.
Browser Extensions: SelectorHub
These are Chrome extensions designed to enhance and simplify locator discovery.
- Auto-generates multiple XPath and CSS options.
- Validates locators in real-time.
- Supports Shadow DOM, iFrame, and relative XPath.
Example from SelectorHub:
//input[@placeholder='Email']
VS Code Plugins and Snippet Tools
For those building tests directly in VS Code, plugins and extensions can help autocomplete locators and generate code snippets.
Recommended Tools:
- Playwright Test Snippets: For Playwright-specific selector syntax.
- Selenium Snippets: For generating WebDriver code blocks.
- XPath Autocomplete: For crafting XPath expressions faster.
Example: Typing By.XPath("//)
will suggest common XPath patterns for you to select and use.
Summary
Using the right locator discovery tools can save hours of manual inspection and debugging. Here’s when to use each:
Tool | Best For |
---|---|
Chrome DevTools | Quick, built-in inspection |
Playwright Inspector | Live debugging and selector testing |
Cypress Selector Playground | Cypress-friendly selectors |
SelectorHub/ChroPath | Complex XPath and CSS discovery |
VS Code Extensions | Streamlining test creation in code |
Mastering these tools ensures your locators are reliable, readable, and maintainable across test automation projects.
Best Practices for Writing and Maintaining Locators
In UI automation, the stability and maintainability of your test scripts heavily rely on how well you write and manage locators. Poor locator strategies often lead to flaky tests, increased maintenance, and slower test execution.
This section outlines best practices that will help you write robust and scalable locators for automation frameworks like Selenium, Playwright, and Cypress with C# examples using Selenium WebDriver.
Prefer Short, Stable, and Readable Locators
Keep your locators as short and clear as possible. Avoid long, deeply nested XPath expressions that can easily break with minor UI changes.
❌ Bad:
driver.FindElement(By.XPath("//div[2]/div[1]/form[1]/div[4]/input"));
✅ Good:
driver.FindElement(By.Id("email"));
Or, if no ID is present:
driver.FindElement(By.CssSelector("input\[placeholder='Enter email'\]"));
Tip: Always prefer id
, name
, data-*
, or CSS class-based selectors over lengthy XPaths.
Avoid Brittle Absolute XPaths
Absolute XPaths (/html/body/div[1]/div[2]/form/input
) rely on the exact structure of the page and will break if even a single layer changes.
Use relative XPaths or CSS selectors instead:
driver.FindElement(By.XPath("//form//input[@type='email']"));
Or:
driver.FindElement(By.CssSelector("form input[type='email']"));
Use Semantic Attributes Where Possible
Attributes like placeholder
, aria-label
, and alt
not only improve accessibility but also make your locators more meaningful.
Example:
driver.FindElement(By.CssSelector("input[placeholder='Search products']"));
Or:
driver.FindElement(By.XPath("//button[@aria-label='Close']"));
Add data-* Attributes for Testing (Collaborate with Developers)
Collaborate with developers to include custom data-testid or data-qa attributes in the application code. These are stable, purpose-built for testing, and do not affect the UI.
HTML:
<button data-testid="submit-login">Login</button>
Selenium C# Example:
driver.FindElement(By.CssSelector("button[data-testid='submit-login']")).Click();
Group Locators Logically in the Page Object Model (POM)
Instead of hardcoding locators in every test, group and manage them in POM classes. This reduces duplication and makes locator maintenance easier.
Example (Page Object Class in C#):
public class LoginPage
{
private IWebDriver driver;
public LoginPage(IWebDriver driver) => this.driver = driver;
public IWebElement UsernameInput => driver.FindElement(By.Id("username"));
public IWebElement PasswordInput => driver.FindElement(By.Id("password"));
public IWebElement LoginButton => driver.FindElement(By.CssSelector("button[type='submit']"));
}
In your test:
var loginPage = new LoginPage(driver);
loginPage.UsernameInput.SendKeys("admin");
loginPage.PasswordInput.SendKeys("password123");
loginPage.LoginButton.Click();
Summary Table
Practice | Benefit |
---|---|
Use short, clear locators | Easier to read and maintain |
Avoid absolute XPaths | Reduces fragility |
Leverage semantic HTML attributes | Increases stability and clarity |
Introduce data-* attributes | Enables stable selectors for QA |
Use Page Object Model (POM) | Keeps code clean and reusable |
Framework-Specific Locator Examples in UI Automation
Different automation frameworks offer unique ways to define and use locators. Understanding these differences helps write clearer, faster, and more stable test scripts. Here’s a quick comparison of how locators are used in popular automation tools like Selenium, Playwright, Cypress, and Appium, with practical examples in relevant languages.
1 Selenium WebDriver
Supports Java, C#, Python, etc. Common locator strategies include By.Id, By.XPath, By.CssSelector, etc. Example (C#):
// Locate by ID
driver.FindElement(By.Id("username")).SendKeys("admin");
// Locate by XPath
driver.FindElement(By.XPath("//button[text()='Login']")).Click();
// Locate by CSS
driver.FindElement(By.CssSelector("input[type='password']")).SendKeys("12345");
Best For: Web testing across multiple browsers with strong language support.
2 Playwright
Supports TypeScript, JavaScript, C#, and Python. Known for smart auto-waiting and modern selector engines like getByRole, getByLabel, etc.
Example (TypeScript):
await page.getByPlaceholder('Email').fill('test@example.com');
await page.getByRole('button', { name: 'Submit' }).click();
Example (C#):
await page.GetByPlaceholder("Email").FillAsync("test@example.com");
await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();
Best For: Modern apps, rich interactions, and handling asynchronous behaviors.
3 Cypress
Focused on end-to-end testing for modern web apps (JavaScript). Uses jQuery-style cy.get() with a strong preference for data-* attributes.
Example (JavaScript):
cy.get('input[name="email"]').type('user@test.com');
cy.get('button[type="submit"]').click();
With data-testid
:
cy.get('[data-testid="login-btn"]').click();
Best For: Fast and reliable testing for React, Angular, Vue, and similar front-end frameworks.
4 Appium
Used for mobile (iOS, Android) and hybrid apps. Supports XPath, accessibility IDs, and platform-specific locators.
Example (Android – XPath):
driver.findElement(By.xpath("//android.widget.TextView[@text='Login']")).click();
Example (iOS – Accessibility ID):
driver.findElement(MobileBy.AccessibilityId("LoginButton")).click();
Best For: Native and hybrid mobile app automation.
Summary Comparison Table
Framework | Language Support | Locator Example | Best Use Case |
---|---|---|---|
Selenium | Java, C#, Python | By.Id , By.XPath , By.CssSelector | Web testing across browsers |
Playwright | TypeScript, C#, Python | getByRole , getByLabel , locator() | Modern web UIs with async flows |
Cypress | JavaScript | cy.get() , cy.contains() | Fast web testing in JS projects |
Appium | Java, Python, etc. | By.xpath , MobileBy.AccessibilityId | Native/hybrid mobile apps |
Common Mistakes to Avoid in Locator Strategy
Writing effective locators is crucial for stable and maintainable UI automation. However, many teams fall into common traps that lead to flaky tests, longer debugging sessions, and high maintenance overhead. In this article, we highlight key locator mistakes to avoid, along with practical examples and better alternatives you can apply right away.
Using Over-Complicated or Deeply Nested Locators
Long XPath chains with multiple parent-child levels are fragile and easily break with minor UI changes. Example (Bad):
driver.FindElement(By.XPath("//div[2]/div[1]/form/div[3]/input"));
Better:
Use a unique attribute or short, relative path:
driver.FindElement(By.Id("email"));
Not Updating Locators After UI Changes
When developers update the UI (e.g., changing class names or nesting), failing to update your locators will break your tests.
Old locator:
driver.FindElement(By.ClassName("btn-submit")).Click();
After UI update (new attribute added):
driver.FindElement(By.CssSelector("button[data-testid='login-button']")).Click();
Tip: Maintain a central locator repository using the Page Object Model to track changes easily.
Relying on Dynamic Class Names
Frameworks like React or Angular often generate dynamic or hashed class names, which change across builds.
Example:
driver.FindElement(By.ClassName("x4ds8f2")).Click(); // may change after deployment
Better:
driver.FindElement(By.XPath("//button[text()='Submit']"));
Or use a stable data-*
attribute:
driver.FindElement(By.CssSelector("button[data-testid='submit-btn']"));
Using Hard-Coded Waits Instead of Explicit Waits
Fixed delays (Thread.Sleep) make tests slow and unreliable.
Example:
Thread.Sleep(5000); // waits even if element is ready
Better:
Use WebDriverWait for conditional waiting:
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
wait.Until(ExpectedConditions.ElementToBeClickable(By.Id("login"))).Click();
Using Text-Based Locators in Localized Applications
If your app supports multiple languages, text-based locators (text()=’Submit’) can break when translations are applied.
Example:
driver.FindElement(By.XPath("//button[text()='Submit']"));
Better:
Use semantic attributes:
driver.FindElement(By.CssSelector("button[data-testid='submit-btn']"));
Ignoring Accessibility and Semantic HTML
Avoiding accessibility features like aria-label or role results in poor locator quality and less inclusive testing.
Use Semantic Locators:
driver.FindElement(By.CssSelector("button[aria-label='Close']"));
Bonus: Improves test stability and helps support screen readers.
Organizing Locators in Automation Projects
In large-scale test automation, managing locators efficiently is critical. Poorly organized locators can lead to inconsistent scripts, frequent breakages, and increased maintenance. A well-structured locator strategy ensures that tests remain stable, reusable, and scalable over time.
This guide explains best practices for organizing locators in your automation project with examples suitable for Selenium with C#, but adaptable to any framework.
1 Centralizing Locators in the Page Object Model (POM)
The Page Object Model (POM) is a design pattern where each web page is represented by a class, and all locators and related methods are defined there.
Example (LoginPage.cs):
public class LoginPage
{
private IWebDriver driver;
public LoginPage(IWebDriver driver) => this.driver = driver;
public IWebElement UsernameInput => driver.FindElement(By.Id("username"));
public IWebElement PasswordInput => driver.FindElement(By.Id("password"));
public IWebElement LoginButton => driver.FindElement(By.CssSelector("button[type='submit']"));
public void Login(string user, string pass)
{
UsernameInput.SendKeys(user);
PasswordInput.SendKeys(pass);
LoginButton.Click();
}
}
Benefits:
- Locators are defined once, used many times.
- If a UI change occurs, update it in one place only.
2 Folder and File Naming Conventions
A predictable project structure improves team collaboration and scalability.
Recommended Structure:
/Pages
├── LoginPage.cs
├── DashboardPage.cs
└── UserProfilePage.cs
/Tests
├── LoginTests.cs
└── UserTests.cs
/Utilities
└── WaitHelpers.cs
Naming Tips:
- Use the page or component name for the class (LoginPage, ProductListPage).
- Match file names with class names for clarity.
- Group related pages into subfolders (e.g., /Pages/Admin/, /Pages/User/).
3 Version Control for Locator Changes
Locators change frequently due to UI updates. Using Git or another version control system helps track and manage these changes.
Example Workflow:
- Create a Git branch for locator updates (e.g., feature/update-login-locators).
- Commit locator file changes with meaningful messages:
Updated login button locator to use data-testid attribute - Use pull requests to review and validate updates with teammates.
4 Locator Maintainability and Scalability Tips
Do:
- Prefer By.Id, By.Name, or data-* attributes for stability.
- Add comments to complex locators:
- // Locates the delete button for a specific user row
driver.FindElement(By.XPath("//td[text()='John']/following-sibling::td/button[text()='Delete']"));
- Reuse locators with utility functions or shared components.
Avoid:
- Hardcoding locators inside test files.
- Using brittle absolute XPaths (e.g., /html/body/div[2]/div[1]…).
- Relying on dynamic class names like .x1a3b2.
Summary:
Practice | Benefit |
Page Object Model | Cleaner, reusable, and maintainable |
Folder/file naming conventions | Improved clarity and collaboration |
Version control for locator changes | Traceable and reversible updates |
Maintainable locator practices | Reduced flakiness and better scaling |
Real-World Example
Stabilizing Flaky UI Tests with Locator Optimization
In this real-world case study, we explore how a poorly implemented locator strategy caused test flakiness and slow execution times in an e-commerce application and how applying best practices led to significant improvements.
Scenario: Flaky UI Tests in an E-Commerce Web App
An automation team was testing a mid-size e-commerce website with over 50 product categories, dynamic filters, and multiple user flows (login, cart, checkout).
The UI was dynamic, built using React, and included custom elements, modals, and tables for managing orders.
Issues caused by dynamic locators and a lack of strategy
The team faced several key issues:
- Tests failed intermittently due to:
- Use of absolute XPath (e.g., /html/body/div[3]/div[2]/ul/li[1])
- Dynamic classes generated by React (e.g., .sc-hBxehG-0.jVjwBz)
- Page load timing caused elements to become “stale”
- Difficult-to-maintain selectors were scattered across test files
As a result:
- Test pass rate dropped to 60–70%
- Debugging time increased
- Team confidence in automation declined
How Locator Optimization Improved Test Stability
The team improved their locator strategy with the following actions:
- Replaced absolute XPath with semantic locators
Example before:
driver.FindElement(By.XPath("/html/body/div[3]/div[2]/ul/li[1]")).Click();
Example after:
driver.FindElement(By.CssSelector("li[data-testid='category-electronics']")).Click();
- Worked with developers to introduce data-testid attributes
HTML:
<button data-testid="add-to-cart">Add to Cart</button>
Selenium C#:
driver.FindElement(By.CssSelector("[data-testid='add-to-cart']")).Click();
- Implemented the Page Object Model (POM)
Centralized all locators in page classes for maintainability.
- Added explicit waits instead of Thread.Sleep()
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
wait.Until(ExpectedConditions.ElementIsVisible(By.Id("checkout")));
Measurable improvements in test stability and execution time
Metric | Before | After |
Test stability (pass rate) | ~65% | >95% |
Avg. test execution time | 18 mins | 12 mins |
Maintenance effort | High | Low |
Team productivity | Poor | Improved |
The new strategy improved overall test reliability, execution speed, and team confidence.
Key Takeaways
- Dynamic UI elements need flexible, stable locators
- Semantic attributes like data-testid are ideal for automation
- Centralizing locators via POM simplifies maintenance
- Avoid brittle locators (e.g., deeply nested XPath, dynamic class names)
Checklist: Writing Stable Locators
Locators are the foundation of UI test automation. A well-written locator improves test reliability, makes scripts easier to maintain, and reduces false failures. Below is a practical checklist every QA engineer should follow when writing locators along with real-world examples using Selenium with C#.
- Is the Locator Unique and Static?
A locator must uniquely identify one element only and remain unchanged unless the UI logic itself changes.
Good Example:
driver.FindElement(By.Id("email"));
Bad Example:
driver.FindElement(By.ClassName("input-text")); // reused across multiple fields
Tip: Use id, name, or custom data-* attributes for uniqueness.
- Does It Rely on Meaningful Attributes?
Use attributes that describe the purpose of the element, such as placeholder, aria-label, or data-testid.
Good Example:
driver.FindElement(By.CssSelector("input[placeholder='Enter your email']"));
With data-testid:
driver.FindElement(By.CssSelector("[data-testid='submit-login']"));
These are less likely to change during design updates than generic class names.
- Can It Survive UI Changes?
Avoid brittle locators that break when the DOM structure changes (e.g., nested or indexed XPath).
Fragile XPath:
driver.FindElement(By.XPath("//div[2]/form/div[4]/input"));
Resilient XPath:
driver.FindElement(By.XPath("//input[@type='email']"));
Prefer relative paths and semantic attributes over position-based locators.
- Is It Readable by Teammates?
Readable locators improve collaboration and speed up debugging. Avoid cryptic or overly complex expressions.
Clean and Clear:
driver.FindElement(By.XPath("//button[text()='Add to Cart']"));
Obscure:
driver.FindElement(By.XPath("//div[3]/button[1]"));
Always write locators that clearly communicate what you’re targeting.
- Is It Centrally Managed for Reuse?
Using a centralized structure like the Page Object Model (POM) ensures maintainability and reusability across multiple tests.
Example :
public class LoginPage
{
private IWebDriver driver;
public LoginPage(IWebDriver driver) => this.driver = driver;
public IWebElement EmailInput => driver.FindElement(By.Id("email"));
public IWebElement LoginBtn => driver.FindElement(By.CssSelector("button[type='submit']"));
}
Update once, apply everywhere this reduces duplicate effort and increases consistency.
Checkpoint | Why It Matters |
Unique and static | Ensures precise and consistent targeting |
Based on meaningful attributes | Improves resilience and readability |
Survivable across changes | Reduces test breakage after UI updates |
Readable by the team | Enhances collaboration and faster debugging |
Centrally maintained | Promotes DRY code and easier test updates |
Conclusion
When you start being mindful of and using stable locators, you’re not simply mindful of documentation and locating web elements; you are developing a stable future-proof foundation for your test automation. The value of stable locators is not simply to have one (or more) locators that find an element that appears to work today, your locators should provide a promise of stability into the future despite any UI changes or changes in the application.
By being aware of your locators, using unique and meaningful attributes, not using brittle locators like absolute XPaths, and centrally managing location strategies, you are increasing not only your effectiveness in automation, but also you are adding increased readability and maintainability for any future developer or development team, because you have implemented locators that are stable. Adding a practice like Page Object Model or utilizing semantic data-* attributes takes your test automation project from basic to a fully fledged maintainable, professional-grade test automation project.
The more you can apply these best-practice measured conditions, the more you will see your UI tests grow from brittle script-filled, trial and error scripts to stable, scalable, and maintainable pieces of test automation that have long-term value. As well, your team will see decreased flakiness, increased speed to find the probable source of failure, and increased confidence with each release.
At the end of the day, a great locator strategy is not simply a behind the scenes aspect, it is an enabler of quality, reliable automation for continuous delivery, and trust in your software.
Witness how our meticulous approach and cutting-edge solutions elevated quality and performance to new heights. Begin your journey into the world of software testing excellence. To know more refer to Tools & Technologies & QA Services.
If you would like to learn more about the awesome services we provide, be sure to reach out.
Happy Testing 🙂