Locators: ID, CSS, XPath, Relative Locators, and Resilient Selection: Guided Hands-On Workflow
Create a disposable local locator lab and progress from stable IDs and CSS through meaningful XPath, component-scoped searches, single-versus-multiple matches, and a current Python relative-locator example.
Learning objectives
- Create a disposable loopback-only HTML fixture and isolated Selenium 4.47.0 Python environment.
- Build stable ID and compact CSS locators before introducing XPath or relative positioning.
- Use XPath only where it expresses an intentional structural relationship that CSS does not communicate as clearly.
- Scope a descendant search from a product component and compare singular and plural match behavior.
- Use Python 4.47 relative-locator API with locate_with and interpret its layout dependency.
- Capture browser/session metadata, match counts, a screenshot, and a JSON evidence summary before teardown.
1. Lab contract and preflight
This workflow uses one disposable local web page and one local
browser session. It does not need Grid, a browser cloud,
credentials, external test accounts, or a production target. The
only network target used by the test is 127.0.0.1:8044.
selenium==4.47.0. Use Python 3.10+ and a supported
locally installed Chromium-family browser. Selenium Manager may
resolve the compatible local driver when you construct
webdriver.Chrome(). Record the actual returned
browser/session information because browser versions evolve
independently.
2. Create an isolated project environment
Create a new directory such as selenium-locator-lab.
The commands below are alternatives for activation; use the one for
your shell. Nothing requires administrator access.
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
python -c "import selenium; print(selenium.__version__)"
The following example makes the Create an isolated project environment behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
python -c "import selenium; print(selenium.__version__)"
Expected result: the final command prints 4.47.0. This
proves the binding version only. It does not prove the browser
version, driver version, target page, or future session identity;
those are different pieces of evidence.
3. Create the controlled locator fixture
Create site/index.html with the following markup. It
intentionally contains both durable contracts (id,
data-testid, data-sku,
data-action) and generated-looking styling classes
(gen-a1, gen-b2) so you can distinguish
semantics from incidental presentation.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Locator Lab</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 760px; margin: 2rem auto; }
.catalog { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.card { border: 1px solid #aaa; padding: 1rem; }
.settings-row { display: flex; gap: 1rem; align-items: center; margin-top: 2rem; }
</style>
</head>
<body>
<main>
<h1>Locator Lab</h1>
<p id="build-marker" data-build="locator-lab-v1">fixture: v1</p>
<p>Cart: <span data-testid="cart-count">2</span></p>
<a id="checkout" data-testid="checkout" href="/checkout.html">Checkout</a>
<section class="catalog" data-testid="catalog" aria-labelledby="catalog-title">
<h2 id="catalog-title">Catalog</h2>
<article class="card gen-a1" data-testid="product-card" data-sku="SKU-42">
<h3>Beacon</h3>
<span data-role="price">$19</span>
<button data-action="add" aria-label="Add Beacon to cart">Add</button>
</article>
<article class="card gen-b2" data-testid="product-card" data-sku="SKU-77">
<h3>Compass</h3>
<span data-role="price">$29</span>
<button data-action="add" aria-label="Add Compass to cart">Add</button>
</article>
</section>
<div class="settings-row">
<button id="cancel" type="button">Cancel</button>
<button data-testid="save-settings" type="button">Save settings</button>
</div>
<p><a id="help-link" href="/help.html">Help</a></p>
</main>
</body>
</html>
The fixture has two product cards, two “Add” buttons, one checkout link, and a horizontally laid-out Cancel/Save pair for the relative-locator demonstration. No JavaScript timing is involved; a missing element in this lab is therefore not supposed to be “fixed” with a sleep.
4. Serve the fixture on loopback and prove the target
Start the server in a separate terminal from the project root:
python -m http.server 8044 --bind 127.0.0.1 --directory site
Then open http://127.0.0.1:8044/ manually once or use a
non-browser HTTP check:
python -c "from urllib.request import urlopen; print(urlopen('http://127.0.0.1:8044/').status)"
Expected status is 200. This separates “AUT is not
reachable” from “Selenium cannot locate an element.”
5. Run ID, CSS, XPath, scoped, plural, and relative searches
Save this as locator_walkthrough.py. The script starts
with read-only session/page evidence, then adds locator techniques
in increasing order of coupling. It never clicks the controls
because this chapter is about selection, not interaction state.
from __future__ import annotations
import json
from pathlib import Path
import selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with
BASE_URL = "http://127.0.0.1:8044/"
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)
driver = webdriver.Chrome()
try:
driver.get(BASE_URL)
# Read-only environment and page evidence first.
summary = {
"selenium": selenium.__version__,
"session_id": driver.session_id,
"browser_name": driver.capabilities.get("browserName"),
"browser_version": driver.capabilities.get("browserVersion"),
"url": driver.current_url,
"title": driver.title,
"fixture": driver.find_element(By.ID, "build-marker").get_attribute("data-build"),
}
# 1. Stable ID.
checkout = driver.find_element(By.ID, "checkout")
assert checkout.text == "Checkout"
# 2. Compact CSS using a dedicated test contract.
cart_count = driver.find_element(By.CSS_SELECTOR, '[data-testid="cart-count"]')
assert cart_count.text == "2"
# 3. XPath only for a meaningful ancestor/descendant relationship.
price = driver.find_element(
By.XPATH,
'//article[@data-sku="SKU-42"]//span[@data-role="price"]',
)
assert price.text == "$19"
# 4. Component-scoped search.
card = driver.find_element(
By.CSS_SELECTOR,
'[data-testid="product-card"][data-sku="SKU-42"]',
)
add = card.find_element(By.CSS_SELECTOR, '[data-action="add"]')
assert add.get_attribute("aria-label") == "Add Beacon to cart"
# 5. Multiple-match evidence.
add_buttons = driver.find_elements(By.CSS_SELECTOR, '[data-action="add"]')
assert len(add_buttons) == 2
summary["add_button_count"] = len(add_buttons)
# 6. Relative locator: useful, but geometry-sensitive.
save = driver.find_element(
locate_with(By.TAG_NAME, "button").to_right_of((By.ID, "cancel"))
)
summary["relative_text"] = save.text
driver.save_screenshot(str(EVIDENCE / "locator-walkthrough.png"))
(EVIDENCE / "summary.json").write_text(
json.dumps(summary, indent=2), encoding="utf-8"
)
print(json.dumps(summary, indent=2))
finally:
driver.quit()
Run it with python locator_walkthrough.py. The browser
should open the local fixture, the script should print a JSON
summary, create a screenshot, and quit even if an assertion fails
because teardown is in finally.
6. Interpret each locator as a state query
The following table organizes the key choices and evidence for Interpret each locator as a state query. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Operation | Search context | Reads | Expected evidence |
|---|---|---|---|
By.ID, "checkout" |
Current document | Unique ID mapping | Text Checkout |
| CSS cart count | Current document | Dedicated test attribute | Text 2 |
| XPath price | Current document | SKU ancestor + price descendant relationship | Text $19 |
| Scoped Add | SKU-42 card WebElement | Descendant action contract | ARIA label for Beacon |
| Plural Add search | Current document | All matching action nodes | Count 2 |
| Relative Save | Current document + Cancel anchor geometry | Rendered layout positions | Text Save settings |
These operations read the current DOM/layout in one WebDriver session. They do not alter AUT data, cookies, storage, files, or CI state. The screenshot and JSON file are evidence artifacts created by the test process, not by the application.
7. Why the XPath in this lab is acceptable—and what would be brittle
The example XPath starts from domain identity,
data-sku="SKU-42", and asks for the descendant price
role. That relationship has a reviewable meaning. Compare it with an
absolute path such as
/html/body/main/section/article[1]/span[1]: inserting a
harmless wrapper or heading would change the path even if the
product and price semantics were unchanged.
CSS could also express this lab relationship:
[data-sku="SKU-42"] [data-role="price"]. Prefer the
form your team finds clearer and easier to diagnose; do not choose
XPath merely because it can encode more structure.
8. Make the geometry dependency visible
Resize the browser manually after the run and inspect the Cancel/Save row. In this fixed fixture the controls remain side by side, so “to the right of Cancel” is predictable. In a responsive application, a mobile breakpoint might stack them vertically. The semantic target “save settings” would remain the same while the geometry contract changes.
locate_with when constructing relative locators.
Selenium Python 4.47.0 marks with_tag_name deprecated.
9. Small challenge: choose the locator from intent
Requirement: “Find the Add control belonging to product
SKU-77, while remaining robust if other product cards
are inserted before it.” Do not use card position, global button
index, or visible product text.
A defensible design is to first locate
[data-testid="product-card"][data-sku="SKU-77"], then
search within that card for [data-action="add"]. The
outer locator owns product identity; the inner locator owns
component behavior.
10. Verification and cleanup
-
Confirm
summary.jsonrecords Selenium version, session ID, browser name/version, URL, title, fixture marker, plural match count, and relative-locator result. -
Confirm
locator-walkthrough.pngshows the loopback fixture and contains no secrets or personal data. -
Confirm the terminal running
http.serverreceived only loopback requests. - Confirm the browser closes after the script finishes; no session is intentionally left running.
- Stop the local HTTP server with Ctrl+C and delete the disposable project directory after preserving any evidence you want to review.
Knowledge check
Why does the workflow verify HTTP 200 before starting Selenium?
It separates target reachability from element-location behavior, so a server problem is not misdiagnosed as a selector problem.
Why does the script use find_elements for the Add buttons?
The expected contract is cardinality: exactly two matching actions. find_elements exposes the count; find_element would return only the first.
What does the scoped card search buy you?
It ties the action lookup to SKU-42 ownership and makes failures local to either product identity or the action inside that product.
Why is no explicit wait used in this fixture?
The page is static and served fully by a local HTTP server. Adding a wait would teach a timing solution where no timing problem exists; dynamic synchronization is handled in Chapter 06.
What should change if the layout stacks Save below Cancel?
The relative locator may no longer express the intended geometry. Prefer the stable save-settings identity or change the geometry relation only if layout itself is the test contract.
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.