SoftwareTestPilot
Topic 12 of 100

Page Object Model — Definition, Architecture & Playwright Implementation

The Page Object Model turns your UI into a typed API. Selectors move out of test files, actions become method calls, and the suite survives every redesign that would otherwise trigger a hundred find-and-replace edits.

Last updated: June 2026

Section 1

Executive Definition

The Page Object Model, or POM, is a design pattern for UI automation in which every screen or major component of an application is represented by a class. That class owns the selectors for its elements, exposes methods that perform user-level actions, and returns either the next page object or a domain value. Tests never speak directly to the DOM — they call methods on page objects, and the page objects speak to the driver.

POM emerged from Selenium projects that had accumulated hundreds of test files, each with hardcoded CSS selectors. When designers renamed a class or restructured a component, every test that touched it broke. The fix was mechanical but painful: search-and-replace across the whole suite. POM removes that pain by giving each selector exactly one home. When the DOM changes, you edit one file. Every test that used the affected page object keeps working.

The pattern also raises the semantic level of tests. Instead of driver.findElement(By.css('#login-btn')).click(), tests read loginPage.submitCredentials(user, pass). That difference is not cosmetic. When a test fails, the stack trace points to a named user action, not an anonymous DOM operation, which makes failure triage dramatically faster.

Modern POM in 2026 looks different from the 2015 Java-Selenium version. Playwright and Cypress ship first-class fixture systems that let page objects be injected as constructor arguments; TypeScript catches selector typos at compile time; and the trend is toward small component objects (Header, Modal, Cart) composed inside larger page objects, rather than one giant class per URL.

The pattern is not universal. For very small suites (fewer than 20 tests) POM adds ceremony without a payoff, and for highly stateful flows a Screenplay-style actor model can be clearer. But for any team maintaining a growing E2E suite, POM remains the default answer because it optimises the metric that actually hurts: time spent updating tests after the UI changes.

Section 2

Architecture & Production Code

A well-structured POM project keeps three layers strictly separated: tests, page objects, and low-level helpers. Crossing those layers is where suites go bad.

┌─────────────────────────────────────────────┐
│  tests/                                     │
│  └── checkout.spec.ts                       │
│        expect(cartPage.total).toBe('$42');  │
└─────────────────────────────┬───────────────┘
                              │ imports
                              ▼
┌─────────────────────────────────────────────┐
│  pages/                                     │
│  ├── LoginPage.ts   (submitCredentials)     │
│  ├── CartPage.ts    (addItem, get total)    │
│  └── CheckoutPage.ts (payWith, confirm)     │
└─────────────────────────────┬───────────────┘
                              │ uses
                              ▼
┌─────────────────────────────────────────────┐
│  driver / framework (Playwright / Selenium) │
│  page.locator(...), page.click(...)         │
└─────────────────────────────────────────────┘

The tests layer contains only assertions and orchestration. It never calls page.locator directly. If you find yourself importing @playwright/test types other than expect into a spec file, you are probably leaking driver detail up a layer.

The pages layer contains selectors, action methods, and getters for observable state. Each page object owns its locators as private fields and exposes intent — 'submitCredentials', 'addItem', 'proceedToCheckout' — not clicks and types. Action methods return the next page object, allowing tests to chain: loginPage.submitCredentials(...).then(dashboard => dashboard.openCart()).

Composition beats inheritance. A modal or navigation bar that appears on many pages should be its own object, not a base class. Compose it inside each page object that needs it. Deep inheritance hierarchies are the second most common way POM suites become unmaintainable — after god-classes with 900 lines of selectors.

typescript
pages/LoginPage.ts + tests/login.spec.ts
// pages/LoginPage.ts
import type { Locator, Page } from "@playwright/test";
import { DashboardPage } from "./DashboardPage";

export class LoginPage {
  private readonly emailInput: Locator;
  private readonly passwordInput: Locator;
  private readonly submitButton: Locator;
  private readonly errorBanner: Locator;

