Playwright Masters

Page Object Model in Playwright: Complete Guide With Examples

If you are writing Playwright tests, you may start by putting everything inside one test file.

At first, this looks easy.

But imagine having 100 test cases.

Your login locator appears in 30 tests. Your search box appears in 20 tests. Your checkout button appears in 15 tests.

Now the application changes.

The login button gets a new locator.

You may need to change the same locator in many test files.

This is where the Page Object Model (POM) becomes useful.

Table of Contents

The Page Object Model is a design pattern that organizes your Playwright automation code by separating test scenarios from page interactions.

Instead of putting every locator and browser action directly inside the test, you create reusable page classes.

Playwright officially recommends page objects as one way to structure large test suites because they can provide a higher-level API for the application, centralize selectors, and reduce repeated code.

In simple words:

Page Object Model means keeping page-related locators and actions in one reusable place so your tests become cleaner and easier to maintain.

Playwright - Page object model

What Is Page Object Model?

The Page Object Model, commonly called POM, is a test automation design pattern.

In POM, an application page or important UI component is represented by a class.

That class normally contains:

  • The Playwright Page object
  • Locators
  • Page actions
  • Reusable workflows
  • Optional state-reading methods

For example, suppose you have a login page.

Without POM, your test may contain:

await page.getByLabel(‘Username’).fill(‘rakesh’);

await page.getByLabel(‘Password’).fill(‘password123’);

await page.getByRole(‘button’, { name: ‘Login’ }).click();

 

If five tests need the same login flow, the same code may be repeated five times.

With POM, you can create:

export class LoginPage {

  constructor(private page: Page) {}

 

  readonly username = this.page.getByLabel(‘Username’);

  readonly password = this.page.getByLabel(‘Password’);

  readonly loginButton = this.page.getByRole(‘button’, { name: ‘Login’ });

 

  async login(username: string, password: string) {

    await this.username.fill(username);

    await this.password.fill(password);

    await this.loginButton.click();

  }

}

 

Your test can then focus on the business scenario:

const loginPage = new LoginPage(page);

 

await loginPage.login(‘rakesh’, ‘password123’);

 

The test now says what the user is doing instead of describing every low-level browser operation.

Why Is Page Object Model Important in Playwright?

POM becomes especially useful when a Playwright project grows.

A small automation project may have only three tests.

A real project may eventually have hundreds or thousands of tests.

Without structure, the test suite can become difficult to understand and maintain.

POM helps solve this problem by creating a separation between:

Test intent

and

UI implementation

For example:

Test:

User logs in successfully

 

Page Object:

Find username

Fill username

Find password

Fill password

Click login

 

This separation gives your automation project a cleaner architecture.

Page Object Model Example in Playwright

Let’s create a simple login page.

Step 1: Create the LoginPage class

Create:

pages/

└── LoginPage.ts

 

Then add:

import { type Locator, type Page } from ‘@playwright/test’;

 

export class LoginPage {

  readonly page: Page;

  readonly username: Locator;

  readonly password: Locator;

  readonly loginButton: Locator;

 

  constructor(page: Page) {

    this.page = page;

 

    this.username = page.getByLabel(‘Username’);

    this.password = page.getByLabel(‘Password’);

    this.loginButton = page.getByRole(‘button’, {

      name: ‘Login’

    });

  }

 

  async open() {

    await this.page.goto(‘/login’);

  }

 

  async login(username: string, password: string) {

    await this.username.fill(username);

    await this.password.fill(password);

    await this.loginButton.click();

  }

}

 

The page object now owns the login page’s locators and actions.

Step 2: Use the Page Object in a Test

Create:

tests/

└── login.spec.ts

 

Example:

import { test, expect } from ‘@playwright/test’;

import { LoginPage } from ‘../pages/LoginPage’;

 

test(‘user can login successfully’, async ({ page }) => {

  const loginPage = new LoginPage(page);

 

  await loginPage.open();

 

  await loginPage.login(

    ‘testuser@example.com’,

    ‘password123’

  );

 

  await expect(page).toHaveURL(/dashboard/);

});

 

Notice something important.

The test does not need to know how the username field is located.

It simply says:

await loginPage.login(…)

 

