From qa-test-review
Pure reference catalog of the canonical object-model architecture patterns for test automation frameworks - Page Object Model (Fowler), Screenplay (Marcano/Palmer/Hill), Component Object, App Actions (Cypress idiom), Service Object, Repository, and Screen Object (the desktop/mobile sibling of Page Object covering Windows UIA, macOS XCTest, Linux AT-SPI, Appium / Espresso) - each with its canonical citation, when-to-use rules, refuse-to-mix anti-patterns, and a worked example. Distinct from `test-code-conventions` (file-level §1-§10) and from per-framework skills (`playwright-testing` etc., tool-specific configuration). Preloaded by `framework-architecture-auditor` and `playwright-codegen-reviewer` as the architecture-tier reference for what each pattern actually is.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-test-review:object-model-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill is a **pure reference** - no execution steps; it is the canonical catalog the [`framework-architecture-auditor`](../../agents/framework-architecture-auditor.md) and [`playwright-codegen-reviewer`](../../../qa-web-e2e/agents/playwright-codegen-reviewer.md) cite to determine "what good looks like" per pattern. The catalog complements [`test-code-conventions`](../test-code-conventions/S...
This skill is a pure reference - no execution steps; it is the canonical catalog the framework-architecture-auditor and playwright-codegen-reviewer cite to determine "what good looks like" per pattern. The catalog complements test-code-conventions (which is file-level §1-§10) with the architecture-tier vocabulary.
Do not use this skill to:
playwright-testing, cypress-testing, etc.).framework-choice-advisor.framework-architecture-auditor, which preloads this skill.Canonical source: Martin Fowler's PageObject definition (the bliki article is the cross-language canonical reference) + Selenium HQ documentation on Page Object Models.
Fowler's definition: "A page object wraps an HTML page, or fragment, with an application-specific API, allowing you to manipulate page elements without digging around in the HTML."
Selenium HQ's elaboration: "A page object is an object-oriented class that serves as an interface to a page of your AUT… There is a clean separation between the test code and page-specific code, such as locators."
The three load-bearing rules:
addToCart, submitOrder), not the DOM mechanic (clickButton, typeIntoField).| Anti-pattern | Why it fails |
|---|---|
| Assertions inside the POM | Couples the page model to test outcomes; reuse across tests becomes brittle |
void-returning navigation methods | Loses the compile-time check Fowler explicitly identifies as the pattern's benefit |
clickAddToCartButton() instead of addToCart() | Couples the test vocabulary to UI mechanics - when the UI changes, every test changes |
| Exposing the underlying WebDriver / Page instance through public POM methods | Leaks framework details into tests; defeats the encapsulation |
| One God-POM serving five pages | Violates single-responsibility; bigger refactor cost than the POM was supposed to prevent |
Canonical source: Antony Marcano, Andy Palmer, and Jan Molak - Serenity BDD documentation on Screenplay; origin paper Marcano & Hill 2007 "Page Objects Refactored: SOLID Steps to the Screenplay Pattern."
The Screenplay vocabulary (Serenity BDD docs):
| Term | Definition |
|---|---|
| Actor | The user or system performing tasks. "In Screenplay we model actors who interact with an application in various ways to perform tasks that help them achieve their goals." |
| Ability | A capability that enables actors to perform tasks (e.g., BrowseTheWeb, CallAnApi). |
| Task | A higher-level domain concept that groups Interactions (e.g., Login, AddToCart). |
| Interaction | A low-level operation (click, type, fetch). |
| Question | A query about system state used in assertions (e.g., TheCartTotal.value()). |
Why Screenplay vs POM: Screenplay separates what the user does (Tasks, Interactions) from what the user can do (Abilities) from what the user observes (Questions). The result is a SOLID-aligned object model that survives UI refactors better than POM in large suites.
| Anti-pattern | Why it fails |
|---|---|
| Mixing Screenplay and POM in the same codebase | Doubles the maintenance surface; engineers can't tell which to write |
| Tasks that do not call Interactions (Task = re-named POM method) | Loses the Screenplay benefit; the team got Page Object Model under a different name |
| Question classes that mutate state | Violates the Question's "pure observation" contract; assertions on observations fail unpredictably |
| Abilities used as a junk-drawer for utilities | The Ability should grant a real capability; using it as a service-locator defeats the dependency-injection benefit |
Canonical source: Selenium HQ docs (Page Components are part of the official POM extension) + practitioner consensus (testing-library, Storybook, Playwright Component Testing). Treated as a refinement of POM, not a competing pattern.
Definition: A Component Object is a Page Object scoped to a UI component (header, nav, form, modal, card) rather than a whole page. Where a page contains a re-used component (the navbar appears on every page), the Component Object models that component once; each Page Object that contains it composes it in.
| Anti-pattern | Why it fails |
|---|---|
| Modelling every DOM element as a Component Object | Component Objects are for re-used components, not every <div> |
| Component Objects that hold cross-component state | Violates the encapsulation; the component should not know which page contains it |
| Page Objects that bypass the Component Object and target its internals | The Component Object's locators get duplicated; refactor leakage |
Canonical source: Kent C. Dodds and the Cypress team - "Stop using Page Objects and Start using App Actions" (Cypress blog).
Definition: App Actions bypass the UI for setup steps by exposing application functions (Redux dispatches, store mutations, API calls) directly via cy.window().its('app') or equivalent. The test still asserts via the UI; only the Arrange phase is short-circuited.
Why App Actions vs POM: "Logging in" is not what the test is about - it's overhead. App Actions skip the login UI flow and inject a session directly, making the test 10× faster and removing flake from the login form.
| Anti-pattern | Why it fails |
|---|---|
| App Actions for the Act phase (the thing under test) | The test no longer verifies the UI path under test |
| App Actions that aren't documented as test-only surface | Production code accidentally depends on the test-only API |
| Mixing App Actions and POM without convention | Engineers can't tell which to use; the suite forks |
| App Actions for end-to-end smoke / critical-path tests | Critical paths must exercise the full UI; App Actions skip the very thing the smoke proves |
Canonical source: Ruby on Rails / Java enterprise testing patterns + practitioner blog consensus. Refinement of POM for non-UI test layers.
Definition: A Service Object is the API-test equivalent of a Page Object - it wraps a remote service (REST endpoint, GraphQL query, gRPC method, message-queue producer) with a domain API the test consumes. Methods like cartService.addItem(sku, qty) rather than httpClient.post('/api/cart/items', { sku, qty }).
| Anti-pattern | Why it fails |
|---|---|
| Service Object that re-implements the production service (mocks-in-disguise) | Tests against a fake instead of the real service; misses contract drift |
| Service Object with assertions inside | Same anti-pattern as POM assertions - couples model to test outcomes |
| Single Service Object for 10 different services | Violates single-responsibility; the object becomes a god-client |
| Service Object that handles retries / circuit breakers identical to production | Tests pass because the Service Object hides the failures the test should catch |
Canonical source: Martin Fowler's Repository pattern (originally for domain-driven design) adapted for test-data setup. Practitioner adoption in 2020+ test frameworks (factory libraries layer on top).
Definition: A Repository in test context is the data-access abstraction that hides the storage mechanism (DB, fixture file, factory call) behind a domain API: userRepo.createTestUser({ role: 'admin' }).
| Anti-pattern | Why it fails |
|---|---|
| Repository that mixes test setup with production data fetching | Production code accidentally adopts test-only quirks |
| Repository methods that return mutable objects shared across tests | Test cross-coupling; one test mutates and breaks another |
| Repository that creates "magic" data the test doesn't see | Tests pass for inscrutable reasons; debugging is impossible |
Canonical source: Martin Fowler's PageObject article - the current bliki entry opens with the note that "An object that wraps an HTML page, or fragment, with an application-specific API." The earlier name WindowDriver (Fowler, 2004) covered desktop GUI windows under the same encapsulation principle before the term migrated to web. The desktop / mobile community reuses the structurally-identical pattern under the name Screen Object (one class per logical screen, locators + actions encapsulated, no assertions inside). No single owner formally documents the rename - screen object is community-canonical across FlaUI, XCUITest, Appium / Espresso practitioner literature.
The mobile sibling is documented inside Google's Android testing guidance as Screen Robot (Jake Wharton - Instrumentation Testing Robots (2016)) and inside Square's mobile literature as well; both reproduce the same encapsulation contract.
The three load-bearing rules transfer unchanged from POM:
window.Title, element.IsEnabled, control-pattern state; the Screen Object exposes those via getters but does not verify them.login.SubmitsCredentials() returns MainScreen. Compile-time detection of broken workflows survives the migration from web POM to desktop Screen Object.login.SubmitsCredentials(creds) not login.LoginButton.Click(). Methods are named after the user-meaningful action - same vocabulary rule as POM.desktop-test-strategy-reference: Windows UIA (FlaUI, WinAppDriver, Appium-Windows), macOS XCTest (XCUIApplication / XCUIElementQuery per Apple's Testing with Xcode UI Testing chapter), Linux AT-SPI (dogtail / pyatspi).| Anti-pattern | Why it fails |
|---|---|
Screen Object that hard-codes AutomationId strings inline in every method (e.g. cf.ByAutomationId("LoginButton") repeated) | Refactor cost when the developer renames the AutomationId; centralise the constant at the top of the Screen class |
| Screen Object that wraps a single accessibility-tree call without adding a domain method | Same anti-pattern as the POM clickAddToCartButton() smell - Screen exposes mechanic, not service |
| Screen Object that asserts on accessibility properties (role, label) it controls | Asserting on internal state defeats the no-assertions rule; assertions belong in the test |
Screen Object that calls Thread.Sleep / Task.Delay between actions | Hides flakiness; route through the driver's retry primitive (FlaUI Retry.WhileNull, XCTest waitForExistence) |
| Screen Object that depends on absolute window coordinates | Defeats the accessibility-tree abstraction; multi-monitor / DPI / locale breaks the test |
| One Screen Object class per dialog AND per main view in the same screen | Modal sub-screens are nested Screen Objects; do not flatten |
Bad (mechanical leakage into the test body - same shape as the web POM anti-pattern):
[StaFact]
public void Logs_in_with_valid_credentials() {
var window = _fx.App.GetMainWindow(_fx.Automation);
window.FindFirstDescendant(cf => cf.ByAutomationId("Username")).AsTextBox().Enter("[email protected]");
window.FindFirstDescendant(cf => cf.ByAutomationId("Password")).AsTextBox().Enter("hunter2");
window.FindFirstDescendant(cf => cf.ByAutomationId("LoginButton")).AsButton().Invoke();
Assert.Equal("Invoices", _fx.App.GetMainWindow(_fx.Automation).Title);
}
Good (Screen Object at the business layer):
[StaFact]
public void Logs_in_with_valid_credentials() {
var login = new LoginScreen(_fx.App.GetMainWindow(_fx.Automation));
var main = login.SubmitsCredentials("[email protected]", "hunter2");
Assert.Equal("Invoices", main.Title);
}
The mechanics live inside LoginScreen (constants for AutomationIds, retry-wrapped element fetches, SubmitsCredentials returns the next Screen Object). The test reads as a specification.
The patterns are not equally good for every project. The matrix:
| Pattern | Best for | Avoid for |
|---|---|---|
| POM | Page-oriented web SUT, 3-50 engineers, classic frameworks | Component-first React/Vue (use Component Object); Cypress (consider App Actions) |
| Screenplay | Large suites (200+ tests), multiple actor types, SOLID enthusiasts | Small projects (overhead exceeds benefit); teams allergic to dependency injection |
| Component Object | React/Vue/Svelte component-architected SUT, Storybook-integrated | Server-rendered traditional pages (use POM) |
| App Actions | Cypress + Redux/store-architected SUT, setup-heavy tests | Critical-path / smoke tests (must exercise UI); SUT without programmatic state API |
| Service Object | API / integration / contract tests with 5+ services | UI-only tests (no service calls); contract tests via schemathesis (the tool generates its own client) |
| Repository | Multi-data-source projects, DB + fixture + factory in one suite | Single-source projects (overhead exceeds benefit) |
| Screen Object | Desktop / mobile SUT through any accessibility-tree backend (UIA, XCTest, AT-SPI, Appium / Espresso) | Pure web SUT (use POM); pure API tests (use Service Object) |
| Anti-pattern | Why it fails |
|---|---|
| Mixing two object-model patterns in the same codebase | Engineers can't tell which to write; vocabulary drift accelerates |
| Inheritance hierarchies >2 levels deep (BasePage → AppPage → DomainPage → SpecificPage) | Per framework-architecture-auditor §A2, depth-3+ chains break unpredictably on root-level changes |
| Page / Component / Task / Service Objects holding mutable test data | Cross-test coupling; parallel-execution breakage |
Public getter-style methods that expose locators (get loginButton()) | Defeats encapsulation; locators leak into test code |
| Object-model methods that wait, retry, or handle SUT errors | Hides flakiness; tests pass when they should fail loudly |
| Object-model classes that import test-framework assertion libraries | Implies assertions are happening inside; smell for the no-assertion rule |
framework-architecture-auditor (preloads this skill).test-code-critic (different scope: file-level §1-§10).playwright-codegen-reviewer.framework-choice-advisor.test-data-patterns (sister catalog).test-isolation-patterns (sister catalog).test-step-design-patterns (sister catalog).test-code-conventions (file-level companion).desktop-test-strategy-reference - the OS-backend reference for Screen Object's accessibility-tree substrate (UIA / XCTest / AT-SPI).test-code-conventions - file-level companion (§1-§10).framework-architecture-auditor - the reviewer that audits codebases against these patterns; preloads this skill.playwright-codegen-reviewer, spec-to-e2e-test-scaffolder - agents that apply these patterns.npx claudepluginhub testland/qa --plugin qa-test-reviewProvides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.