Chapter 04Lesson 02~175 minutes

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.

Loopback labScoped searchUniquenessRelativeByEvidence

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.

Current baseline: pin 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.
Safety boundary: do not point these examples at a public website merely to avoid creating the fixture. A local fixture makes the DOM contract, traffic, evidence, and cleanup fully controlled.

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.

Current Python API: use 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.json records Selenium version, session ID, browser name/version, URL, title, fixture marker, plural match count, and relative-locator result.
  • Confirm locator-walkthrough.png shows the loopback fixture and contains no secrets or personal data.
  • Confirm the terminal running http.server received 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?

Why does the script use find_elements for the Add buttons?

What does the scoped card search buy you?

Why is no explicit wait used in this fixture?

What should change if the layout stacks Save below Cancel?

Next concept

Configuration, Design Patterns, and Trade-Offs

Turn successful selector syntax into an explicit locator policy: what your team promises will stay stable, what can change freely, and how you review exceptions.

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.