Page Object Model (POM)
Organise your tests with the Page Object Model pattern — encapsulate locators and actions in reusable classes.
What Is the Page Object Model?
The Page Object Model (POM) is a design pattern that creates an abstraction layer between your tests and the raw Playwright API. Each page or component in the application is represented by a class that exposes high-level methods like login(email, password) instead of raw fill and click calls. When the UI changes you update only the page object — all tests that use it stay green without modification.
POM classes should own locators and actions but not assertions. Keep assertions in the test files so that each test clearly expresses the intent and expected outcome. Page objects are about DRY (Don't Repeat Yourself) for interactions, not about hiding what the test is checking.
page fixture into the page object constructor. For larger projects, wrap all page objects in a custom fixture so tests receive them automatically without any new calls.Structuring Your Page Objects
A good page object has three sections: (1) the constructor that accepts the page and defines locators as private properties, (2) action methods that perform multi-step workflows, and (3) optionally a navigation method that calls page.goto(). Keep each class focused on a single page or modal — avoid "God" page objects that cover the entire application.
Using Page Objects in Tests
Tests that use page objects read almost like English: await loginPage.login('user@example.com', 'pass'). This makes them easy to review by non-engineers and straightforward to update when requirements change. Combine POM with fixtures to inject page objects automatically and avoid repetitive setup code in every test file.
// pages/LoginPage.js
export class LoginPage {
/** @param {import('@playwright/test').Page} page */
constructor(page) {
this.page = page;
// Define locators once — change here if UI changes
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByTestId('auth-error');
}
async goto() {
await this.page.goto('/login');
}
async login(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('valid credentials redirect to dashboard', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'secret123');
await expect(page).toHaveURL('/dashboard');
});
test('invalid password shows error', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrong');
await expect(loginPage.errorMessage).toBeVisible();
});
package pages;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.AriaRole;
public class LoginPage {
private final Page page;
private final Locator emailInput;
private final Locator passwordInput;
private final Locator submitButton;
public final Locator errorMessage;
public LoginPage(Page page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.submitButton = page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Sign in"));
this.errorMessage = page.getByTestId("auth-error");
}
public void navigate() { page.navigate("/login"); }
public void login(String email, String password) {
emailInput.fill(email);
passwordInput.fill(password);
submitButton.click();
}
}
# pages/login_page.py
from playwright.sync_api import Page, Locator
class LoginPage:
def __init__(self, page: Page):
self.page = page
self.email_input = page.get_by_label("Email")
self.password_input = page.get_by_label("Password")
self.submit_button = page.get_by_role("button", name="Sign in")
self.error_message = page.get_by_test_id("auth-error")
def navigate(self):
self.page.goto("/login")
def login(self, email: str, password: str):
self.email_input.fill(email)
self.password_input.fill(password)
self.submit_button.click()
import pytest
from playwright.sync_api import Page, expect
from pages.login_page import LoginPage
@pytest.fixture
def login_page(page: Page) -> LoginPage:
lp = LoginPage(page)
lp.navigate()
return lp
def test_valid_login(login_page: LoginPage, page: Page):
login_page.login("user@example.com", "secret123")
expect(page).to_have_url("/dashboard")
def test_invalid_password(login_page: LoginPage):
login_page.login("user@example.com", "wrong")
expect(login_page.error_message).to_be_visible()
Written by PV
© 2026 All Rights Reserved