What Is the Page Object Model
The Page Object Model (POM) is a design pattern where each page or significant component of an application is represented by a class that encapsulates its locators and the user actions available on it. Instead of a test scattering page.getByRole() calls throughout its body, it calls high-level methods like loginPage.login(username, password), while the LoginPage class owns the actual locator details. This separation means that when the UI changes, only the page object needs updating, not every test that touches that page.
Cricket analogy: A team's fielding coach maintains a single playbook of field settings for each bowler type, so captains just call 'set the death-overs field' rather than re-deriving positions each match, just as a page object centralizes locator knowledge for tests to reuse.
Structuring a Page Object Class
A well-formed Playwright page object typically accepts a Page instance in its constructor, defines locators as readonly class properties initialized in that constructor, and exposes async methods named after user intents rather than DOM operations, such as addToCart() rather than clickButton(). Locators should be defined once as properties (this.emailInput = page.getByLabel('Email')) rather than re-queried inline in every method, which keeps the class the single source of truth for how that page's elements are found.
Cricket analogy: A stadium's ground staff maintains one master map of pitch, boundary, and sightscreen positions that every groundsman references rather than remeasuring the field before each match, just as locators are defined once in the constructor and reused.
// pages/login-page.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email address');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectLoggedIn() {
await expect(this.page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
}
}Composing Page Objects with Fixtures
Rather than instantiating new LoginPage(page) at the top of every test, Playwright's test.extend() lets you define custom fixtures that construct and inject page objects automatically, so a test simply declares async ({ loginPage }) => {...} as its parameter. This keeps test files free of boilerplate construction code and makes it trivial to add new page objects to every test in a project by extending the base test object once, typically in a shared fixtures.ts file.
Cricket analogy: A franchise's support staff arrives pre-assigned to each net session, physio, throwdown specialist, analyst, without the captain manually requesting each one, just as a fixture auto-injects a ready-made page object into every test.
Define custom fixtures in a shared file, e.g. fixtures.ts, using test.extend<{ loginPage: LoginPage }>({ loginPage: async ({ page }, use) => { await use(new LoginPage(page)); } }). Import this extended test object in your spec files instead of the base @playwright/test import.
Avoiding Common POM Pitfalls
A frequent mistake is turning page objects into 'god objects' that expose every possible locator on a large page, making the class hundreds of lines long and hard to navigate; instead, break large pages into smaller component objects (e.g., a NavBar component reused across many page objects). Another pitfall is embedding test assertions deep inside page object methods, which couples verification logic to page structure and makes failures harder to trace back to the specific test scenario that expected a given outcome.
Cricket analogy: A team that tries to have one all-rounder bat, bowl, keep wicket, and captain ends up overloaded and error-prone, just as a single page object trying to model an entire large application becomes unwieldy and hard to maintain.
Avoid putting hard assertions like expect(...).toBeVisible() inside every page object action method. Keep page objects focused on locating elements and performing actions; let the test itself decide what to assert, so a failed expectation clearly points to the specific test scenario, not a buried line inside a shared helper class.
- POM encapsulates locators and actions for a page in a dedicated class, separate from test logic.
- Define locators once as constructor properties rather than re-querying them in every method.
- Name methods after user intent (login, addToCart) rather than raw DOM operations.
- Use test.extend() fixtures to auto-inject page objects into tests, removing boilerplate.
- Split large pages into smaller component objects instead of one monolithic class.
- Keep assertions in the test file, not buried inside page object action methods.
- POM pays off most in suites with many tests reusing the same UI flows.
Practice what you learned
1. What is the primary purpose of the Page Object Model?
2. Where should locators typically be defined in a page object class?
3. What Playwright feature lets you auto-inject a page object into tests without manual construction?
4. Why is it discouraged to put hard assertions inside page object action methods?
5. What is a 'god object' anti-pattern in the context of POM?
Was this page helpful?
You May Also Like
Playwright Best Practices
A practical guide to writing reliable, maintainable Playwright tests by choosing the right locators, avoiding flaky waits, and keeping tests isolated and fast.
Building an E2E Test Suite for a Real App
A practical walkthrough of planning, structuring, and running a Playwright end-to-end test suite for a production application, from config to CI.
Playwright Interview Questions
The core Playwright concepts and tricky topics that come up most often in QA and SDET interviews, from auto-waiting to network interception.