Page Object, Page Component, Screenplay, and Test Abstraction Patterns: Core Concepts and Mental Model
As suites grow, raw Selenium code can remain technically correct while becoming expensive to change. This lesson establishes abstraction boundaries so tests speak in user intent, UI mechanics stay localized, assertions remain diagnostically useful, and browser/session infrastructure does not leak into every page class.
Learning objectives
- Explain the responsibility chain from test intent through task/page/component abstractions to WebDriver and the AUT.
- Define Page Object, Page Component, task/DSL, and Screenplay-style actor/task/question structures before using them.
- Place locators, waits, assertions, fixtures, browser creation, and evidence collection in deliberate layers.
- Explain why page objects expose page services rather than raw DOM mechanics or a globally shared driver.
- Use read-only inspection to prove session/page state before abstraction methods mutate the AUT.
- Connect abstraction design to CI maintainability, browser matrices, failure triage, and change blast radius.
1. The practical problem: duplication becomes operational debt
Chapters 1–12 built reliable browser mechanics: sessions, locators, waits, contexts, actions, dialogs, modern DOMs, files/state, and browser configuration. A suite can use all of those correctly and still become fragile if every test repeats selectors, waits, navigation rules, screenshots, and business workflows.
The abstraction problem is therefore not “how do I hide Selenium?” It is “which code should know about which kind of change?” A renamed locator should not force fifty scenario edits. A new browser option should not require changing every page object. A failed business assertion should still tell the test author what outcome was wrong.
2. Mental model: intent flows down; evidence flows back up
The following diagram visualizes the relationships described in Mental model: intent flows down; evidence flows back up. Read the nodes in sequence and use the arrows to connect the conceptual state changes to the explanation around the diagram.
flowchart TD T[Test scenario intent] --> K[Task / domain action] T --> Q[Assertion / question] K --> P[Page Object service] P --> C[Page Component] P --> L[Locator + wait policy] C --> L L --> W[WebDriver] W --> B[Browser context] B --> A[AUT DOM / application state] A --> Q Q --> T I[Test infrastructure] --> W I --> E[Evidence / teardown] E --> T
The scenario names the behavior that matters. A task or domain action may coordinate several page services. A Page Object represents services offered by one page or view. Page Components model reusable regions contained by a page. Locators and explicit waits are mechanics inside those boundaries. WebDriver still owns the browser session; infrastructure owns driver creation, evidence directories, and teardown.
Assertions and “questions” flow in the other direction: the abstraction exposes observable state and the test decides whether that state satisfies the scenario.
3. Terms and boundaries
The following table organizes the key choices and evidence for Terms and boundaries. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Term | Meaning in this chapter | Should not become |
|---|---|---|
| Page Object | Object that exposes services/observable state for one page or coherent view. | a bucket for every workflow, assertion, driver option, and screenshot |
| Page Component | Object for a reusable region such as a product card, navbar, row, or modal. | a second giant page class |
| Task/domain action | Intent-oriented operation such as “add product to cart” that may coordinate page services. | an opaque retry/sleep wrapper |
| Question/state query | Readable retrieval of observable state used by a test assertion. | an assertion whose failure context is hidden deep in helpers |
| Screenplay-style model | Optional actor → ability → task → question vocabulary for scaling intent-oriented tests. | a Selenium API or mandatory framework |
| Fixture/infrastructure | Driver lifecycle, server/base URL, evidence path, test data setup/teardown. | page-object responsibility |
4. Page Objects model services, not HTML trivia
Selenium’s current Page Object guidance treats the object as the
test-facing interface to a page. Public methods should describe what
the page offers: filter_products("Cable"),
product_named("Fiber Cable"), or
cart_count(). The object may know locators and wait
conditions, because those are implementation details the scenario
should not repeat.
class CatalogPage:
FILTER = (By.CSS_SELECTOR, "[data-testid='filter']")
CART_COUNT = (By.ID, "cart-count")
def filter_products(self, text: str) -> None:
field = self._driver.find_element(*self.FILTER)
field.clear()
field.send_keys(text)
def cart_count(self) -> int:
return int(self.driver.find_element(*self.CART_COUNT).text)
The methods are not “clickFilterField” and “readStrongTag.” They expose application-oriented services/state while keeping DOM details private.
5. Assertion placement: preserve failure meaning
The Selenium project’s Page Object guidance generally keeps test assertions in the test, with a narrow exception for validating that a newly constructed page object is actually on the page it claims to represent. This keeps “what outcome should be true?” close to the scenario while allowing the page object to throw a clear contract error if its basic page precondition is absent.
| Responsibility | Good location | Example |
|---|---|---|
| Page loaded/contract precondition | Page Object constructor/factory | wait for the catalog heading or stable root |
| Business expectation | test/scenario | assert page.cart_count() == 1 |
| Reusable observable query | Page/Page Component | product.price_text() |
| Evidence on failure | test runner/infrastructure hook | screenshot + URL + capabilities |
6. Page Components use composition
Real pages are composed of regions. A catalog contains product cards; a dashboard contains widgets; a table contains rows. The page can return component objects scoped to those regions. This keeps a product card’s name/price/add-button locators out of the page class and avoids an inheritance tree that tries to mirror DOM nesting.
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()
A component root is still a WebElement reference and can become stale after rerender. The page should reacquire components when the DOM changes rather than caching them for the life of the session.
7. Task-oriented and Screenplay-style layers
A task layer becomes useful when the same business action spans page/component services or must read like domain language. A Screenplay-style design names an actor, the actor’s browser ability, tasks the actor performs, and questions the actor asks. This chapter uses a small framework-free sketch so learners can understand the architecture without adding a dependency.
class AddProductToCart:
def __init__(self, product_name: str):
self.product_name = product_name
def perform(self, catalog: "CatalogPage") -> None:
catalog.product_named(self.product_name).add()
class CartCount:
def answered_by(self, catalog: "CatalogPage") -> int:
return catalog.cart_count()
The key distinction is responsibility, not naming. The task coordinates intent; the page/component owns UI mechanics; the test owns the expectation.
8. Read-only state inspection before mutation
The following example makes the Read-only state inspection before mutation behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
import selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
URL = "http://127.0.0.1:8782/"
driver = webdriver.Chrome()
try:
driver.get(URL)
print({
"selenium": selenium.__version__,
"session_id": driver.session_id,
"browser": driver.capabilities.get("browserName"),
"browser_version": driver.capabilities.get("browserVersion"),
"url": driver.current_url,
"title": driver.title,
"products": len(driver.find_elements(By.CSS_SELECTOR, "[data-testid='product-card']")),
"cart": driver.find_element(By.ID, "cart-count").text,
})
finally:
driver.quit()
This inspection proves which session/page/DOM state exists before any abstraction changes it. The page object does not create the browser; it receives a session whose provenance and teardown remain infrastructure concerns.
9. Keep state stores distinct
The following table organizes the key choices and evidence for Keep state stores distinct. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| State store | Owner | Abstraction rule |
|---|---|---|
| WebDriver session/profile | test infrastructure | create/quit outside page classes |
| AUT DOM/view state | browser + application | page/component methods observe or mutate through WebDriver |
| test data | fixture/setup layer | do not hide shared mutable records inside page objects |
| assertion expectation | test/scenario | keep failure meaning visible |
| screenshots/logs/report files | evidence layer | capture around failures with test/session correlation |
| Grid/CI configuration | execution infrastructure | never make a page object decide node/browser policy |
10. Common misconceptions
The following table organizes the key choices and evidence for Common misconceptions. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Misconception | Why it fails | Better boundary |
|---|---|---|
| “One BasePage should contain everything.” | It becomes a God object and couples unrelated pages to every helper change. | small shared primitives only; compose page/components |
| “Store WebElements once for speed.” | Modern rerenders invalidate node references. | store locators; reacquire at the action/query boundary |
| “Assertions in helpers make tests shorter.” | Failures become detached from scenario meaning. | return observable state; assert in test |
| “Screenplay replaces Page Objects.” | They solve different levels of abstraction and can coexist. | tasks/questions may delegate to page/component services |
| “Page Object is a Selenium Python class.” | It is a design pattern, not a required binding base class. | plain Python classes are sufficient |
11. Why this matters in DevOps
Cross-browser CI magnifies maintenance cost: every browser/version/environment can expose slightly different timing and layout behavior. Centralized locator/wait contracts reduce duplicated fixes, while visible test assertions and infrastructure evidence keep diagnosis explainable. Good abstractions also make code review meaningful: a browser-policy change belongs in infrastructure, a locator change in a page/component, and a business expectation change in the scenario.
12. Summary and next step
Page Objects localize page services and mechanics; Page Components localize reusable regions; task/DSL layers express domain actions; Screenplay-style actor/task/question structure is optional; assertions normally remain in tests; browser/test infrastructure remains outside page classes.
Knowledge check
Why should a Page Object expose “services” rather than raw DOM details?
Because tests should depend on user/application behavior while locators and layout mechanics remain localized to one maintenance boundary.
Where should a business assertion normally live?
In the test/scenario. A Page Object may validate that its page contract loaded, but scenario expectations should remain visible to the test.
Why prefer composition for Page Components?
A page contains reusable regions; composition models that relationship without coupling class inheritance to DOM nesting.
Is Screenplay a Selenium WebDriver feature?
No. It is a test-architecture style. Selenium still provides the browser automation engine underneath.
Which layer should create and quit WebDriver?
Test infrastructure or fixture/lifecycle code, not a Page Object.
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.