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.
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:
- prove initial session/browser/component state;
- predict version 1 → 2 and old-node invalidation;
- capture the expected stale-reference signal;
- reacquire and verify version 2;
- predict version 2 → 3, advance again with current references, and verify version 3;
- save final screenshot/JSON evidence;
- 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?
Only to prove the expected node-lifetime transition with a stale-reference signal. Business assertions use newly reacquired references.
What proves the test searched from the correct root?
The document-level deep-looking CSS finds nothing while the current ShadowRoot finds the expected status node.
What belongs in a component testability contract?
Supported host/root/test IDs, readiness/lifecycle signals, reference-lifetime rules, public outcomes, excluded private implementation details, and required failure evidence.
Why is reacquisition not equivalent to a blanket retry?
It occurs after a specific observed lifecycle transition and obtains a new live node from a stable contract; a blanket retry repeats an operation without establishing cause.
What state boundary is introduced next in Chapter 11?
Browser/session-held state and files: uploads, downloads, cookies, storage, and session state.
Official references and version notes
- Selenium 4.47 release notes — stable baseline pinned for this chapter.
- Selenium downloads — current stable bindings and Selenium Server/Grid versions.
- Finding web elements — Shadow DOM — official shadow-host/root/descendant pattern.
-
Selenium Python WebElement API
—
shadow_rootand WebElement semantics. -
Selenium Python ShadowRoot API
— root-scoped
find_element(s). - Selenium Python exceptions — stale, no-shadow-root, and detached-shadow-root errors.
- Selenium troubleshooting — stale elements — why node replacement invalidates an element reference.
- W3C WebDriver Working Draft — Shadow Roots — protocol references and detached-shadow-root semantics.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.