  constructor(private readonly page: Page) {
    this.emailInput = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Password");
    this.submitButton = page.getByRole("button", { name: "Sign in" });
    this.errorBanner = page.getByRole("alert");
  }

  async goto() {
    await this.page.goto("/login");
  }

  async submitCredentials(email: string, password: string): Promise<DashboardPage> {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
    await this.page.waitForURL("/dashboard");
    return new DashboardPage(this.page);
  }

  get errorMessage() {
    return this.errorBanner.innerText();
  }
}

// tests/login.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";

test("valid credentials take the user to the dashboard", async ({ page }) => {
  const login = new LoginPage(page);
  await login.goto();
  const dashboard = await login.submitCredentials("qa@example.com", "s3cret!");
  await expect(dashboard.welcomeBanner).toContainText("Welcome back");
});
Section 3

POM vs Screenplay vs Raw Selectors

AspectPage Object ModelScreenplayRaw selectors in tests
Selector locationPage object classTask / Question classInline in test
Abstraction unitPage or componentActor and abilityNone
Learning curveLowMedium-highNone
Refactor cost after UI changeLow (1 file)Low (1 file)High (every test)
Best for suite size20+ tests100+ tests, complex flowsUnder 20 tests / prototypes
Tool alignmentPlaywright, Selenium, CypressSerenity BDD, PlaywrightAny

POM is the sensible default for most teams. Screenplay pays off when your flows are highly stateful and involve multiple actors with different permissions. Raw selectors are fine for a spike but should be refactored the moment the suite crosses about 20 tests.

Section 4

Production Debugging Scenarios

Three POM anti-patterns account for most maintenance pain. Watch for them during code review.

Scenario 1

Page object leaks driver types to the test

Symptom
Test file imports Locator or Page from Playwright and calls .click() directly.
Root cause
A page-object method returned a raw locator instead of performing the action itself.
Fix
Make action methods void (or return the next page object). Expose observable state as string or number getters, never as raw locators.
Scenario 2

Selectors duplicated across two page objects

Symptom
A header search bar has selectors in both HomePage and SearchPage. A redesign breaks half the suite.
Root cause
The header should have been its own component object composed into every page.
Fix
Extract HeaderComponent with its own selectors and methods. Instantiate it inside each page object's constructor.
Scenario 3

Action methods return void when the flow continues on a new page

Symptom
Tests call loginPage.submit() then immediately call methods on a page object they had to construct manually.
Root cause
Chaining was skipped, so the test now knows both LoginPage and DashboardPage lifecycles.
Fix
Return the next page object from every navigational action: return new DashboardPage(this.page). Tests read as a fluent flow.

Practice this concept in a real QA interview

Run a live mock with our AI Interview Coach, tune your resume with the ATS Resume Reviewer, and screen live listings on the QA Jobs Radar.

People Also Ask

1.What is the Page Object Model?
A design pattern where each screen or component of an app is a class that owns selectors and exposes user-level action methods, so tests never touch the DOM directly.
2.Why should I use POM instead of raw selectors?
So a UI change requires editing one file, not every test that touched the affected element. It also raises test readability from clicks to intent.
3.Is POM the same as BDD?
No. BDD structures how tests are written (Gherkin scenarios); POM structures how selectors and actions are organised. They combine cleanly.
4.Does POM work with Cypress and Playwright?
Yes. Modern frameworks encourage POM through fixtures and TypeScript classes; the pattern is not Selenium-specific.
5.How large should a page object be?
Small. If a class grows past ~150 lines, extract shared UI (headers, modals, forms) into component objects and compose them.
6.Should page objects contain assertions?
No — assertions belong in the test. Page objects expose state via getters; the test asserts against those values.
7.What is a component object?
A page-object-shaped class for a reusable UI fragment (header, modal, cart drawer) composed inside larger page objects instead of being duplicated.
8.How does POM compare to Screenplay?
POM organises by page; Screenplay organises by actor and task. Screenplay scales better for very large suites but has a steeper learning curve.
9.Can POM be used for API tests?
The pattern generalises to any layered client — API tests can wrap endpoints in request-builder classes for the same maintenance benefit.