Playwright Page Object Model (POM)

This chapter introduces the design philosophy and implementation of the Page Object Model to help you organize large test suites.


What is POM

The Page Object Model is a test design pattern that encapsulates page elements and operationsinto independent classesin.

Each page object represents a page or component in a web application and contains the locators and operation methods for that page.


Advantages of POM

AdvantageDescription
Simplifies test writingTest code uses high-level APIs (such asloginPage.login('user', 'pass')) instead of raw Locator operations
Reduces maintenance costChanges to the page structure only require modifying the Page Object class, not every test
Improves readabilityTest code reads more like a business language, making it understandable even to non-technical people
Avoids duplicationThe same locators and operations only need to be defined once

Creating a Page Object Class

The following uses the EXAMPLE website as an example to create a Page Object for a login page.

Example

// File path: pages/LoginPage.ts
import { type Page, type Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  // Declare all locators as read-only properties
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    // Initialize all locators in the constructor
    this.usernameInput = page.getByLabel('Username');
    this.passwordInput = page.getByLabel('Password');
    this.loginButton = page.getByRole('button', { name: 'Log in' });
    this.errorMessage = page.getByTestId('login-error');
  }

  // Navigate to the login page
  async goto() {
    await this.page.goto('/login');
  }

  // Perform the login operation
  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  // Verify the error message for failed login
  async expectLoginError(message: string) {
    await expect(this.errorMessage).toBeVisible();
    await expect(this.errorMessage).toContainText(message);
  }
}

Using Page Object in Tests

Example

// File path: tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test.describe('Login functionality', () => {
  test('Login succeeds with correct username and password', async ({ page }) => {
    const loginPage = new LoginPage(page);

    // Use the Page Object's methods
    await loginPage.goto();
    await loginPage.login('example_user', 'correct_password');

    // Verify the redirect after successful login
    await expect(page).toHaveURL('/dashboard');
  });

  test('Login fails with incorrect password', async ({ page }) => {
    const loginPage = new LoginPage(page);

    await loginPage.goto();
    await loginPage.login('example_user', 'wrong_password');

    // Use the Page Object's assertion methods
    await loginPage.expectLoginError('Incorrect username or password');
  });
});

Multi-level Page Objects

In real projects, you can create multiple Page Objects, and they can be associated with each other.

Example

// File path: pages/DashboardPage.ts
import { type Page, type Locator, expect } from '@playwright/test';

export class DashboardPage {
  readonly page: Page;
  readonly welcomeText: Locator;
  readonly logoutButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.welcomeText = page.getByText('Welcome back');
    this.logoutButton = page.getByRole('button', { name: 'Log out' });
  }

  async expectWelcomeMessage(username: string) {
    await expect(this.welcomeText).toContainText(username);
  }

  async logout() {
    await this.logoutButton.click();
  }
}

Combining Page Objects:

Example

test('Complete login and logout flow', async ({ page }) => {
  const loginPage = new LoginPage(page);
  const dashboardPage = new DashboardPage(page);

  // Log in
  await loginPage.goto();
  await loginPage.login('example_user', 'password');

  // Verify the dashboard
  await dashboardPage.expectWelcomeMessage('example_user');

  // Log out
  await dashboardPage.logout();
  await expect(page).toHaveURL('/login');
});

POM Best Practices

PracticeDescription
Centralize locators in the constructorDefine all locators in the constructor; do not create them dynamically in methods
Methods return Page ObjectsOperation methods can return a new Page Object (e.g., return DashboardPage after login)
Encapsulate assertions, but do not overuse themEncapsulate commonly used assertion combinations into methods; write simple assertions directly in the tests
Use composition instead of inheritanceAvoid deep inheritance; use the composition pattern to associate multiple Page Objects
One set of objects covers one page/componentKeep the granularity moderate; do not create separate classes for every small element

POM is not mandatory.

For small projects with only a few tests, writing locators directly in the tests is clearer. When there are more than 5 test files, consider introducing POM.

Other Extensions