Chapter 10Lesson 05~200 minutes

Checkpoint Lab — Shadow DOM, Web Components, Dynamic DOMs, and Modern Front Ends

Checkpoint: automate a custom element that rerenders twice, intentionally prove a cached internal node becomes stale, reacquire through the supported host/root contract, and produce an evidence packet plus a maintainable component testability contract.

Checkpoint labTwo rerendersEvidence packetContractChapter 11 bridge

Learning objectives

  • Build a disposable Selenium project and component fixture from an empty directory.
  • Predict and verify two component-version transitions and corresponding node-reference invalidation.
  • Capture stale-reference evidence without hiding it, then reacquire current host/root/children.
  • Prove ordinary DOM, iframe, and shadow-root boundaries remain conceptually distinct.
  • Write an evidence packet and explicit testability contract for the component.
  • Finish with deterministic cleanup and connect the operating model to Chapter 11 state/files.

1. Checkpoint scenario and acceptance criteria

You own a custom profile-card. Each “Advance component” action increments a version and rerenders the internal tree. The checkpoint must:

  1. prove initial session/browser/component state;
  2. predict version 1 → 2 and old-node invalidation;
  3. capture the expected stale-reference signal;
  4. reacquire and verify version 2;
  5. predict version 2 → 3, advance again with current references, and verify version 3;
  6. save final screenshot/JSON evidence;
  7. write a testability contract explaining which surfaces are stable and which are private.

2. Setup and preflight

Create selenium-ch10-checkpoint, create/activate a virtual environment, pin Selenium, generate the same local fixture from Lesson 2, and serve it only on loopback.

python -m venv .venv
# Linux/macOS
. .venv/bin/activate
# Windows PowerShell alternative:
# .\.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__)"
python make_fixture.py
python -m http.server 8775 --bind 127.0.0.1 --directory site

Preflight assumptions: Python 3.10+, Selenium 4.47.0, one supported local Chromium-family browser, Selenium Manager normal driver resolution, no Grid required, no BiDi required, and no external service.

3. Write predictions before running

Record these predictions in predictions.txt:

Prediction 1
- initial profile-card data-version = 1
- clicking the current advance button changes app-result to app:advanced:1
- component data-version becomes 2
- the pre-click internal button reference becomes stale

Prediction 2
- after reacquisition, clicking the current advance button changes app-result to app:advanced:2
- component data-version becomes 3
- the version-2 internal nodes are replaced by version-3 nodes

Boundary prediction
- host remains discoverable from document
- internal status remains discoverable only from the current ShadowRoot
- no iframe/window/context switch is required

4. Run the checkpoint and capture evidence

The following example makes the Run the checkpoint and capture evidence behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

import json
from pathlib import Path
import selenium
from selenium import webdriver
from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

URL = "http://127.0.0.1:8775/index.html"
HOST = (By.ID, "profile-card")
ADVANCE = (By.CSS_SELECTOR, '[data-testid="advance"]')
STATUS = (By.CSS_SELECTOR, '[data-testid="status"]')
APP_RESULT = (By.ID, "app-result")
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)

def live_component(driver):
    host = driver.find_element(*HOST)
    return host, host.shadow_root

def version(driver):
    return int(driver.find_element(*HOST).get_attribute("data-version"))

def advance_and_wait(driver, wait):
    host, root = live_component(driver)
    before = int(host.get_attribute("data-version"))
    button = root.find_element(*ADVANCE)
    button.click()
    wait.until(EC.staleness_of(button))
    wait.until(lambda d: version(d) == before + 1)
    new_host, new_root = live_component(driver)
    return before, button, new_host, new_root

driver = webdriver.Chrome()
wait = WebDriverWait(driver, 5)
record = {"selenium": selenium.__version__, "transitions": []}
try:
    driver.get(URL)
    caps = driver.capabilities
    record.update({
        "session_id": driver.session_id,
        "browser_name": caps.get("browserName"),
        "browser_version": caps.get("browserVersion"),
        "platform": caps.get("platformName"),
        "url": driver.current_url,
        "title": driver.title,
        "initial_version": version(driver),
    })
    assert record["initial_version"] == 1

    # Transition 1 -> 2. Keep the old button only to prove node lifetime.
    before, old_button, host2, root2 = advance_and_wait(driver, wait)
    assert before == 1
    stale_1 = False
    try:
        _ = old_button.text
    except StaleElementReferenceException as exc:
        stale_1 = True
        record["stale_1"] = type(exc).__name__
    assert stale_1
    status2 = root2.find_element(*STATUS).text
    app2 = driver.find_element(*APP_RESULT).text
    assert version(driver) == 2
    assert "version:2" in status2
    assert app2 == "app:advanced:1"
    record["transitions"].append({"from": 1, "to": 2, "status": status2, "app": app2})

    # Transition 2 -> 3. Reacquire current controls; do not reuse version-2 nodes.
    before2, old_button2, host3, root3 = advance_and_wait(driver, wait)
    assert before2 == 2
    stale_2 = False
    try:
        _ = old_button2.is_enabled()
    except StaleElementReferenceException as exc:
        stale_2 = True
        record["stale_2"] = type(exc).__name__
    assert stale_2
    status3 = root3.find_element(*STATUS).text
    app3 = driver.find_element(*APP_RESULT).text
    assert version(driver) == 3
    assert "version:3" in status3
    assert app3 == "app:advanced:2"
    record["transitions"].append({"from": 2, "to": 3, "status": status3, "app": app3})

    # Boundary proof: document CSS does not pierce the root.
    assert driver.find_elements(By.CSS_SELECTOR, '#profile-card [data-testid="status"]') == []
    assert root3.find_element(*STATUS).text == status3

    record["final_version"] = version(driver)
    record["final_app_result"] = app3
    driver.save_screenshot(str(EVIDENCE / "final.png"))
    (EVIDENCE / "run.json").write_text(json.dumps(record, indent=2), encoding="utf-8")
    print(json.dumps(record, indent=2))
