Checkpoint Lab — Page Object, Page Component, Screenplay, and Test Abstraction Patterns
Checkpoint: refactor three catalog scenarios behind Page Object and Page Component contracts, introduce a controlled DOM selector change, prove the blast radius is one locator boundary, and produce an evidence-backed design rationale showing exactly what belongs in tests, abstractions, fixtures, and infrastructure.
Learning objectives
- Build a disposable two-variant catalog fixture and record preflight/version/session assumptions.
- Run three scenarios through Page Object, ProductCard component, and one task abstraction.
- Predict browser/DOM/application changes before each scenario and verify them independently.
- Introduce a selector-only DOM contract change, preserve the first failure, and localize the repair to one abstraction constant.
- Produce an evidence packet and short design rationale covering tests, abstractions, fixture, and infrastructure.
- Prove cleanup leaves no browser session, profile, persistent app state, or unrelated repository change behind.
1. Checkpoint scenario and acceptance target
The fixture contains two semantically equivalent catalog variants.
Variant 1 uses product-root class catalog-card; Variant
2 changes only that root class to inventory-card.
Product names, prices, buttons, cart behavior, and scenario
expectations remain the same.
The checkpoint starts with the Page Object locator written for Variant 1. The three tests should pass on V1, fail observably on V2, then all pass on V2 after one locator constant is updated. That is the blast-radius proof.
data-testid contract so a cosmetic class change does
not break tests at all. This lab intentionally changes a locator
contract to make maintenance locality measurable.
2. Setup: isolated project and two fixture variants
The following example makes the Setup: isolated project and two fixture variants behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
mkdir selenium-ch13-checkpoint
cd selenium-ch13-checkpoint
python -m venv .venv
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Linux/macOS: source .venv/bin/activate
python -m pip install "selenium==4.47.0"
The following example makes the Setup: isolated project and two fixture variants behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from pathlib import Path
root = Path("site")
(root / "v1").mkdir(parents=True, exist_ok=True)
(root / "v2").mkdir(parents=True, exist_ok=True)
template = """<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Academy Catalog</title>
<style>body{font-family:system-ui,sans-serif;max-width:900px;margin:2rem auto;padding:0 1rem}.toolbar,article,header{border:1px solid #8886;border-radius:.6rem;padding:1rem;margin:.7rem 0}#catalog{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:.7rem}button,input{padding:.45rem .65rem}</style></head>
<body><header><h1>Academy Catalog</h1><p>Cart: <strong id="cart-count">0</strong></p><p id="last-action">ready</p></header>
<div class="toolbar"><label>Filter <input data-testid="filter" id="filter" autocomplete="off"></label></div><section id="catalog"></section>
<script>
const products=[{sku:'adapter',name:'USB-C Adapter',price:'19.00'},{sku:'cable',name:'Fiber Cable',price:'29.00'},{sku:'sensor',name:'Lab Sensor',price:'49.00'}];
let cart=0;const catalog=document.querySelector('#catalog');const filter=document.querySelector('#filter');
function render(){const term=filter.value.toLowerCase();catalog.replaceChildren(...products.filter(p=>p.name.toLowerCase().includes(term)).map(p=>{const card=document.createElement('article');card.className='__CARD_CLASS__';card.dataset.sku=p.sku;card.innerHTML=`<h2 data-testid="product-name">${p.name}</h2><p data-testid="product-price">$${p.price}</p><button data-testid="add" type="button">Add</button>`;card.querySelector('[data-testid="add"]').addEventListener('click',()=>{cart+=1;document.querySelector('#cart-count').textContent=String(cart);document.querySelector('#last-action').textContent=`added:${p.sku}`});return card}));}
filter.addEventListener('input',render);render();
</script></body></html>"""
(root / "v1" / "index.html").write_text(template.replace('__CARD_CLASS__', 'catalog-card'), encoding='utf-8')
(root / "v2" / "index.html").write_text(template.replace('__CARD_CLASS__', 'inventory-card'), encoding='utf-8')
print((root / 'v1').resolve())
print((root / 'v2').resolve())
Save as make_fixture.py, run it, then:
The following example makes the Setup: isolated project and two fixture variants behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python make_fixture.py
python -m http.server 8783 --bind 127.0.0.1 --directory site
3. Preflight and explicit predictions
- Binding: Selenium Python 4.47.0 on Python 3.10+.
- Browser: one supported locally installed Chromium-family browser; Selenium Manager resolves the driver normally.
- Execution: local WebDriver only; no Grid/browser cloud is required.
-
Targets:
http://127.0.0.1:8783/v1/and/v2/only. - Each test creates a fresh browser session and quits it in teardown.
Predict before run: (1) each fresh session starts
with cart 0 and three products; (2) filtering to
Cable replaces product nodes but leaves one visible
product; (3) adding Adapter changes cart to 1 and last
action to added:adapter; (4) V2 should initially fail
because the page-level root-card locator no longer matches, while
the component’s child locators and test expectations remain valid.
4. Implement page, component, and task boundaries
The following example makes the Implement page, component, and task boundaries behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
class ProductCard:
NAME = (By.CSS_SELECTOR, "[data-testid='product-name']")
PRICE = (By.CSS_SELECTOR, "[data-testid='product-price']")
ADD = (By.CSS_SELECTOR, "[data-testid='add']")
def __init__(self, root):
self.root = root
def name(self) -> str:
return self.root.find_element(*self.NAME).text
def price_text(self) -> str:
return self.root.find_element(*self.PRICE).text
def add(self) -> None:
self.root.find_element(*self.ADD).click()
class CatalogPage:
CARD = (By.CSS_SELECTOR, "article.catalog-card")
FILTER = (By.CSS_SELECTOR, "[data-testid='filter']")
CART_COUNT = (By.ID, "cart-count")
LAST_ACTION = (By.ID, "last-action")
def __init__(self, driver):
self._driver = driver
WebDriverWait(driver, 3).until(lambda d: d.title == "Academy Catalog")
@classmethod
def open(cls, driver, url: str) -> "CatalogPage":
driver.get(url)
return cls(driver)
def products(self) -> list[ProductCard]:
return [ProductCard(root) for root in self._driver.find_elements(*self.CARD)]
def product_named(self, name: str) -> ProductCard:
return next(card for card in self.products() if card.name() == name)
def filter_products(self, text: str) -> None:
field = self._driver.find_element(*self.FILTER)
field.clear()
field.send_keys(text)
WebDriverWait(self._driver, 3).until(
lambda d: len(self.products()) == 1 and self.products()[0].name() == "Fiber Cable"
)
def cart_count(self) -> int:
return int(self._driver.find_element(*self.CART_COUNT).text)
def wait_for_cart_count(self, expected: int) -> None:
WebDriverWait(self._driver, 3).until(lambda d: self.cart_count() == expected)
def last_action(self) -> str:
return self._driver.find_element(*self.LAST_ACTION).text
class AddProductToCart:
def __init__(self, name: str):
self.name = name
def perform(self, page: CatalogPage) -> None:
before = page.cart_count()
page.product_named(self.name).add()
page.wait_for_cart_count(before + 1)
5. Run three scenarios with visible assertions and evidence
The following example makes the Run three scenarios with visible assertions and evidence behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
import json
import os
import unittest
from pathlib import Path
import selenium
from selenium import webdriver
from catalog import AddProductToCart, CatalogPage
BASE_URL = os.environ.get("BASE_URL", "http://127.0.0.1:8783/v1/")
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)
class CatalogCheckpoint(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.page = CatalogPage.open(self.driver, BASE_URL)
self.test_id = self.id().split(".")[-1]
(EVIDENCE / f"{self.test_id}-session.json").write_text(
json.dumps({
"selenium": selenium.__version__,
"session_id": self.driver.session_id,
"browser": self.driver.capabilities.get("browserName"),
"browser_version": self.driver.capabilities.get("browserVersion"),
"url": self.driver.current_url,
}, indent=2),
encoding="utf-8",
)
def tearDown(self):
if self.driver:
self.driver.save_screenshot(str(EVIDENCE / f"{self.test_id}.png"))
self.driver.quit()
def test_catalog_prices(self):
prices = {p.name(): p.price_text() for p in self.page.products()}
self.assertEqual(prices["USB-C Adapter"], "$19.00")
self.assertEqual(prices["Fiber Cable"], "$29.00")
self.assertEqual(prices["Lab Sensor"], "$49.00")
def test_filter(self):
self.page.filter_products("Cable")
self.assertEqual([p.name() for p in self.page.products()], ["Fiber Cable"])
def test_add_product(self):
AddProductToCart("USB-C Adapter").perform(self.page)
self.assertEqual(self.page.cart_count(), 1)
self.assertEqual(self.page.last_action(), "added:adapter")
if __name__ == "__main__":
unittest.main(verbosity=2)
Save the complete abstraction block as catalog.py and
the test block as test_catalog.py. The explicit import
makes the checkpoint runnable as shown while the separate files keep
responsibilities visible.
6. Establish the passing V1 baseline
The following example makes the Establish the passing V1 baseline behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# PowerShell
$env:BASE_URL = "http://127.0.0.1:8783/v1/"
python -m unittest -v test_catalog.py
# Bash/zsh equivalent:
# BASE_URL="http://127.0.0.1:8783/v1/" python -m unittest -v test_catalog.py
Expected: three tests pass. Preserve the three session JSON files and screenshots as synthetic evidence. Record that each test has a distinct session ID and begins from cart count zero.
7. Introduce the DOM change and preserve first failure
The following example makes the Introduce the DOM change and preserve first failure behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# PowerShell
$env:BASE_URL = "http://127.0.0.1:8783/v2/"
python -m unittest -v test_catalog.py
# Bash/zsh equivalent:
# BASE_URL="http://127.0.0.1:8783/v2/" python -m unittest -v test_catalog.py
Expected before repair: product-dependent tests fail because
CatalogPage.CARD matches zero V2 card roots. Preserve
the first traceback, V2 URL, current session/capabilities evidence,
and screenshot before changing code. Do not alter the three
assertions, component child locators, browser configuration, or
fixture behavior.
8. Apply the one-boundary repair
The following example makes the Apply the one-boundary repair behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# V2 repair: one locator contract changes in CatalogPage.
class CatalogPage:
CARD = (By.CSS_SELECTOR, "article.inventory-card")
# All other page methods and all ProductCard/test/task code stay unchanged.
Rerun the V2 suite. All three scenarios should pass. The intentional DOM change touched one Page Object locator boundary; scenario expectations, task behavior, and component-internal locators remained unchanged.
9. Prove the blast radius
The following table organizes the key choices and evidence for Prove the blast radius. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Artifact | Changed for V2? | Reason |
|---|---|---|
| three test methods/assertions | No | business behavior did not change |
| AddProductToCart task | No | workflow intent did not change |
| ProductCard child locators | No | card internals stayed stable |
| CatalogPage root-card locator | Yes — one line | page-level DOM selector contract changed |
| driver factory/browser version | No | not related to UI structure |
| fixture variant | Yes — deliberate AUT input | creates the controlled change being diagnosed |
10. Write the design rationale
The following example makes the Write the design rationale behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
Tests / scenarios
- Own expected business outcomes and failure meaning.
- Do not contain repeated product-card selectors or filter timing rules.
Page Object
- Owns catalog-level locators, readiness, filtering, cart queries.
- Does not create WebDriver, create test data, capture CI artifacts, or assert business outcomes.
Page Component
- Owns one current product-card root and card-local name/price/add mechanics.
- Is reacquired after catalog rerender; not cached across DOM replacement.
Task/domain action
- Owns repeated intent “add named product to cart” and action-completion synchronization.
- Does not hide the final scenario assertion.
Fixture / AUT
- Loopback-only synthetic catalog with V1/V2 DOM variants.
- Contains no real accounts, files, network dependencies, or secrets.
Infrastructure / evidence
- Owns Selenium/browser session creation and quit.
- Records Selenium/browser/session/URL evidence and screenshots per test.
- In CI, browser matrix/Grid configuration belongs here, not in page classes.
11. Evidence packet and independent verification
- Selenium 4.47.0 binding version and returned browser/version for each test.
- Distinct session IDs proving fresh browser isolation.
- V1 three-test pass output.
- First V2 failure traceback before repair.
- V2 URL and screenshot showing the semantically equivalent page.
- One-line locator diff for the repair.
- V2 three-test pass output after repair.
- Design-rationale record identifying ownership boundaries.
Do not capture cookies, personal profiles, credentials, external network logs, or unrelated machine paths.
12. Verification checklist
- Exactly three scenario tests exist and assertions remain in the test layer.
- V1 passes before introducing the controlled DOM change.
- V2 initially fails for the expected locator-boundary reason.
- The first failure is preserved before code changes.
- Only the CatalogPage root-card locator changes for the repair.
- V2 passes after the repair.
- Each test uses a fresh WebDriver session and quits it.
- No fixed delays, blanket retries, JavaScript interaction bypasses, or browser restarts are used to hide the failure.
- Evidence files contain only synthetic/version/session metadata.
13. Cleanup and rollback
Quit all browser sessions, stop the loopback server, preserve only
the small synthetic evidence packet if needed for review, then
delete the checkpoint project and its site//evidence/
directories. The lab did not alter a personal browser profile,
system browser policy, Grid, repository, production app, or external
account.
14. What Chapter 13 adds to the production operating model
The suite now has a maintainability architecture: scenarios own intent/assertions, page objects own page services and synchronization, page components own reusable UI regions, tasks own repeated domain workflows, infrastructure owns sessions/evidence/configuration, and dynamic elements are reacquired instead of cached. The result is a smaller, explainable blast radius when the UI changes.
Chapter 14 adds the next missing layer: deterministic test data, parameterization, fixture lifecycle, and environment configuration. Those concerns should feed the abstractions created here without being hidden inside them.
15. Summary
A good abstraction does not make Selenium disappear; it makes change ownership explicit. The checkpoint proved that a controlled DOM change can be repaired at one UI boundary while three scenario expectations and the business task remain intact. That is maintainability measured as behavior, not aesthetics.
Knowledge check
Why is the V2 change intentionally a root selector change rather than a business behavior change?
It isolates the maintenance question: a UI contract changed while scenario meaning stayed constant, so the expected repair belongs at the Page Object boundary.
What evidence proves test isolation in the checkpoint?
Each test records a distinct WebDriver session ID and starts from a fresh browser/AUT state with cart count zero.
Why do ProductCard child locators not change in the repair?
The controlled DOM change affects only the card root selector; the component’s internal semantic elements remain unchanged.
Why should the first V2 failure be preserved?
It proves the actual failing layer before the repair and prevents an abstraction change from erasing the original diagnostic signal.
What does Chapter 14 add on top of this architecture?
Deterministic test data, parameterization, fixture setup/teardown, and environment configuration as separate concerns feeding the test/abstraction layers.
Official references and version notes
- Selenium 4.47 release notes — stable binding/Grid baseline pinned for this chapter.
- Selenium downloads — current stable client and Server/Grid releases.
- Page Object Models — page services, assertion placement, page component composition, and encapsulation guidance.
- Design patterns and development strategies — page/object and command-oriented alternatives for maintainable suites.
- Domain-specific language — express test intent in user/domain terms rather than UI mechanics.
- Avoid sharing state — isolate test data and create a new WebDriver instance per test where practical.
- Fresh browser per test — clean browser/session state guidance.
- Generating application state — repetitive setup should normally use lower-layer APIs rather than browser UI.
Version-sensitive behavior was rechecked against current Selenium primary documentation on 2026-08-28. Mandatory examples pin Selenium Python 4.47.0 and Python 3.10+, use a supported locally installed Chromium-family browser with Selenium Manager, and target only loopback fixtures. Page Object, Page Component, task/DSL, and Screenplay-style structures are test-architecture patterns rather than capabilities negotiated with the browser. The mandatory path uses Python standard-library unittest for the small suite so no external test-architecture framework is required. The Screenplay-style example is intentionally framework-free and remains an organizational layer over ordinary Selenium WebDriver.
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.