Chapter 04Lesson 05~230 minutes

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.

CheckpointRefactorTwo variantsEvidence packetLocator governance

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 NoSuchElementException because 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.
Do not “repair” the intentionally fragile controls. Their failure is required evidence showing that incidental DOM/class contracts changed while semantic locators remained stable.

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 200 preflight.
  • 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?

What does a 0 count for .generated-btn-7f9 on variant B prove?

Why is changing checkout text from “Checkout” to “Proceed” useful in this lab?

What makes the refactored Add locator more resilient?

The absolute XPath fails on variant B. Why should you not add a retry?

What new question does Chapter 05 answer after a locator succeeds?

Next concept

Chapter 05 — WebElement State, Interactions, Forms, and Validation

Carry the resilient element references from this chapter into state inspection and user-like interactions without confusing “found” with “ready to interact.”

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.