finally:
    driver.quit()

The old references are retained only as diagnostic probes. Every business assertion after a render uses newly acquired host/root/child references.

5. Write the component testability contract

Create evidence/testability-contract.md:

# profile-card testability contract

## Supported browser-test surface
- Host: `#profile-card` / `data-testid="profile-card"`
- Lifecycle evidence: host `data-version`
- Open ShadowRoot is intentionally supported for application-owned UI automation
- Stable internal semantics: `data-testid="advance"`, `data-testid="status"`, `data-testid="nickname"`
- Application outcome: `#app-result`

## Reference lifetime
- Internal WebElements are valid only for the current render generation.
- After any action that changes `data-version`, reacquire host -> ShadowRoot -> descendants.
- Tests may assert expected staleness for diagnostics; they must not retry the same detached object.

## Readiness
- Component host must exist.
- Required root/child must be discoverable.
- For asynchronously hydrated components, use a documented host readiness marker plus required child/outcome.

## Not part of the contract
- Generated CSS classes
- Internal framework IDs/keys
- Absolute shadow-tree depth beyond supported test IDs
- Private/closed-root internals
- JavaScript or browser-specific shadow-piercing techniques

## Failure evidence
- Selenium/browser/session provenance
- URL/title/current context
- host version/readiness
- exception class for stale/detached reference
- reacquired status/application result
- screenshot on first relevant failure

6. Interpret the evidence packet

run.json should show version transitions 1→2 and 2→3, two expected stale signals, browser/session provenance, and final application state. final.png is visual evidence, not the assertion oracle. The testability contract explains why reacquisition is correct engineering rather than an ad-hoc retry.

7. Negative diagnostic check: wrong root

The checkpoint explicitly verifies that a document-level selector does not find the internal status. That negative assertion proves root ownership. If a future refactor moves the control into light DOM, this contract change becomes visible rather than silently changing test behavior.

8. Safety and cleanup boundaries

The lab stores only synthetic browser metadata and fixture state. Do not add credentials, tokens, production screenshots, or customer DOM dumps to evidence. Stop the loopback server with Ctrl+C; preserve the evidence directory if required, then delete the disposable checkpoint folder.

cd ..
rm -rf selenium-ch10-checkpoint   # Linux/macOS
# Windows PowerShell alternative:
# Remove-Item -Recurse -Force .\selenium-ch10-checkpoint

9. Verification checklist

  • Initial component version was proven before mutation.
  • Both expected rerenders were observed through data-version.
  • Old internal nodes became stale and the exceptions were preserved as evidence.
  • Current host/root/children were reacquired for each post-render assertion.
  • Document-level CSS did not pierce the shadow boundary.
  • Final application state and screenshot/JSON evidence were produced.
  • The testability contract separates supported public test hooks from private implementation detail.
  • No fixed sleeps, JavaScript piercing, browser restarts, TLS changes, credentials, or production targets were used.

10. What Chapter 10 adds to the production operating model

The operating model can now identify modern component boundaries and lifecycle transitions explicitly: document versus shadow-root search context, host/root/element reference identity, hydration readiness, stale/detached recovery, and a versioned component testability contract. That makes frontend CI failures explainable across refactors and browsers.

Chapter 11 moves from DOM/component state to browser-held state and files: uploads, downloads, cookies, storage, and session state. The same rule continues—identify the state store and ownership boundary before mutating it.

11. Summary

Reliable Shadow DOM automation does not mean “find a way to reach every node.” It means automate only supported boundaries, bind waits to lifecycle state, reacquire after node replacement, and preserve evidence that explains exactly what changed.

Knowledge check

Why does the checkpoint deliberately keep an old button after each click?

What proves the test searched from the correct root?

What belongs in a component testability contract?

Why is reacquisition not equivalent to a blanket retry?

What state boundary is introduced next in Chapter 11?

Next chapter

File Uploads, Downloads, Cookies, Storage, and Session State: Core Concepts and Mental Model

Continue with File Uploads, Downloads, Cookies, Storage, and Session State: Core Concepts and Mental Model. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked against current primary documentation on 2026-08-28. Mandatory examples pin Selenium Python 4.47.0, require Python 3.10+, use a supported local Chromium-family browser with Selenium Manager for normal driver resolution, and target only loopback fixtures. Selenium documents WebElement.shadow_root for Chromium, Firefox, and Safari. The mandatory path treats closed-shadow internals as a component boundary and does not rely on JavaScript or browser-specific techniques to pierce private internals. Classic WebDriver is sufficient for the mandatory path; BiDi/CDP are not required for native ShadowRoot lookup or stale-reference diagnosis.

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.