That is one of the main strengths of POM.

Page Object Model Folder Structure in Playwright

A clean project structure can look like this:

playwright-project/

├── pages/

│   ├── LoginPage.ts

│   ├── HomePage.ts

│   ├── ProductPage.ts

│   └── CheckoutPage.ts

├── components/

│   ├── Header.ts

│   ├── Navigation.ts

│   └── Cart.ts

├── tests/

│   ├── login.spec.ts

│   ├── product.spec.ts

│   └── checkout.spec.ts

├── fixtures/

│   └── testFixtures.ts

├── utils/

│   ├── testData.ts

│   └── helpers.ts

├── playwright.config.ts

└── package.json

This structure separates responsibilities.

pages

Contains page objects.

components

Contains reusable UI parts.

tests

Contains actual test scenarios.

fixtures

Contains reusable Playwright test setup.

utils

Contains genuinely reusable utilities.

Do not create folders simply to make the project look complicated.

The structure should make the project easier to understand.

Page Object Model vs Normal Playwright Test

Consider a simple test without POM:

test(‘login’, async ({ page }) => {

  await page.goto(‘/login’);

 

  await page.getByLabel(‘Username’).fill(‘user’);

  await page.getByLabel(‘Password’).fill(‘pass’);

 

  await page.getByRole(‘button’, {

    name: ‘Login’

  }).click();

 

  await expect(page).toHaveURL(/dashboard/);

});

 

This works.

There is nothing technically wrong with it.

But if the same login interaction appears in many tests, duplication starts to grow.

With POM:

test(‘login’, async ({ page }) => {

  const loginPage = new LoginPage(page);

 

  await loginPage.open();

  await loginPage.login(‘user’, ‘pass’);

 

  await expect(page).toHaveURL(/dashboard/);

});

 

The test is shorter and describes the user journey more clearly.

Benefits of Page Object Model in Playwright

1. Reduces Duplicate Code

If the same interaction appears in many tests, move it into the page object.

For example:

async login(username: string, password: string) {

  await this.username.fill(username);

  await this.password.fill(password);

  await this.loginButton.click();

}

 

Now every test can reuse it.

2. Easier Maintenance

Imagine your login button changes from:

<button>Login</button>

 

to a different implementation.

If the locator is centralized inside LoginPage, you have one primary place to update.

You do not have to search through every test to find duplicated login code.

This centralization is one of the major reasons POM is useful for larger test suites.

