Chapter 13Lesson 05~230 minutes

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.

Checkpoint labBlast radiusThree scenariosDesign rationaleChapter 14 bridge

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.

Important: in a real application, prefer a stable 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?

What evidence proves test isolation in the checkpoint?

Why do ProductCard child locators not change in the repair?

Why should the first V2 failure be preserved?

What does Chapter 14 add on top of this architecture?

Next chapter

Test Data, Parameterization, Fixtures, and Environment Configuration: Core Concepts and Mental Model

Continue with Test Data, Parameterization, Fixtures, and Environment Configuration: 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 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.

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