Introduction
As your Playwright test suite grows, you might find yourself repeating the same selectors and sequences of actions across multiple tests. This can lead to tests that are hard to read and even harder to maintain. If a UI element changes, you could end up updating dozens of files!
That's where the Page Object Model (POM) design pattern comes to the rescue. Let's break down what it is and how to implement a basic POM in your Playwright TypeScript projects for cleaner, more maintainable tests.
What is the Page Object Model (POM)?
The Page Object Model is a design pattern where you create an object (a class) for each page (or significant component) of your web application. This class encapsulates:
- Locators: Definitions of the elements on that page.
- Methods: Functions that represent user interactions with those elements (e.g.,
login(username, password)
,navigateToProductDetails()
,searchForItem(term)
).
Your actual test scripts then use these Page Object methods instead of interacting directly with page.locator()
or page.getByRole()
calls.
Why Bother with POM? The Benefits!
- Readability: Your tests become much easier to read and understand because they describe user journeys at a higher level (e.g.,
loginPage.loginAsUser('test', 'password');
is clearer than a series offill
andclick
commands). - Maintainability: If a UI element's locator changes, you only need to update it in one place โ the relevant Page Object class. No more hunting through countless test files!
- Reusability: Common actions and locators are defined once and can be reused across many tests.
- Reduced Duplication: Say goodbye to copying and pasting locators and interactions.
- Clear Separation of Concerns: Test logic (what to test, assertions) is separated from page interaction logic (how to interact with elements).
Let's Build a Basic POM: A Login Page Example
Imagine we have a simple login page with an email field, a password field, and a login button.
1. Create a Page Object Directory:
It's good practice to keep your Page Objects organized. Create a new directory, for example, page-objects
(or poms
, pages
, etc.) in your project root or alongside your tests
folder.
my-playwright-project/
โโโ page-objects/
โ โโโ LoginPage.ts
โโโ tests/
โ โโโ login.spec.ts
โโโ playwright.config.ts
โโโ package.json
2. Create the LoginPage.ts
Page Object:
// page-objects/LoginPage.ts
import { type Page, type Locator } from '@playwright/test';
export class LoginPage {
// 'readonly' makes sure these properties are only assigned in the constructor
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator; // Optional: for displaying login errors
constructor(page: Page) {
this.page = page;
// Using getByLabel for better accessibility and resilience
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
// Using getByRole for the button
this.loginButton = page.getByRole('button', { name: 'Login' });
// Example for an error message (you might use getByTestId or another locator)
this.errorMessage = page.locator('.error-message');
}
async navigate() {
await this.page.goto('/login'); // Assuming /login is your login page URL
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorMessageText() {
return this.errorMessage.textContent();
}
}
Breakdown of LoginPage.ts
:
- We import
Page
andLocator
types from Playwright. - The class
LoginPage
takes aPage
object in its constructor. Thispage
instance is passed from your test. - Locators: We define locators like
emailInput
,passwordInput
,loginButton
asreadonly
properties. We initialize them in the constructor using Playwright's recommended locators. - Methods:
navigate()
: A helper to go to the login page.login(email, password)
: This method encapsulates the actions of filling the email, filling the password, and clicking the login button. This is the core of the interaction logic.getErrorMessageText()
: An example of a method to retrieve information from the page.
3. Using the LoginPage
in a Test (tests/login.spec.ts
):
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../page-objects/LoginPage'; // Adjust path as needed
test.describe('Login Functionality', () => {
let loginPage: LoginPage; // Declare loginPage here
test.beforeEach(async ({ page }) => {
// Initialize LoginPage before each test
loginPage = new LoginPage(page);
await loginPage.navigate();
});
test('should allow login with valid credentials', async ({ page }) => {
await loginPage.login('testuser@example.com', 'Password123');
// Add assertions here to verify successful login, e.g.:
// await expect(page).toHaveURL('/dashboard');
// await expect(page.getByRole('heading', { name: 'Welcome User' })).toBeVisible();
console.log('Simulating successful login check...'); // Placeholder for actual assertions
});
test('should show error message with invalid credentials', async () => {
await loginPage.login('invalid@example.com', 'wrongpassword');
const errorMessage = await loginPage.getErrorMessageText();
expect(errorMessage).toContain('Invalid username or password'); // Or whatever your error message is
});
});
Breakdown of login.spec.ts
:
- We import our
LoginPage
. - In
test.beforeEach
, we create an instance ofLoginPage
and navigate to the login page. This setup runs before each test in the suite. - Test Cases: Notice how clean the test cases are!
await loginPage.login('testuser@example.com', 'Password123');
is much more descriptive than seeing individualfill
andclick
calls here.- Assertions are still done in the test file, as the Page Object should primarily focus on interactions and element access, not assertions (though simple state-checking methods like
isErrorMessageVisible()
can sometimes be useful in a PO).
Key Takeaways for POM
- One Page/Component, One Class: Keep it granular.
- Pass the
page
Fixture: Your Page Objects need thepage
context from your tests. - Locators in the Constructor: Define your element locators once.
- Methods for User Actions: Encapsulate sequences of interactions.
- Tests Use PO Methods: Your tests become orchestrators of high-level user actions.
Is POM Always Necessary?
For very small projects with only a handful of tests, POM might feel like overkill. However, as soon as your project starts to grow, or if you anticipate it growing, adopting POM early will save you a massive amount of time and effort in the long run.
It's a foundational pattern for building scalable and maintainable test automation suites.
What's Next?
- Start refactoring one of your existing test files to use a Page Object.
- Think about how you can create Page Objects for other key areas of your application.
- Explore advanced POM techniques like Base Pages or Component Objects for shared UI elements (like headers or footers).
Using POM will significantly level up your Playwright testing game!