3. Better Test Readability

  • Compare:

    await page.locator(‘#username’).fill(‘user’);

    await page.locator(‘#password’).fill(‘pass’);

    await page.locator(‘#login’).click();


    with:

    await loginPage.login(‘user’, ‘pass’);


    The second version is easier to understand.

    A person reading the test immediately knows that the user is logging in.

4. Better Reusability

One page object can be used by many test cases.

For example:

login.spec.ts

checkout.spec.ts

profile.spec.ts

orders.spec.ts

 

All can reuse:

LoginPage

 

This reduces unnecessary repetition.

5. Easier Team Collaboration

Large automation projects are often worked on by multiple testers and developers.

A consistent POM structure gives everyone a predictable place to find:

  • Locators
  • Page actions
  • Components
  • Tests
  • Fixtures

That makes the codebase easier to navigate.

Page Object Model and Playwright Locators

Good POM architecture depends heavily on good locators.

Playwright provides user-facing locator methods such as:

page.getByRole()

page.getByLabel()

page.getByText()

page.getByPlaceholder()

page.getByTestId()

For example:

this.loginButton = page.getByRole(‘button’, {

  name: ‘Login’

});

This is generally easier to understand than blindly using a long CSS selector.

Playwright’s official documentation provides detailed guidance on the Page API and locator-based interaction.

When designing a POM, choose locators that represent how users identify elements whenever practical.

Should Assertions Be Inside Page Objects?

This is an important design question.

There is no single rule that works for every project.

A useful approach is to keep most business assertions in test files, while page objects expose actions or useful state information.

For example:

async getWelcomeMessage() {

  return this.page.getByRole(‘heading’, {

    name: /welcome/i

  });

}

 

The test can then decide what should be verified:

await expect(

  await dashboardPage.getWelcomeMessage()

).toBeVisible();

 

The important idea is to avoid turning every page object into a giant collection of test cases.

Keep responsibilities clear.

Page Object Model With Reusable Components

Not everything should be treated as a complete page.

Modern applications contain reusable components.

For example:

Header

Navigation

Search box

Shopping cart

Date picker

Modal

Sidebar

Footer

 

Instead of copying these locators into every page object, you can create component objects.

Example:

export class Header {

  constructor(private page: Page) {}

 

  readonly searchBox = this.page.getByRole(‘searchbox’);

 

  async search(text: string) {

    await this.searchBox.fill(text);

    await this.searchBox.press(‘Enter’);

  }

}

 

Then another page can use the component.

This creates a more flexible architecture than simply creating one enormous class for every screen.

Page Object Model With Playwright Fixtures

This is where a basic POM implementation can become much better.

A traditional POM test often does this:

const loginPage = new LoginPage(page);

const homePage = new HomePage(page);

For a small project, that is completely fine.

For a larger project, repeatedly creating page objects can become boilerplate.

Playwright fixtures can provide reusable page objects to tests.

For example:

import { test as base } from ‘@playwright/test’;

import { LoginPage } from ‘../pages/LoginPage’;

type Fixtures = {

  loginPage: LoginPage;

};

export const test = base.extend<Fixtures>({

  loginPage: async ({ page }, use) => {

    await use(new LoginPage(page));

  }

});

Then the test can become:

test(‘user can login’, async ({ loginPage }) => {

  await loginPage.open();

  await loginPage.login(‘user’, ‘password’);

});

This approach combines two powerful ideas:

Page Object Model + Playwright Fixtures

Playwright’s fixture system is designed to provide reusable test setup and dependencies, while Checkly’s current Playwright guidance also highlights fixtures as a useful way to remove repetitive POM initialization.

Page Object Model for Multi-Page Workflows

Real applications rarely have only one page.

Imagine an e-commerce flow:

Login

   ↓

Home

   ↓

Product

   ↓

Cart

   ↓

Checkout

   ↓

Payment

 

You can create:

LoginPage

HomePage

ProductPage

CartPage

CheckoutPage

PaymentPage

 

A test can then describe the journey:

await loginPage.login(username, password);

 

await productPage.openProduct(‘Laptop’);

 

await productPage.addToCart();

 

await cartPage.open();

 

await cartPage.proceedToCheckout();

 

await checkoutPage.completeOrder();

 

This is much easier to read than hundreds of raw browser commands.

Page Object Model for Authentication

Authentication is one area where POM can be useful, but you should not automatically perform a full UI login before every test.

For large suites, Playwright supports authentication-state reuse using storageState.

A common architecture is:

Authentication setup

        ↓

Saved authentication state

        ↓

Test fixtures

        ↓

Page Objects

        ↓

Test scenarios

 

This can reduce unnecessary login work and make tests faster.

For example, instead of logging in through the UI before every test, a setup project can authenticate once and save the required state for later tests.

Use UI login tests to verify that login itself works.

Use saved authentication state where appropriate for tests that are testing other functionality.

Page Object Model Best Practices

1. Keep Page Objects Focused

A LoginPage should not contain shopping-cart logic.

Keep each class responsible for a clear area of the application.

2. Do Not Create Giant Page Classes

Avoid creating something like:

ApplicationPage.ts

 

with thousands of lines.

Break large interfaces into meaningful pages and components.

3. Prefer Stable Locators

Avoid depending on fragile selectors when better user-facing or test-specific locators are available.

Good:

page.getByRole(‘button’, { name: ‘Submit’ })

 

Potentially fragile:

page.locator(‘div:nth-child(7) > span > button’)

4. Avoid Hard Waits

Do not solve synchronization problems with:

await page.waitForTimeout(5000);

 

A five-second sleep does not make a test intelligent.

Prefer Playwright’s locator waiting and assertions.

5. Keep Test Intent in the Test

A test should answer:

What are we testing?

The page object should answer:

How do we interact with the application?

This separation makes automation easier to maintain.

6. Use TypeScript Types

For larger Playwright projects, TypeScript can make page objects safer and easier to maintain.

For example:

import { type Page, type Locator } from ‘@playwright/test’;

This makes the expected types clear.

Common Page Object Model Mistakes

POM is useful, but bad POM can make a project worse.

Mistake 1: Creating POM for Every Tiny Test

If you have one tiny test, creating five classes may add unnecessary complexity.

POM becomes more valuable when code is reused or the suite is growing.

Mistake 2: Putting Everything in One Class

A huge class becomes difficult to understand.

Use pages and reusable components.

Mistake 3: Duplicating Methods

If five page objects contain the exact same code, consider whether that behavior belongs in a reusable component or helper.

Mistake 4: Using POM as a Locator Dump

A page object should not simply contain hundreds of selectors.

Create meaningful methods around user actions.

Instead of:

clickButton1()

clickButton2()

clickButton3()

prefer meaningful operations where appropriate:

async submitOrder() {

  await this.placeOrderButton.click();

}

Mistake 5: Hiding Important Test Logic

Do not move the entire test into a page object just to make the .spec.ts file short.

A short test is not automatically a good test.

The goal is clarity, not minimum line count.

Page Object Model vs Fixtures

These are not necessarily competitors.

They solve different problems.

Feature

Page Object Model

Playwright Fixtures

Main purpose

Organize page interactions

Provide reusable test setup

Stores locators

Yes

Not its primary purpose

Stores page actions

Yes

Not its primary purpose

Dependency injection

No

Yes

Reusable across tests

Yes

Yes

Useful for large projects

Yes

Yes

Can work together

Yes

Yes

A strong Playwright framework can use both.

Think of it this way:

POM organizes your application interactions.

Fixtures organize how those objects and dependencies are provided to tests.

When Should You Use Page Object Model?

Use POM when:

  • You have many Playwright tests.
  • The same page is used by multiple tests.
  • The same interactions are repeated.
  • Your application UI changes regularly.
  • Multiple testers work on the same automation framework.
  • You want cleaner test files.
  • You need reusable workflows.
  • You are building a long-term automation framework.

You may not need a full POM architecture for a one-off script.

The goal is not to use POM because everyone else uses it.

The goal is to use it when the structure makes your automation easier to maintain.

A Practical Playwright POM Architecture

For a growing automation project, this is a useful starting point:

playwright-project/

├── pages/

│   ├── LoginPage.ts

│   ├── HomePage.ts

│   ├── ProductPage.ts

│   └── CheckoutPage.ts

├── components/

│   ├── Header.ts

│   ├── Cart.ts

│   └── Modal.ts

├── fixtures/

│   └── testFixtures.ts

├── tests/

│   ├── login.spec.ts

│   ├── product.spec.ts

│   └── checkout.spec.ts

├── test-data/

│   └── users.json

├── utils/

│   └── helpers.ts

└── playwright.config.ts

 

This is not a rule that every project must follow.

Adapt the structure to your application’s size and team.

Is Page Object Model Still Useful in Modern Playwright?

Yes.

But modern Playwright automation should not stop at basic POM.

A mature framework can combine:

Page Objects

  •  

Reusable Components

  •  

Playwright Fixtures

  •  

Stable Locators

  •  

Authentication State

  •  

API Setup

  •  

Parallel Execution

  •  

CI/CD

  •  

Trace and Reporting

This gives you a much stronger automation architecture than simply creating a pages folder.

Playwright’s own documentation describes page objects as one approach for structuring large test suites and emphasizes the value of capturing selectors in one place and avoiding repetition.

Page Object Model: Simple Real-World Example

Imagine an online shopping website.

The user wants to buy a laptop.

The test should read something like:

test(‘user can purchase a laptop’, async ({

  loginPage,

  productPage,

  cartPage,

  checkoutPage

}) => {

 

  await loginPage.login(

    ‘user@example.com’,

    ‘password’

  );

 

  await productPage.searchProduct(‘Laptop’);

 

  await productPage.addProductToCart(‘Laptop’);

 

  await cartPage.open();

 

  await cartPage.proceedToCheckout();

 

  await checkoutPage.completeOrder();

});

 

Look at the test.

You can almost read it like a sentence.

That is the real goal of good Page Object Model design.

The test should describe the user journey.

The page object should hide unnecessary implementation details.

How to Learn Page Object Model in Playwright

If you are a beginner, learn POM in this order:

Step 1: Learn Playwright basics

Understand:

  • Browser
  • Context
  • Page
  • Locators
  • Assertions
  • Test runner

Step 2: Learn TypeScript basics

Understand:

  • Classes
  • Constructors
  • Methods
  • Objects
  • Types
  • Imports and exports

Step 3: Create your first page object

Start with:

LoginPage

 

Step 4: Move locators into the class

Keep the page-specific locators together.

Step 5: Create reusable actions

For example:

login()

logout()

search()

addToCart()

 

Step 6: Use the page object from tests

Keep test scenarios separate.

Step 7: Learn Playwright fixtures

Use fixtures when repeated object initialization becomes unnecessary boilerplate.

Step 8: Build a real project

Create POMs for:

Login

Home

Search

Product

Cart

Checkout

 

That is where the architecture becomes easier to understand.

Final Takeaway - POM

The Page Object Model in Playwright is more than creating a pages folder.

Good POM architecture means creating a clean boundary between what your test wants to verify and how the application is operated.

A strong implementation should:

  • Keep page interactions reusable.
  • Use stable Playwright locators.
  • Avoid unnecessary duplication.
  • Keep tests focused on behavior.
  • Use components when UI pieces are shared.
  • Use fixtures when repeated setup becomes boilerplate.
  • Avoid giant page classes.
  • Avoid hard waits.
  • Keep the architecture simple enough for the team to understand.

When a Playwright project grows from a few tests into a serious automation framework, this separation can make a major difference.

If you are learning Playwright for automation testing, Page Object Model is one of the most important framework-design concepts to understand because it teaches you how to move from writing individual automation scripts to building maintainable automation systems.

Frequently Asked Questions About Page Object Model

What is Page Object Model in Playwright?

Page Object Model is a design pattern that represents pages or application components as reusable objects. Locators and common interactions are kept inside those objects, while tests focus on the scenario being verified.

Why use Page Object Model in Playwright?

POM can reduce duplicate code, improve test readability, centralize page interactions, and make large Playwright test suites easier to maintain.

Is Page Object Model mandatory in Playwright?

No. Playwright does not require POM. It is an architectural pattern that can be useful when a test suite becomes large or contains repeated page interactions.

What is a Page Object in Playwright?

A Page Object is normally a class or module that represents a page or component of an application and provides reusable locators and actions for interacting with it.

Should assertions be inside Page Object Model?

Not always. A good design often keeps test-specific assertions in the test while allowing page objects to expose useful locators, actions, or state-reading methods.

Can Page Object Model and Playwright fixtures be used together?

Yes. They work well together. POM organizes page interactions, while fixtures can create and provide page objects to tests automatically.

Is Page Object Model good for beginners?

Yes, if it is taught progressively. Beginners should first understand Playwright pages, locators and assertions before creating a complex POM architecture.

What is the difference between POM and fixtures in Playwright?

POM mainly organizes application pages and interactions. Fixtures provide reusable setup and dependencies to tests. They can be combined in the same framework.

Which locator should I use inside a Page Object?

Prefer stable, user-facing locators such as getByRole() and getByLabel() when appropriate. Use test IDs or other stable selectors when they provide a better contract for the application.

Can Page Object Model make Playwright tests faster?

POM itself does not automatically make browser execution faster. Its primary benefit is code organization, reuse and maintainability. Performance improvements usually come from better test architecture, parallel execution, authentication-state reuse, efficient setup and other Playwright capabilities.

When should I avoid Page Object Model?

Avoid overengineering a tiny one-off test suite. If introducing multiple classes makes a simple test harder to understand, a direct Playwright test may be better.

Playwright Masters automation testing logo - White Back ground

Playwright Masters Team

Playwright Automation Testing Experts | Industry-Focused Training & Practical Learning

Playwright Masters is a dedicated automation testing training platform focused on helping learners build practical skills in Playwright automation testing. Our training covers real-world automation concepts, framework practices, coding, debugging, API testing, cross-browser testing, CI/CD, and interview preparation to help learners develop job-ready testing skills.

Scroll to Top