Locators: ID, CSS, XPath, Relative Locators, and Resilient Selection: Core Concepts and Mental Model
Build a precise locator mental model: search context, strategy and value, matching node set, WebElement references, semantic identity, scoped search, and the maintenance consequences of coupling selectors to incidental DOM structure.
Learning objectives
- Distinguish a DOM search context, locator strategy/value, matching node set, and returned WebElement reference.
- Explain why locator intent should track stable UI semantics rather than incidental DOM depth, generated classes, or styling.
- Compare Selenium’s traditional locator strategies and the failure behavior of find_element versus find_elements.
- Use component scoping to narrow a search and make uniqueness an explicit test property.
- Explain relative locators as geometry-based selection and why responsive layout can change their result.
- Record session, URL, title, DOM-marker, and match-count evidence before changing a locator contract.
1. Why locator design is test engineering, not selector trivia
Chapter 03 established that every WebDriver command is scoped to a concrete session. The next problem is more local but equally important: which DOM node does a test mean? A browser can contain thousands of nodes, repeated components, generated classes, localized text, and markup that changes without changing user behavior. A locator is the contract that turns test intent into a node reference.
A weak locator says “the third button inside the second wrapper.” A resilient locator says “the checkout control,” “the product card for SKU-42,” or “the add action inside that card.” The second form survives harmless markup refactors because it couples to behavior or stable identity rather than incidental structure.
2. The locator pipeline: context → matches → element reference
A search context is the object within which
Selenium searches. The driver searches the current document; a
WebElement can itself become a narrower search context for
descendant elements. Selenium combines that context with a locator
strategy such as ID or CSS and a locator value such as
checkout or [data-testid="checkout"]. The
browser evaluates the locator and produces a set of matching DOM
nodes. Selenium then returns one or more protocol-backed
WebElement references.
The following diagram visualizes the relationships described in The locator pipeline: context → matches → element reference. Read the nodes in sequence and use the arrows to connect the conceptual state changes to the explanation around the diagram.
flowchart TD I[Test intent] --> C[Search context] C --> L[Locator strategy + value] L --> M[Matching DOM node set] M --> E[WebElement reference] E --> O[Read or interact] O --> V[Assertion + evidence]
Every arrow matters. Test intent chooses the semantic target. Search context defines where Selenium looks. The locator expresses how to identify the target. The match set determines whether the result is unique, absent, or ambiguous. The WebElement reference belongs to the current session and browsing context; it is not a copied DOM node. Later rerendering can replace that node and make the reference stale, a lifecycle issue explored in later chapters.
3. The standard locator strategies and what they actually couple to
Selenium currently exposes eight traditional WebDriver locator strategies. Their syntax differs, but the engineering question is always the same: what property are you promising will remain stable?
| Strategy | Typical expression | What it couples to | Use with care when… |
|---|---|---|---|
| ID | By.ID, "checkout" |
Unique element identity | IDs are generated or reused |
| Name | By.NAME, "email" |
Form/control name contract | Names exist for submission rather than stable test identity |
| CSS selector |
By.CSS_SELECTOR, "[data-testid=checkout]"
|
Attributes, classes, hierarchy you choose | Selector becomes a long DOM traversal |
| XPath | //article[@data-sku="SKU-42"]//button |
Structure, attributes, text, relationships | It becomes an absolute path or text/localization dependency |
| Class name | By.CLASS_NAME, "card" |
One class token | Class is generated/styling-only or not unique |
| Tag name | By.TAG_NAME, "button" |
HTML element type | Many nodes share the tag |
| Link text | By.LINK_TEXT, "Help" |
Visible anchor text | Content is localized or edited |
| Partial link text | By.PARTIAL_LINK_TEXT, "Hel" |
Part of visible anchor text | Multiple links share the text fragment |
Official Selenium guidance prefers unique predictable IDs when available and otherwise favors compact readable CSS. XPath remains valid, especially when a true ancestor/descendant or text relationship is the clearest statement of intent; the problem is not XPath itself but unnecessary traversal and hidden coupling.
4. find_element and find_elements encode different evidence
find_element returns the first match
in the chosen context. If nothing matches, Selenium raises
NoSuchElementException. That is useful when the test
contract requires “an element must exist,” but it can hide ambiguity
when two elements accidentally match.
find_elements returns a list of all matches and returns
an empty list when there are none. That makes it the better
diagnostic tool when uniqueness or cardinality matters.
from selenium.webdriver.common.by import By
checkout = driver.find_element(By.ID, "checkout")
print(checkout.tag_name)
matches = driver.find_elements(By.CSS_SELECTOR, '[data-action="add"]')
print(f"add-button count={len(matches)}")
assert len(matches) == 2
5. Stable identity: production semantics and dedicated test attributes
The strongest locator is usually one backed by an explicit product
contract. A stable id, semantic name,
accessible label, or domain attribute such as
data-sku may already express durable meaning. Teams may
also define dedicated attributes such as
data-testid when production semantics are insufficient
or too volatile.
A test attribute is not automatically superior. It becomes valuable when the UI team treats it as a reviewed contract rather than disposable test-only markup. Conversely, a CSS class created by a build tool for styling is a poor test contract even if it looks unique today.
<!-- Durable identity contracts -->
<a id="checkout" href="/checkout.html">Checkout</a>
<article data-testid="product-card" data-sku="SKU-42">
<button data-action="add" aria-label="Add Beacon to cart">Add</button>
</article>
<!-- Incidental styling/build output: do not promote this to a test contract -->
<button class="Button_root__x7F9a primary mt-2">Add</button>
6. Scope searches to the component that owns the behavior
A global selector often becomes complicated because it must distinguish one component from every similar component on the page. A component-scoped search turns the owning element into a new search context. The outer locator establishes domain identity; the inner locator describes the action within that component.
card = driver.find_element(
By.CSS_SELECTOR,
'[data-testid="product-card"][data-sku="SKU-42"]',
)
add_button = card.find_element(By.CSS_SELECTOR, '[data-action="add"]')
print(add_button.get_attribute("aria-label"))
This also improves diagnostics. If the product card is missing, the failure is at the domain-identity layer. If the card exists but its add action is missing, the failure is inside that component. One clever global selector would blur those two causes.
7. Relative locators are geometry, not identity
Relative locators ask Selenium to find an element spatially
above, below, near,
to the left of, or to the right of another
element. In current Selenium Python, construct them with
locate_with(...). The older
with_tag_name(...) helper is deprecated in 4.47.0.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with
save = driver.find_element(
locate_with(By.TAG_NAME, "button").to_right_of((By.ID, "cancel"))
)
print(save.text)
This is useful when the visual relationship is itself the test intent and stable identity is unavailable. It is also more layout-sensitive: responsive CSS can stack controls vertically, translations can change size, and viewport differences can change geometry without changing semantics. Prefer stable identity when you have it; use relative position deliberately, not as a replacement for an explicit contract.
8. Read-only state inspection before changing selectors
Before “fixing” a locator, prove the environment and page state. The following inspection reads session identity, browser capabilities, URL, title, a fixture build marker, and candidate match counts. It does not click, submit, alter storage, or reset data.
import selenium
from selenium.webdriver.common.by import By
print("selenium", selenium.__version__)
print("session", driver.session_id)
print("browser", driver.capabilities.get("browserName"), driver.capabilities.get("browserVersion"))
print("url", driver.current_url)
print("title", driver.title)
print("fixture", driver.find_element(By.ID, "build-marker").get_attribute("data-build"))
print("checkout-id-count", len(driver.find_elements(By.ID, "checkout")))
print("checkout-testid-count", len(driver.find_elements(By.CSS_SELECTOR, '[data-testid="checkout"]')))
If the URL or fixture marker is wrong, changing the selector is the wrong first move. If the correct page is loaded but the match count is zero, inspect the DOM and context. If the count is greater than one, the locator contract is ambiguous.
9. Locator quality is CI signal quality
A browser test becomes release evidence only when its failure semantics are interpretable. Stable locator contracts reduce false negatives, scoped searches make incident ownership clearer, and explicit uniqueness checks reveal ambiguous markup before it becomes intermittent pipeline behavior. Locator review therefore belongs in the same engineering conversation as API compatibility and schema contracts.
The next lesson turns this model into a disposable local workflow and records concrete evidence for each locator choice.
Knowledge check
Why can a passing find_element call still hide a locator defect?
Because it returns the first matching element. If two nodes match, the call can pass while the locator contract is ambiguous; use find_elements when you need to prove cardinality.
What is gained by searching from a product-card WebElement instead of the whole document?
The search context encodes component ownership, shortens the selector, and separates “component missing” from “action missing inside component” failures.
When is XPath a reasonable choice?
When it expresses an intentional relationship—such as a descendant of a domain-identified ancestor or a text/structure relation—more clearly than another strategy, without relying on brittle absolute DOM depth.
Why is a generated CSS class risky even if it is unique today?
Its lifecycle belongs to styling/build output rather than the product’s test contract, so harmless recompilation or redesign can change it without changing behavior.
What special risk comes with relative locators?
They depend on rendered geometry, so responsive layout, viewport, fonts, or localization can change which element is spatially related even when semantic identity is unchanged.
A selector fails in CI. What should you confirm before editing it?
Confirm the expected target/environment, session/browser identity, URL/title/build marker, browsing context, and actual match count. Otherwise a wrong page or context can be misdiagnosed as a selector problem.
Official references and version notes
- Selenium downloads — current supported-binding and Selenium Server release baseline.
- Locator strategies — current Selenium documentation for the traditional locator set and relative-locator examples.
- Finding web elements — first-match, nested-search, and multiple-element behavior.
- Tips on working with locators — Selenium project guidance on IDs, compact CSS, XPath, readability, and narrowed search scope.
-
Python relative-locator API 4.47.0
— current
locate_with/RelativeByAPI;with_tag_nameis deprecated.
Version-sensitive behavior was rechecked against current Selenium primary documentation on 2026-08-28. The mandatory examples pin Selenium Python 4.47.0, use Python 3.10+, a supported locally installed Chromium-family browser, Selenium Manager for ordinary local driver resolution, and loopback-only fixtures. Grid and WebDriver BiDi are not required in this chapter; remote execution uses the same locator semantics but has a distinct session/context and transport boundary.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.