Checkpoint Lab — Locators: ID, CSS, XPath, Relative Locators, and Resilient Selection
Refactor a deliberately brittle browser test into a resilient locator contract, prove uniqueness across two markup variants, preserve one fragile locator as diagnostic evidence, and document reproducibility and cleanup.
Learning objectives
- Build two local markup variants that preserve product semantics while changing incidental wrappers, classes, and DOM depth.
- Predict which brittle and resilient locators should survive each variant before executing the tests.
- Refactor absolute XPath and generated-class selectors into stable, scoped locator contracts.
- Prove locator uniqueness independently on both variants and retain one intentionally fragile locator as a diagnostic control.
- Produce an evidence packet with versions, session IDs, URLs, match counts, screenshots, and a machine-readable report.
- Write cleanup, reproducibility, and locator-governance notes that bridge into Chapter 05 element state and interactions.
1. Checkpoint acceptance contract
You will build two local versions of the same synthetic store component. Variant B intentionally changes wrappers, generated classes, DOM depth, and checkout text while preserving the domain/test contracts for product SKU, action, and checkout identity. You will predict which locators survive, run the evidence, refactor the test, and keep one fragile control only to demonstrate the diagnostic difference.
-
Mandatory path is free and local: Python, Selenium 4.47.0, one
supported local browser, and
python -m http.server. - No production site, account, credential, browser cloud, Grid, proxy, certificate change, or external identity system is required.
- Every browser session must quit in
finally. - Uniqueness must be proved with match counts on both variants.
- The evidence packet must preserve the intentionally brittle result rather than retrying until it passes.
2. Setup and preflight
Create an empty directory named
selenium-locator-checkpoint. Use the activation command
for your shell, then pin the binding:
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 sys, selenium; print(sys.version.split()[0], selenium.__version__)"
The following example makes the Setup and preflight 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 sys, selenium; print(sys.version.split()[0], selenium.__version__)"
Preflight also requires a supported local Chromium-family browser. Selenium Manager may resolve the compatible driver at session creation. Record the actual browser version from returned capabilities; do not assume your local browser version from this lesson text.
3. Generate the two markup variants
Save the following as write_variants.py and run
python write_variants.py. The code writes only inside
the disposable project directory.
from pathlib import Path
site = Path("site")
site.mkdir(exist_ok=True)
variant_a = r"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Locator Checkpoint A</title></head>
<body><main>
<h1>Store</h1><p id="build-marker" data-build="locator-checkpoint-a">variant A</p>
<div class="layout-a"><section data-testid="catalog">
<article class="product generated-91ac" data-testid="product-card" data-sku="SKU-42">
<h2>Beacon</h2><div class="meta"><span data-role="price">$19</span></div>
<div class="actions"><button class="generated-btn-7f9 primary" data-action="add" aria-label="Add Beacon to cart">Add</button></div>
</article>
</section></div>
<a id="checkout" data-testid="checkout" href="#checkout">Checkout</a>
</main></body></html>"""
variant_b = r"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Locator Checkpoint B</title></head>
<body><main>
<h1>Store</h1><p id="build-marker" data-build="locator-checkpoint-b">variant B</p>
<section class="new-shell"><div class="catalog-wrap"><section data-testid="catalog">
<div class="promo">Synthetic promotion</div>
<article class="tile generated-c441" data-testid="product-card" data-sku="SKU-42">
<header><h2>Beacon</h2></header><div class="details"><div><span data-role="price">$19</span></div></div>
<footer><div class="controls"><button class="generated-btn-a22" data-action="add" aria-label="Add Beacon to cart">Add</button></div></footer>
</article>
</section></div></section>
<nav><a data-testid="checkout" id="checkout" href="#checkout">Proceed</a></nav>
</main></body></html>"""
(site / "variant-a.html").write_text(variant_a, encoding="utf-8")
(site / "variant-b.html").write_text(variant_b, encoding="utf-8")
print("wrote", site / "variant-a.html", "and", site / "variant-b.html")
Variant A contains generated class
generated-btn-7f9 and one specific wrapper structure.
Variant B changes both. The stable contracts remain
data-testid="product-card",
data-sku="SKU-42", data-action="add", and
data-testid="checkout".
4. Start the local AUT and verify both resources
The following example makes the Start the local AUT and verify both resources behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python -m http.server 8044 --bind 127.0.0.1 --directory site
In another terminal, confirm both fixture files are reachable before starting WebDriver:
python -c "from urllib.request import urlopen; print(urlopen('http://127.0.0.1:8044/variant-a.html').status, urlopen('http://127.0.0.1:8044/variant-b.html').status)"
Expected output is 200 200. If it is not, fix the local
target first; selector changes cannot repair an unreachable AUT.
5. Predict before execution
The following table organizes the key choices and evidence for Predict before execution. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Locator | Variant A prediction | Variant B prediction | Reason |
|---|---|---|---|
Generated class .generated-btn-7f9 |
1 match | 0 matches | Build/presentation identifier changes |
| Absolute XPath control | May match | Should fail | DOM wrappers/depth changed |
| Product root by test ID + SKU | 1 match | 1 match | Semantic/domain contract preserved |
| Add action scoped within product | 1 match | 1 match | Component action contract preserved |
| Checkout by test ID | 1 match | 1 match | Identity preserved although visible text changes |
Write your own prediction note before running the script. The checkpoint is about causality: after execution, compare observed counts and exceptions with these expectations.
6. Run the brittle controls and resilient contract side by side
Save this as checkpoint.py. The absolute XPath and
generated class are explicitly marked
INTENTIONALLY BRITTLE; they exist only as controls.
The production candidate uses stable component identity, scoping,
and uniqueness checks.
from __future__ import annotations
import json
from pathlib import Path
import selenium
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
BASE = "http://127.0.0.1:8044"
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)
# INTENTIONALLY BRITTLE control copied from variant A.
BRITTLE_XPATH = "/html/body/main/div/section/article/div[2]/button"
BRITTLE_CLASS = ".generated-btn-7f9"
PRODUCT = '[data-testid="product-card"][data-sku="SKU-42"]'
ADD = '[data-action="add"]'
CHECKOUT = '[data-testid="checkout"]'
report = {
"selenium": selenium.__version__,
"predictions": {
"variant_a": "brittle and resilient locators should identify the Add action",
"variant_b": "generated-class and absolute-XPath controls should fail; resilient contract should remain unique",
},
"variants": [],
}
for name in ("variant-a", "variant-b"):
driver = webdriver.Chrome()
try:
url = f"{BASE}/{name}.html"
driver.get(url)
entry = {
"name": name,
"session_id": driver.session_id,
"browser_name": driver.capabilities.get("browserName"),
"browser_version": driver.capabilities.get("browserVersion"),
"url": driver.current_url,
"fixture": driver.find_element(By.ID, "build-marker").get_attribute("data-build"),
}
# Prove resilient locator uniqueness independently.
products = driver.find_elements(By.CSS_SELECTOR, PRODUCT)
checkouts = driver.find_elements(By.CSS_SELECTOR, CHECKOUT)
assert len(products) == 1
assert len(checkouts) == 1
adds = products[0].find_elements(By.CSS_SELECTOR, ADD)
assert len(adds) == 1
assert adds[0].get_attribute("aria-label") == "Add Beacon to cart"
entry["resilient_counts"] = {
"product": len(products),
"add": len(adds),
"checkout": len(checkouts),
}
# Keep two fragile controls as diagnostic evidence, not as production locators.
entry["brittle_class_count"] = len(
driver.find_elements(By.CSS_SELECTOR, BRITTLE_CLASS)
)
try:
driver.find_element(By.XPATH, BRITTLE_XPATH)
entry["brittle_xpath"] = "matched"
except NoSuchElementException:
entry["brittle_xpath"] = "NoSuchElementException"
driver.save_screenshot(str(EVIDENCE / f"{name}.png"))
report["variants"].append(entry)
finally:
driver.quit()
(EVIDENCE / "checkpoint-report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
print(json.dumps(report, indent=2))
Run python checkpoint.py. It creates a fresh browser
session for each variant, proving that the locator contract does not
depend on reusing browser state. Both sessions are closed in
finally.
7. Expected observations and how to interpret them
- Variant A: resilient product/add/checkout counts are all 1. The generated-class control should count 1. The absolute XPath may match only if its copied structure exactly corresponds to variant A.
-
Variant B: resilient counts remain 1. The
generated-class control should be 0. The absolute XPath should
report
NoSuchElementExceptionbecause wrapper depth changed. - Checkout text: changes from “Checkout” to “Proceed,” proving why a dedicated identity can survive a content change when visible text is not the requirement.
- Session IDs: differ between variants because each iteration creates a new WebDriver session. Locator behavior is evaluated independently.
8. The locator refactor
The following example makes the The locator refactor behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
Before — brittle
ADD = /html/body/main/div/section/article/div[2]/button
ADD_STYLE = .generated-btn-7f9
After — resilient and scoped
PRODUCT = [data-testid="product-card"][data-sku="SKU-42"]
ADD = [data-action="add"]
CHECKOUT = [data-testid="checkout"]
Algorithm
1. find all PRODUCT matches; require exactly 1
2. use that product WebElement as the search context
3. find all ADD descendants; require exactly 1
4. independently require exactly 1 CHECKOUT match
5. interact only after identity/cardinality assertions pass
The refactor does more than shorten syntax. It changes the test contract from DOM geometry and generated styling to domain identity, component ownership, behavior, and explicit cardinality.
9. Required evidence packet
Preserve the following files long enough to review them; they contain only synthetic fixture state:
-
evidence/checkpoint-report.json— Selenium version, predictions, two session IDs, browser versions, URLs, fixture markers, resilient match counts, and brittle-control outcomes. -
evidence/variant-a.png— screenshot of the first local fixture. -
evidence/variant-b.png— screenshot of the refactored local fixture. -
Terminal output showing the binding version and HTTP
200 200preflight. - Your locator-contract note explaining why SKU, action, and checkout identifiers are stable while generated classes/DOM depth are not.
In a real CI system, screenshots and DOM/log evidence may contain sensitive data. Redact or restrict artifacts appropriately; this local checkpoint avoids that risk with synthetic content.
10. Verification checklist
- Both fixture URLs return HTTP 200 on loopback only.
- Both browser sessions report Selenium 4.47.0 in the project environment and record actual browser name/version in the report.
- Variant A resilient counts are product=1, add=1, checkout=1.
- Variant B resilient counts are product=1, add=1, checkout=1.
- The intentionally generated-class selector differs across variants and therefore demonstrates brittleness.
- The intentionally absolute XPath is preserved as diagnostic evidence rather than hidden by retry/sleep.
- Screenshots show only the synthetic local pages.
- Every driver is quit and no session is intentionally left open.
11. Cleanup and rollback
Stop the local HTTP server with Ctrl+C. After reviewing the evidence, delete the disposable checkpoint directory with your file manager or an ordinary platform-appropriate delete command. No system browser profile, driver installation, Grid configuration, proxy, TLS setting, account, or production data was modified by the lab.
Rollback in a real codebase means reverting the locator-contract change if it was incorrect while preserving failure evidence and discussion. It does not mean restoring a generated class or absolute XPath merely because an older build happened to match it.
12. What Chapter 04 adds to the production operating model
You now have a locator operating discipline: define stable identity, narrow the search context, prove cardinality, use XPath for real relationships rather than DOM archaeology, treat relative position as geometry, and diagnose failures by evidence before changing selectors. This turns locator changes into reviewable compatibility decisions instead of CI whack-a-mole.
Chapter 05 moves from which node? to what state is that node in and what does an interaction mean? It will distinguish presence, visibility, enabled/selected state, form behavior, validation, and WebDriver interaction semantics.
Knowledge check
Why do both variants use fresh WebDriver sessions?
It proves the locator contract independently of prior browser/session state and records distinct session identities for reproducibility.
What does a 0 count for .generated-btn-7f9 on variant B prove?
Only that the generated styling class changed. It does not prove the Add behavior disappeared; the scoped semantic locator checks that separately.
Why is changing checkout text from “Checkout” to “Proceed” useful in this lab?
It demonstrates that content can legitimately change while a stable identity contract remains the same, unless text itself is the requirement.
What makes the refactored Add locator more resilient?
It first identifies the domain component by test ID + SKU, then finds the action within that component, and explicitly requires one match at each layer.
The absolute XPath fails on variant B. Why should you not add a retry?
The failure is deterministic structural coupling, not transient readiness. Retrying cannot restore the old DOM path and would only hide the root cause.
What new question does Chapter 05 answer after a locator succeeds?
Whether the referenced element is in the correct observable/interactable state—visible, enabled, selected, editable, validated, and ready for the intended WebDriver interaction.
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.