Playwright Page Object Model
The Page Object Model encapsulates elements of a web page and their interactions within a single class to create reusable components and ensure test scripts are maintainable.
Standard Metadata
- Area: Playwright
- Owner: Technical Leadership group
- Last reviewed: 2026-10-07
- Next review due from: 2026-12-01
- Enforcement: Code review
- Status: Active
Example Layout of the Page Object Model
export default class LoginPage {
page: Page;
constructor(page: Page) {
this.page = page;
}
// Locators
emailInput = () => this.page.getByLabel('Email');
passwordInput = () => this.page.getByLabel('Password');
loginButton = () => this.page.getByRole('button', { name: 'Login', exact: true });
// Actions
public async goto() {
await this.page.goto('/login');
}
/**
* Logs in as an existing user
* @param {string} email - the email address of the user
* @param {string} password - the user's password
*/
public async loginAs(email: string, password: string): Promise<void> {
await this.goto();
await this.emailInput().fill(email);
await this.passwordInput().fill(password);
const loginResponse = this.page.waitForResponse(response =>
response.url().includes('/connect/token') && response.status() === 200,
);
await this.loginButton().click();
await loginResponse;
}
}
Playwright page files must be in the pages folder and use the .page type · PW-06.1 · MUST · TEST
Define each page of the system under test in its own file in the pages folder, named after the system area with .page as the type, for example pages/customers.page.ts containing the CustomersPage class. Name files and classes following the TypeScript naming standards.
Playwright page objects may include one or more components · PW-06.2 · COULD · TEST
Where part of a page is self-contained or reused across pages, such as a modal or a navigation menu, it can be defined as its own component class. Components are created in the page’s constructor and belong to that page; a component does not contain pages.
Action functions in the page then use the component’s locators, for example await this.addCustomerModal.nameInput().fill(username);
Example Page using Components
// Example pages/customers.page.ts
export default class CustomersPage {
page: Page;
addCustomerModal: AddCustomerModal;
removeCustomerModal: RemoveCustomerModal;
navigationMenu: NavigationMenu;
constructor(page: Page) {
this.page = page;
this.addCustomerModal = new AddCustomerModal(this.page);
this.removeCustomerModal = new RemoveCustomerModal(this.page);
this.navigationMenu = new NavigationMenu(this.page);
}
// Locators
// ...
// Actions
/**
* Create a new customer with the provided username. This function presses the trigger submitButton.
* @param {string} username
*/
public async addCustomerInModal(username: string) {
await this.addButton().click();
// fill() waits for the name input to be visible and editable, so no explicit wait for the modal is needed
await this.addCustomerModal.nameInput().fill(username);
await this.addCustomerModal.submitButton().click();
}
Playwright locators must only be defined in page objects · PW-06.3 · MUST · TEST
Define every locator in a page or component class, and use it in tests through the page object. Tests must not create locators from page, either through the page fixture or a page object’s page property, so that each locator is defined once and can be updated in one place.
❌ The test creates its own locator:
await expect(loginPage.page.getByText('Welcome back')).toBeVisible();
✔️ The locator is defined in the page object, and the test uses it:
// pages/login.page.ts
welcomeText = () => this.page.getByText('Welcome back');
// tests/e2e-tests/login.spec.ts
await expect(loginPage.welcomeText()).toBeVisible();
Playwright tests should not chain locators onto page object locators · PW-06.4 · SHOULD · TEST
Where a test needs an element inside another element, define the chained locator in the page object rather than chaining it in the test. This keeps the locator reusable and in one place. A chained locator used once in a single test is acceptable where defining it in the page object adds no value.
❌ The test chains a new locator onto a page object locator:
await expect(customersPage.customersTable().getByRole('row')).toHaveCount(3);
✔️ The chained locator is defined in the page object:
// pages/customers.page.ts
customerRows = () => this.customersTable().getByRole('row');
// tests/e2e-tests/customers.spec.ts
await expect(customersPage.customerRows()).toHaveCount(3);
Playwright locators should be defined under the page object constructor · PW-06.5 · SHOULD · TEST
Group all locators under the constructor following the Playwright locator standards.
Playwright page object members should not repeat the page name · PW-06.6 · SHOULD · TEST
Locators and action functions are always called through their page, for example customersPage.titleText(), so the page name is already visible where they are used. Do not repeat the page name in locator or function names.
| ❌ Repeats the page name | ✔️ Relies on the page name |
|---|---|
customersPage.customersPageTitle() |
customersPage.titleText() |
customersPage.customersPageSearchInput() |
customersPage.searchInput() |
This does not apply to the entity an action function acts on, which function names should include, for example customersPage.searchForCustomer(name).
Playwright hooks and tests should not use the default page fixture · PW-06.7 · SHOULD · TEST
Only page object classes use the page fixture. Hooks and tests must not request page. Instead, they use page object fixtures such as loginPage or customersPage, which keep locators and waits in one place.
❌ The test uses page directly, so the wait and locator logic can’t be reused:
test('As an admin, I can search for a customer', async ({ customersPage, page }) => {
const customersResponse = page.waitForResponse((response) =>
response.url().includes('/customers') && response.status() === 200);
await customersPage.searchInput().fill(Customer.NAME);
await customersResponse;
await expect(customersPage.customerRow(Customer.NAME)).toBeVisible();
});
✔️ The page object uses page, and the test only uses the page object:
// pages/customers.page.ts
public async searchForCustomer(customerName: string) {
const customersResponse = this.page.waitForResponse((response) =>
response.url().includes('/customers') && response.status() === 200);
await this.searchInput().fill(customerName);
await customersResponse;
}
// tests/e2e-tests/customers.spec.ts
test('As an admin, I can search for a customer', async ({ customersPage }) => {
await customersPage.searchForCustomer(Customer.NAME);
await expect(customersPage.customerRow(Customer.NAME)).toBeVisible();
});
Action Functions · PW-06.8
Playwright page object action functions must only relate to their own page · PW-06.8.1 · MUST · TEST
Action functions are the functions in a page object that perform a series of actions on that page, such as addCustomerInModal(). Functions which are defined within a page class must use a single page and any related components.
❌ A customers page function that also acts on the orders page:
// pages/customers.page.ts
public async createOrderForCustomer(name: string) {
await this.viewOrdersButton(name).click();
// These steps act on the orders page, so they belong in OrdersPage
await this.ordersPage.newOrderButton().click();
await this.ordersPage.productInput().fill(Product.NAME);
}
✔️ Each page object acts only on its own page, and the test combines them:
// pages/customers.page.ts
public async openOrdersForCustomer(name: string) {
await this.viewOrdersButton(name).click();
}
// pages/orders.page.ts
public async createOrder(productName: string) {
await this.newOrderButton().click();
await this.productInput().fill(productName);
}
// tests/e2e-tests/orders.spec.ts
await customersPage.openOrdersForCustomer(Customer.NAME);
await ordersPage.createOrder(Product.NAME);
Playwright page object function names should follow the convention verb + context · PW-06.8.2 · SHOULD · TEST
Start function names with a verb, followed by the “component context” that the function is interacting with i.e. what entity it is having an effect on.
| ❌ | Why | ✔️ |
|---|---|---|
staffMember() |
No verb | createStaffMember() |
doSearch() |
No context | searchForManufacturer() |
remove() |
No context | deleteProperty() |
clickSave() |
Describes the click, not its effect on the entity | saveTeamDetails() |
Playwright page object action functions should include JSDocs · PW-06.8.3 · SHOULD · TEST
All functions that perform more than a single action should use JSDocs to add usage documentation.
/**
* Creates a customer using the add customer modal
* @param {string} name - the name of the new customer
*/
public async addCustomerInModal(name: string): Promise<void> { ... }
// A function with a single action does not need a JSDoc
public async goto(): Promise<void> {
await this.page.goto('/customers');
}