← Back to libraryQuestion 171 of 273
🎭PlaywrightIntermediate

Page Object Model in Playwright

📌 Definition:

The Page Object Model (POM) is a pattern where each page or component is a class exposing locators and high-level actions, so tests describe behavior while locators and interactions live behind a reusable API. In Playwright, page objects store Locators (not resolved elements) as fields.

📖 Detailed Explanation:

The pattern is the same as in Selenium, but Playwright page objects are simpler and more robust because they hold Locators — lazy, self-re-querying descriptions — rather than WebElements that can go stale. A page object typically takes the page in its constructor, defines locators as readonly fields (this.emailInput = page.getByLabel('Email')), and exposes methods like login(email, password) that combine actions and Playwright's auto-waiting. You generally do NOT put assertions inside page objects; keep them in tests, or expose expectation helpers thinly. Combined with fixtures, page objects can even be injected, so a test receives a ready loginPage. This keeps tests readable and centralizes locator maintenance.

🔑 Key Points:
  • Each page/component is a class holding Locators as fields (lazy, never stale)
  • Constructor takes the page; methods expose intent-level actions (login, addToCart)
  • Keep assertions in tests (or thin expectation helpers), not buried in page objects
  • Locator changes happen in one class, not across every spec
  • Combine with fixtures to inject ready-made page objects into tests
🌍 Real-World Example:

A LoginPage class exposes goto() and login(email, password). Fifteen tests call loginPage.login(...) without knowing the field locators. When the app renames the email field's label, you update one locator in LoginPage and all fifteen tests keep passing.

📎 Code Example:
// 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;

  constructor(page: Page) {
    this.page = page;
    this.email = page.getByLabel('Email');
    this.password = page.getByLabel('Password');
    this.submit = page.getByRole('button', { name: 'Sign in' });
  }

  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();
  }
}

// login.spec.ts
test('valid login lands on dashboard', async ({ page }) => {
  const login = new LoginPage(page);
  await login.goto();
  await login.login('qa@qapractice.com', 'Secret123');
  await expect(page).toHaveURL(/\/dashboard/);
});
🎯 Scenario-Based Interview Question:

Scenario: A reviewer says your page objects are 'doing too much' — they contain expect() calls, waits, and test data. What's the problem, and how should responsibilities be split?