Skip to content

Page Object Model in Playwright

Page Object Model (POM) encapsulates page-specific locators and actions in classes, keeping spec files focused on business scenarios rather than selector details. Playwright's TypeScript support makes POM natural with typed Page parameters.

Why Use Page Objects

When a login form's locators change, update one LoginPage class instead of twenty spec files. Page objects expose methods like login(email, password) that hide getByLabel and click details.

Playwright does not require POM — small projects may inline locators. Adopt POM when selector duplication across specs becomes painful or when non-engineers read tests as documentation.

import { type Page, expect } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}

  async goto() { await this.page.goto('/login'); }
  email = () => this.page.getByLabel('Email');
  async login(email: string, password: string) {
    await this.email().fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Sign in' }).click();
  }
}

Pass page from the test fixture into the page object constructor.

Using Page Objects in Tests

test('admin can login', async ({ page }) => {
  const login = new LoginPage(page);
  await login.goto();
  await login.login('admin@example.com', 'secret');
  await expect(page).toHaveURL(/dashboard/);
});
  • Page objects receive Page in the constructor — never store global state.
  • Expose locators as methods returning locators, not resolved elements.
  • Actions are async methods that await interactions internally.
  • Assertions can live in page objects or specs — pick one convention.

POM Patterns

Conventions for Playwright page objects.

Pattern Guidance
Constructor injection Pass page from fixture
Locator methods Return locators, don't await early
Action methods Async, await all interactions
No assertions in PO Optional — some teams allow expect in PO
Fixtures + PO test.extend to inject page objects
Composition Share base class for common nav

Page Objects as Fixtures

Use test.extend to inject page objects automatically so specs stay concise.

const test = base.extend<{ loginPage: LoginPage }>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

When Not to Use POM

Single-spec prototypes and one-off tests don't need classes. Over-abstracting three-line flows into page objects adds indirection without payoff.

Common Mistakes

  • Storing locators as resolved elements instead of lazy locator methods.
  • Page objects that know about test assertions from unrelated pages.
  • God objects — one class for an entire app instead of per-page classes.
  • Sharing mutable state between page object instances across tests.

Key Takeaways

  • POM centralizes locators and actions per page or component.
  • Inject Page via constructor or custom fixtures.
  • Update selectors in one place when UI changes.
  • Adopt POM when duplication grows — not on day one of a tiny project.

Pro Tip

Name methods after user intent (submitOrder) not DOM mechanics (clickCheckoutButton) — specs read as user stories.