Page Object Model (POM) — Complete Guide with Code (2026)
The Page Object Model explained for 2026 with runnable Playwright and Selenium code. When POM helps, when it hurts, the App Actions alternative, and how to migrate a 200-test suite without breaking CI.

Last updated 2026-07-20 · 13 min read · By Avinash K
Page Object Model (POM) is the default framework pattern taught in every course — and the pattern most teams misuse. Applied well, it cuts locator churn by 60%; applied blindly, it doubles maintenance. This guide is the 2026 version with runnable Playwright and Selenium code, plus when to pick App Actions instead.
Key takeaways
- Canonical POM in Playwright and Selenium (with types).
- The three POM anti-patterns that kill maintainability.
- When App Actions beats POM — with a worked example.
- How to migrate a 200-test suite to POM in three sprints.
1. Why POM exists
POM encapsulates a page's locators and actions in one class so tests read like user intent, not selectors. When the DOM changes, one file changes — not fifty.
2. Playwright POM — canonical implementation
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly email: Locator;
readonly password: Locator;
readonly submit: Locator;
readonly error: Locator;
constructor(page: Page) {
this.page = page;
this.email = page.getByRole('textbox', { name: 'Email' });
this.password = page.getByLabel('Password');
this.submit = page.getByRole('button', { name: 'Sign in' });
this.error = page.getByRole('alert');
}
async goto() { await this.page.goto('/login'); }
async login(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.submit.click();
}
async expectError(message: string) {
await expect(this.error).toHaveText(message);
}
}See our Playwright installation guide for full project setup.
3. Selenium POM in Java
public class LoginPage {
private final WebDriver driver;
@FindBy(id = "email") private WebElement email;
@FindBy(id = "password") private WebElement password;
@FindBy(id = "submit") private WebElement submit;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void login(String u, String p) {
email.sendKeys(u);
password.sendKeys(p);
submit.click();
}
}Deep dive: Selenium interview questions.
4. Three POM anti-patterns
- God Page Objects — one class for a 40-section page. Split by component instead.
- Assertions inside the Page Object — hides intent, blocks reuse. Assert in the test.
- Sleeps and waits inside methods — mask timing bugs. Use auto-waiting locators.
5. When App Actions beats POM
For state setup (creating a user, seeding a cart), calling your API is 10x faster and more reliable than driving the UI. Reserve POM for what UI tests must verify — the UI itself. See Cypress's App Actions article for the canonical framing.
6. Migrating a 200-test suite
Sprint 1: convert your 5 highest-churn pages. Sprint 2: convert the next 15. Sprint 3: enforce with a lint rule that fails PRs importing raw selectors outside pages/. Track locator-change PRs per sprint — a healthy POM cuts them by 60%+ within a quarter. Pair with the test automation complete guide.