Supercharge QA with AI for Faster & Smarter Software Testing

Learn how the Playwright page object model enhances test automation with reusable, maintainable, and scalable scripts across browsers and platforms.
Published on: November 9, 2025
The Playwright Page Object Model is a design pattern that organizes test automation by separating page-specific locators and actions into dedicated classes. Its purpose is to make Playwright tests maintainable, reusable, readable, and scalable.
This approach enables faster test development, easier maintenance, and a robust automation framework that works across multiple browsers and platforms.
Overview
What Is Page Object Model?
The Playwright Page Object Model is a structured approach for organizing automation tests by representing each web page as a separate class. This separation keeps test scripts clean and focused on workflows rather than UI details.
What Are the Advantages of Using the Playwright Page Object Model?
Using the Playwright Page Object Model enhances efficiency and maintainability in test automation. It ensures test scripts remain consistent even when UI elements change.
What Are the Steps to Set up Playwright Page Object Model?
Implementing the Playwright Page Object Model requires creating a clear folder structure and defining reusable page actions. This ensures your test framework is scalable and easy to maintain.
How Do You Refactor an Existing Project to Use Playwright Page Object Model?
Refactoring an existing project to use Playwright Page Object Model improves maintainability, readability, and test reusability. Even legacy tests can be reorganized efficiently without rewriting the entire suite, saving time and effort.
The Page Object Model (POM) in test automation separates test scripts from the underlying page structure. Each page or component is represented as a class containing its element locators and the actions that can be performed on them.
This approach promotes code reusability, maintainability, and readability. If a UI element changes, only the corresponding page class needs updating instead of modifying multiple test scripts.
POM ensures that the test logic remains clean and focused on test scenarios rather than UI details. It’s a language-agnostic concept, applicable across different test automation frameworks and programming environments.

The Page Object Model (POM) is widely adopted in Playwright automation projects to create clean, maintainable, and scalable test frameworks. It helps structure automation code by separating test logic from page interactions, making large test suites easier to manage.
Here are the key advantages of using Playwright Page Object Model:
Using Playwright Page Object Model ensures consistent structure, better test management, and long-term stability, making it an essential design pattern for modern web automation.
Watch this video for a hands-on walkthrough of implementing Playwright POM, from creating reusable page classes to Playwright locators organizing locators and maintaining scalable test suites.
In a typical web application, each section or page can be treated as an independent object within the test framework.
For example, the home page may verify that essential elements are visible and that the layout loads correctly. Meanwhile, pages like search page and account page handle user interactions specific to product searches and account management, similar to what you’d find on the LambdaTest eCommerce Playground platform.

