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
| Advantage | Description |
|---|---|
| Simplifies test writing | Test code uses high-level APIs (such asloginPage.login('user', 'pass')) instead of raw Locator operations |
| Reduces maintenance cost | Changes to the page structure only require modifying the Page Object class, not every test |
| Improves readability | Test code reads more like a business language, making it understandable even to non-technical people |
| Avoids duplication | The 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
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
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
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
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
| Practice | Description |
|---|---|
| Centralize locators in the constructor | Define all locators in the constructor; do not create them dynamically in methods |
| Methods return Page Objects | Operation methods can return a new Page Object (e.g., return DashboardPage after login) |
| Encapsulate assertions, but do not overuse them | Encapsulate commonly used assertion combinations into methods; write simple assertions directly in the tests |
| Use composition instead of inheritance | Avoid deep inheritance; use the composition pattern to associate multiple Page Objects |
| One set of objects covers one page/component | Keep the granularity moderate; do not create separate classes for every small element |
Other ExtensionsPOM 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.