mastering-web-elements-image
Mastering Web Element Locators Test Automation

Mastering Web Element Locators for Effective UI Test Automation

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.

Table of Content

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, or data-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.

  1. Dynamic Buttons
    HTML:
   <button id="save_btn_458">Save</button>

Locator:

   driver.FindElement(By.XPath("//button[contains(@id, 'save_btn')]")).Click();
  1. Alerts and Toast Messages
    HTML:
    <div class="alert-success">Successfully saved!</div>

Locator:

   driver.FindElement(By.XPath("//div[starts-with(@class, 'alert-')]")).Click();
  1. 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();    
  1. 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.

  1. 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");
  1. 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

  1. 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
  1. 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();
  1. 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();
  1. 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.

  1. 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();
  1. 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();
  1. 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.

  1. Forms
    Sometimes, the <label> and <input> are not connected via for and id. 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:

ComponentSolution
IframesUse driver.SwitchTo().Frame()
Shadow DOMAccess via JavaScriptExecutor
ModalsWait for visibility
Hidden ElementsScroll 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.

SelectorHub

  • 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:

ToolBest For
Chrome DevToolsQuick, built-in inspection
Playwright InspectorLive debugging and selector testing
Cypress Selector PlaygroundCypress-friendly selectors
SelectorHub/ChroPathComplex XPath and CSS discovery
VS Code ExtensionsStreamlining 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

PracticeBenefit
Use short, clear locatorsEasier to read and maintain
Avoid absolute XPathsReduces fragility
Leverage semantic HTML attributesIncreases stability and clarity
Introduce data-* attributesEnables 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

FrameworkLanguage SupportLocator ExampleBest Use Case
SeleniumJava, C#, PythonBy.Id, By.XPath, By.CssSelectorWeb testing across browsers
PlaywrightTypeScript, C#, PythongetByRole, getByLabel, locator()Modern web UIs with async flows
CypressJavaScriptcy.get(), cy.contains()Fast web testing in JS projects
AppiumJava, Python, etc.By.xpath, MobileBy.AccessibilityIdNative/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:

PracticeBenefit
Page Object ModelCleaner, reusable, and maintainable
Folder/file naming conventionsImproved clarity and collaboration
Version control for locator changesTraceable and reversible updates
Maintainable locator practicesReduced 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:

  1. 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();
  1. 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();
  1. Implemented the Page Object Model (POM)

Centralized all locators in page classes for maintainability.

  1. 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

MetricBeforeAfter
Test stability (pass rate)~65%>95%
Avg. test execution time18 mins12 mins
Maintenance effortHighLow
Team productivityPoorImproved

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#.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

CheckpointWhy It Matters
Unique and staticEnsures precise and consistent targeting
Based on meaningful attributesImproves resilience and readability
Survivable across changesReduces test breakage after UI updates
Readable by the teamEnhances collaboration and faster debugging
Centrally maintainedPromotes 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 🙂