By organizing your Playwright test code this way, each page class encapsulates its own logic and functionality. Test scripts can then call simple methods from these classes without needing to know how elements are located or interacted with behind the scenes.
This modular structure minimizes duplication and isolates changes. So if a Playwright locator or workflow changes, only that specific page object needs updating, keeping your test suite stable and easy to maintain.
POM provides a clear structure that scales efficiently as your application grows, ensuring consistent and maintainable test automation across large projects.
Note: Run your Playwright tests at scale across 3000+ browsers and OS combinations. Try LambdaTest Now!
When building scalable Playwright test automation, structuring your project using the Page Object Model (POM) helps isolate locators and actions into individual page classes.
This approach enhances readability, simplifies maintenance, and makes test logic easier to expand across complex user journeys.
Prerequisites:
Before starting, ensure you have the following:
node -vOnce installed, verify the installation using the following commands:
npm -vnpm init playwright@latestTo implement Playwright Page Object Model, let’s consider a test scenario where the goal is to automate an end-to-end eCommerce flow covering multiple pages, including login, search, product selection, and adding an item to the cart.
The purpose of using Playwright Page Object Model is to maintain a clear separation of concerns by keeping locators and page-specific actions in separate files, which makes tests more maintainable, reusable, and readable.
Each page class, such as LoginPage, SearchPage, SoftwarePage, and CartPage, encapsulates the elements and behaviors of that page, allowing the test file to focus purely on the automation flow and assertions.
This approach simplifies updates, promotes code reuse, and ensures that complex flows across multiple pages remain organized and easy to manage.
Test Scenario:
Code Implementation:
Below is the code implementation of the above test scenario. Implementing the Playwright Page Object Model helps you create separate test files for each action, such as navigating to the login page and logging in with valid credentials, selecting the “Software” category, searching for an item, navigating between result pages, adding an item to the cart, and verifying the success message.
login.page.js
const { expect } = require('@playwright/test');
exports.LoginPage = class LoginPage {
constructor(page) {
this.page = page;
this.emailField = page.locator('id=input-email');
this.passwordField = page.locator('id=input-password');
this.loginButton = page.locator('input:has-text("Login")');
}
async enterEmail(email) {
await this.emailField.fill(email);
}
async enterPassword(password) {
await this.passwordField.fill(password);
}
async clickLogin() {
await this.loginButton.click();
}
};
Code Walkthrough:
Now that the login functionality has been automated using the LoginPage class, let’s move on to creating a test script for the search functionality using the SearchPage class.
search.page.js
const { expect } = require('@playwright/test');
exports.SearchPage = class SearchPage {
constructor(page) {
this.page = page;
this.categoryAllCategories = page.locator('#entry_217822 button:has-text("All Categories")');
this.categorySoftware = page.locator('#entry_217822 >> text=Software');
this.searchField = page.locator('#entry_217822 [placeholder="Search For Products"]');
this.searchButton = page.locator('text=Search');
}
async selectCategorySoftware() {
await this.categoryAllCategories.click();
await this.categorySoftware.click();
}
async searchForItem(item) {
await this.searchField.fill(item);
}
async clickSearchButton() {
await this.searchButton.click();
}
};
Code Walkthrough:
Now that the search functionality has been automated using the SearchPage class, let’s move on to creating test scripts for the product page using the SoftwarePage class.
softwarepage.js
const { expect } = require('@playwright/test');
exports.SoftwarePage = class SoftwarePage {
constructor(page) {
this.page = page;
this.secondPage = page.locator('a:has-text("2")');
this.addToCart = page.locator('button:has-text("Add to Cart")');
this.carouselItem = page.locator('.carousel-item');
}
async clickSecondPage() {
await this.secondPage.click();
}
async addFirstItemToCart() {
await this.carouselItem.first().hover();
await this.addToCart.first().click();
}
};
Code Walkthrough:
Now that you have automated product listing, item selection, and adding to cart using the SoftwarePage class, let’s move on to creating test scripts for the CartPage class.
cart.page.js
const { expect } = require('@playwright/test');
exports.CartPage = class CartPage {
constructor(page) {
this.page = page;
this.cartPopup = page.locator('div[role="alert"]');
}
};
Code Walkthrough:
Now that the CartPage class has been automated, let’s move on to creating a test script for the complete add-to-cart flow using all page objects.
add-to-cart.js
// @ts-check
const { test, expect } = require('@playwright/test');
const { LoginPage } = require('../pages/login.page');
const { SearchPage } = require('../pages/search.page');
const { SoftwarePage } = require('../pages/software.page');
const { CartPage } = require('../pages/cart.page');
test.beforeEach(async ({ page }) => {
await page.goto('?route=account/login');
test('add to cart - software product', async ({ page }) => {
const loginPage = new LoginPage(page);
const searchPage = new SearchPage(page);
const softwarePage = new SoftwarePage(page);
const cartPage = new CartPage(page);
await loginPage.enterEmail("steve.james@gmail.com");
await loginPage.enterPassword("Abc123");
await loginPage.clickLogin();
await searchPage.selectCategorySoftware();
await searchPage.searchForItem('iPod');
await searchPage.clickSearchButton();
await softwarePage.clickSecondPage();
await softwarePage.addFirstItemToCart();
await expect(cartPage.cartPopup).toContainText('Success: You have added iPod Touch to your shopping cart!');});
Code Walkthrough:
Test Execution:
You can run the tests using the following command:
node lambdatestPlaywright.js
Local Playwright automation is often limited by the number of browsers, versions, and operating systems available on your machine. This can restrict test coverage and make it difficult to validate cross-platform functionality effectively.
Cloud-based platforms overcome these limitations by providing access to a wide range of browser and OS combinations without requiring local setup or maintenance. One such platform is LambdaTest.
Using LambdaTest, you can run parallel tests on different environments, reduce execution time, and gain detailed insights via the dashboard, ensuring your tests are both reliable and scalable.
LambdaTest, which allows you to perform Playwright testing across 3000+ browsers and OS combinations, enabling comprehensive cross-platform testing, faster execution, and improved test stability.
To get started with running Playwright Page Object Model tests in parallel across websites, follow the steps below:
LT_USERNAME="<your_username>"
LT_ACCESS_KEY="<your_access_key>" wsEndpoint:{"wss://cdp.lambdatest.com/playwright?capabilities=\$\{encodeURIComponent(JSON.stringify(capabilities))}" st capabilities = [
{
browserName: "MicrosoftEdge",
browserVersion: "latest",
"LT:Options": {
platform: "Windows 11",
build: "Playwright Page Object Model",
name: "Playwright Page Object Model Sample",
user: process.env.LT_USERNAME,
accessKey: process.env.LT_ACCESS_KEY,
network: true,
video: true,
console: true,
}
}
];
You can generate the above Playwright capabilities from the LambdaTest Automation Capabilities Generator.
With all the configuartions setup your playwright configuration file must look like this:
const { devices } = require('@playwright/test')
// Playwright config to run tests on LambdaTest platform and local
const config = {
testDir: 'tests',
testMatch: '**/*.spec.js',
timeout: 60000,
use: {
viewport: null,
baseUrl: 'https://ecommerce-playground.lambdatest.io/',
},
projects: [
// -- LambdaTest Config --
// name in the format: browserName:browserVersion:platform@lambdatest
// Use additional configuration options provided by Playwright if required: https://playwright.dev/docs/api/class-testconfig
{
name: 'chrome:latest:MacOS Big sur@lambdatest',
use: {
viewport: { width: 1920, height: 1080 },
}
},
{
name: 'pw-webkit:latest:MacOS Big sur@lambdatest',
use: {
viewport: { width: 1920, height: 1080 },
}
}
]
}
module.exports = config
To speed up test execution, you can run Playwright tests in parallel across multiple workers. A worker in Playwright is an isolated environment that executes tests independently.
Setting Workers in Playwright:
By default, Playwright already runs tests in parallel, but you can control the number of workers to optimize execution time.
You can configure the number of workers in two ways:
npx playwright test --workers=4 const { devices } = require('@playwright/test');
const config = {
testDir: 'tests',
testMatch: '**/*.spec.js',
timeout: 60000,
workers: 4, // Run 4 workers in parallel
use: {
baseUrl: 'https://ecommerce-playground.lambdatest.io/',
},
projects: [
{
name: 'chrome:latest:MacOS Big sur@lambdatest',
use: {},
},
{
name: 'pw-webkit:latest:MacOS Big sur@lambdatest',
use: {},
},
{
name: 'chrome',
use: { browserName: 'chromium', channel: 'chrome' },
},
{
name: 'safari',
use: { browserName: 'webkit' },
},
{
name: 'firefox',
use: { browserName: 'firefox' },
},
],
};
module.exports = config;
Test Execution:

To get started with parallel testing on LambdaTest, follow this support documentation on how to run Playwright tests in parallel.
In many legacy Playwright projects, the Page Object Model (POM) may not have been implemented initially. Refactoring such projects is crucial to improve maintainability, readability, reusability, and scalability of your test automation framework.
By encapsulating locators and actions into page classes, your test scripts focus solely on test workflows and test data, making them easier to read, update, and scale.
// @ts-check
const { test, expect } = require('@playwright/test');
test('register new user', async ({ page }) => {
await page.goto('?route=account/register')
await page.locator('id=input-firstname').fill('steve')
await page.locator('id=input-lastname').fill('james')
await page.locator('id=input-email').fill('steve.james@gmail.com')
await page.locator('id=input-telephone').fill('07864538876')
await page.locator('id=input-password').fill('Abc123')
await page.locator('id=input-confirm').fill('Abc123')
await page.locator('text=I have read and agree to the Privacy Policy').click()
await page.locator('text=Continue').click()
await page.locator('text=Continue').isHidden()
await expect(page.locator('.page-title')).toHaveText(' Your Account Has Been Created!')
});
test('register new user', async ({ page }) => {
await page.goto('?route=account/register')
await page.locator('id=input-firstname').fill('diane')
await page.locator('id=input-lastname').fill('peters')
await page.locator('id=input-email').fill('diane.peters@outlook.com')
await page.locator('id=input-telephone').fill('0269638885')
await page.locator('id=input-password').fill('DefÂŁ$%')
await page.locator('id=input-confirm').fill('DefÂŁ$%')
await page.locator('text=I have read and agree to the Privacy Policy').click()
await page.locator('text=Continue').click()
await page.locator('text=Continue').isHidden()
await expect(page.locator('.page-title')).toHaveText(' Your Account Has Been Created!')
});
These tests perform similar steps but use different input data. Without POM, repetitive code increases maintenance efforts.
test.beforeEach(async ({ page }) => {
await page.goto('?route=account/register')
});This ensures that each test starts on the registration page, keeping your test suite consistent and clean.
exports.RegisterPage = class RegisterPage {
constructor(page) {
this.page = page;
this.firstNameField = page.locator('id=input-firstname');
}
async enterFirstName(firstName) {
await this.firstNameField.fill(firstName)
}
}
}); Using parameters for field values allows the same methods to be reused across multiple tests with different data, improving reusability and maintainability.
const { test, expect } = require('@playwright/test');
const { RegisterPage } = require('../pages/register.page');
test.beforeEach(async ({ page }) => {
await page.goto('?route=account/register')
});
test('register new user', async ({ page }) => {
const registerPage = new RegisterPage(page);
await registerPage.enterFirstName('steve')
await page.locator('id=input-email').fill('steve.james@gmail.com')
await page.locator('id=input-telephone').fill('07864538876')
await page.locator('id=input-password').fill('Abc123')
await page.locator('id=input-confirm').fill('Abc123')
await page.locator('text=I have read and agree to the Privacy Policy').click()
await page.locator('text=Continue').click()
await page.locator('text=Continue').isHidden()
await expect(page.locator('.page-title')).toHaveText(' Your Account Has Been Created!')
});
The test script is now cleaner, easier to read, and more focused on test data rather than implementation details.
this.lastNameField = page.locator('id=input-lastname');
this.emailField = page.locator('id=input-email');
this.telephoneField = page.locator('id=input-telephone');
this.passwordField = page.locator('id=input-password');
this.confirmField = page.locator('id=input-confirm');
this.privacyPolicyCheckbox = page.locator('text=I have read and agree to the Privacy Policy');
this.continueButton = page.locator('text=Continue');
this.accountCreationMessage = page.locator('.page-title');
async enterLastName(lastName) { await this.lastNameField.fill(lastName) }
async enterEmail(email) { await this.emailField.fill(email) }
async enterTelephoneNumber(number) { await this.telephoneField.fill(number) }
async enterPassword(password) { await this.passwordField.fill(password) }
async enterConfirmPassword(password) { await this.confirmField.fill(password) }
async clickPrivacyPolicy() { await this.privacyPolicyCheckbox.click() }
async clickContinue() { await this.continueButton.click() }
async assertAccountCreation(message) {
await this.continueButton.isHidden()
await expect(this.accountCreationMessage).toHaveText(message)
}
Now the page class contains all necessary interactions and assertions, allowing tests to be concise and readable.
Test Execution:

In Playwright, fixtures are a powerful feature that allows you to define setup and teardown logic for your tests in a reusable and consistent way.
Fixtures integrate seamlessly with the Page Object Model, ensuring that page instances, test data, or other shared resources are properly initialized and cleaned up before and after tests.
How Fixtures Complement POM:
Example: Using Fixtures with a Page Object:
// register.fixture.js
const { test } = require('@playwright/test');
const { RegisterPage } = require('../pages/register.page');
test.use({
registerPage: async ({ page }, use) => {
const registerPage = new RegisterPage(page);
await page.goto('?route=account/register'); // Setup
await use(registerPage); // Provide fixture to tests
// Teardown happens automatically after test
}
});
// register.spec.js
const { test, expect } = require('@playwright/test');
test('register new user', async ({ registerPage }) => {
await registerPage.enterFirstName('steve');
await registerPage.enterLastName('james');
await registerPage.enterEmail('steve.james@gmail.com');
await registerPage.clickPrivacyPolicy();
await registerPage.clickContinue();
await registerPage.assertAccountCreation('Your Account Has Been Created!');
});
By combining Playwright fixtures with the Page Object Model, you streamline test setup, improve reusability, reduce duplication, and maintain clean, readable tests.
Hopefully, this guide gives you a clear understanding of how to implement the Playwright Page Object Model (POM) effectively and the advantages it brings to your automation projects.
By using the Playwright POM framework, you can achieve highly maintainable, reusable, reliable, and readable test automation scripts.
This approach provides a scalable test architecture that works across different applications, browsers, and environments, making it ideal for teams looking to build robust end-to-end automated testing solutions.
On This Page
Did you find this page helpful?
More Related Hubs
Start your journey with LambdaTest
Get 100 minutes of automation test minutes FREE!!