The Page Object Model (POM) is one of the most widely adopted design patterns in UI test automation. Instead of placing selectors and business logic directly inside test files, POM separates them into reusable page classes.
Every web page is represented as a class. The class contains the page's locators and methods, while the test focuses only on business scenarios. This separation makes automation frameworks easier to read, maintain, and extend — which is why almost every enterprise Playwright framework uses some variation of POM.
What is Page Object Model?
POM is a design pattern where each page (or major screen) of your application is represented as a class. The class encapsulates the locators used to interact with the page, plus high-level methods that describe user actions on that page.
Tests never touch selectors directly — they call methods like login(), searchProduct(), or addToCart(). This means UI changes only affect the corresponding page class, not the hundreds of tests that consume it.
Why Use Page Object Model?
Without POM, every test contains duplicated selectors and repeated actions. Imagine 200 tests all using the same login button selector. If the selector changes, every test must be updated. With POM, the selector exists in only one place.
- Improved maintainability — change a locator once, not 200 times.
- Better readability — tests describe behavior, not clicks.
- Reusable code — page methods are shared across the whole suite.
- Easier debugging — failures point to a single page class.
- Cleaner test cases — no clutter of selectors.
- Faster framework development — new tests reuse existing pages.
Problems Without POM
Here is what a test looks like without POM — selectors and actions are hardcoded inside the test itself:
test('Login', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'admin@test.com');
await page.fill('#password', 'password');
await page.click('#login');
});Now imagine hundreds of tests repeating this logic. Changing a single selector becomes expensive, error-prone, and demoralizing.
Creating Your First Page Object
A page object is just a TypeScript class that receives a Playwright Page in its constructor and exposes intent-revealing methods.
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Login' }).click();
}
}This class now owns every interaction with the login page. If the login button becomes an <a> tag tomorrow, only LoginPage.ts changes.
Login Page Example — Extended
Real pages usually expose more than one action. Keep locators private and methods focused on user intent:
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
private readonly email: Locator;
private readonly password: Locator;
private readonly submit: Locator;
private readonly error: Locator;
constructor(private page: Page) {
this.email = page.getByLabel('Email');
this.password = page.getByLabel('Password');
this.submit = page.getByRole('button', { name: 'Login' });
this.error = page.getByRole('alert');
}
async navigate() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.submit.click();
}
async expectError(message: string | RegExp) {
await expect(this.error).toHaveText(message);
}
}Using Page Objects in Tests
The test itself becomes almost plain English — no selectors, no plumbing, just user behavior.
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('Login Test', async ({ page }) => {
const login = new LoginPage(page);
await login.navigate();
await login.login('admin@test.com', 'password');
});A new engineer can read this test and immediately understand what it does, without knowing anything about the login DOM.
Recommended Folder Structure
A predictable folder structure is what turns a personal test project into a framework the whole team can contribute to.
src/
pages/
LoginPage.ts
DashboardPage.ts
ProductPage.ts
CartPage.ts
CheckoutPage.ts
tests/
fixtures/
utils/
models/
services/
data/This structure scales well for enterprise projects and cleanly separates page objects, tests, fixtures, data, and utilities.
Page Components
Large applications share UI sections across many pages — headers, navigation menus, footers, sidebars, modals, search bars. Duplicating locators for these across every page object is exactly the problem POM was invented to solve.
Instead, model each shared section as its own component class and compose it into the pages that use it.
src/components/
Header.ts
Sidebar.ts
Footer.ts
SearchBar.ts
Modal.tsimport { Page, Locator } from '@playwright/test';
export class Header {
private readonly search: Locator;
private readonly account: Locator;
constructor(private page: Page) {
this.search = page.getByRole('searchbox');
this.account = page.getByRole('button', { name: 'Account' });
}
async searchFor(term: string) {
await this.search.fill(term);
await this.search.press('Enter');
}
async openAccountMenu() {
await this.account.click();
}
}Page Manager Pattern
Many enterprise teams introduce a central Page Manager that lazily exposes every page object through a single entry point. Tests never call `new LoginPage(page)` — they access `pages.login` instead.
import { Page } from '@playwright/test';
import { LoginPage } from './LoginPage';
import { DashboardPage } from './DashboardPage';
import { CheckoutPage } from './CheckoutPage';
export class PageManager {
constructor(private page: Page) {}
get login() { return new LoginPage(this.page); }
get dashboard() { return new DashboardPage(this.page); }
get checkout() { return new CheckoutPage(this.page); }
}Combined with a Playwright fixture, tests receive a ready-to-use pages manager:
import { test as base } from '@playwright/test';
import { PageManager } from '../pages/PageManager';
export const test = base.extend<{ pages: PageManager }>({
pages: async ({ page }, use) => {
await use(new PageManager(page));
},
});
export { expect } from '@playwright/test';test('checkout flow', async ({ pages }) => {
await pages.login.navigate();
await pages.login.login('admin@test.com', 'password');
await pages.checkout.completeOrder();
});- Centralized object creation.
- Cleaner dependency management.
- Easier maintenance — one place to wire pages up.
Enterprise Framework Architecture
A production-ready Playwright framework goes beyond `pages/` and `tests/`. It layers page objects with components, fixtures, API services, database helpers, models, and reporting — each concern in its own folder.
src/
pages/ # UI page objects
components/ # Reusable UI sections
fixtures/ # Playwright fixtures (auth, pages, data)
api/ # API clients
services/ # Business services combining API + DB
database/ # DB clients & seeders
utils/ # Loggers, helpers
config/ # Environment config
models/ # TypeScript types & interfaces
constants/ # Enums, URLs, roles
reporting/ # Allure / Slack / Jira integrationsThis modular architecture is what supports thousands of automated tests across many engineers without becoming unmaintainable.
Best Practices
- Keep locators private inside the page class.
- Keep business logic inside page objects, not in tests.
- Avoid assertions inside page objects unless the assertion IS the behavior (e.g. expectError).
- One page = one class. Split components out when they're reused.
- Name methods after user intent: login(), searchProduct(), addToCart(), checkout() — never clickButton1() or fillTextbox2().
- Use TypeScript types for method parameters — no untyped string bags.
- Compose page objects through a Page Manager and expose it via a fixture.
Common Mistakes
- Building one huge page object that contains every page in the app.
- Storing unrelated locators together in a shared 'Selectors' file.
- Writing assertions inside every page method — pages should act, tests should assert.
- Mixing API logic and UI logic in the same class.
- Exposing raw Locators — expose behavior, not internals.
- Skipping the Page Manager and instantiating pages manually in every test.
Interview Questions
What is Page Object Model?
A design pattern that separates UI interactions from test logic by representing each page as a reusable class containing locators and methods.
Why is POM important?
It reduces code duplication, improves maintainability, and makes automation frameworks scalable — a single UI change touches one class instead of hundreds of tests.
Can POM be combined with Fixtures?
Yes. Playwright Fixtures inject dependencies while Page Objects encapsulate business behavior. Together they create highly maintainable enterprise frameworks.
Is POM still recommended in Playwright?
Absolutely. Even with powerful locators and fixtures, Microsoft and the automation community still recommend Page Objects for medium and large projects.
Summary
The Page Object Model remains one of the most important design patterns in modern test automation. By separating page behavior from test scenarios, teams create frameworks that are easier to maintain, scale, and read.
Combined with Playwright Fixtures, Auto Waiting, and robust locators, POM forms the foundation of enterprise-grade automation frameworks used by leading software companies.
Get Playwright tutorials in your inbox
Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.
Playwright Framework Series
View all →- 1Getting Started with Playwright: Installation, Setup, and Your First Test
- 2Playwright Locators: The Complete Guide with Real-World Examples
- 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
- 4Playwright Assertions: The Complete Guide with Real-World Examples
- 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
- 6Playwright Fixtures: The Complete Guide with Real-World Examples
- 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
- 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
- 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
- 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
- 11Page Object Model with Playwright: A Practical Guide
- 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
- 13Building an Enterprise Playwright Framework from Scratch
API Testing Series
View all →- 1Playwright API Testing: The Complete Guide with Real-World Examples
- 2Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
- 3Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
- 4Part 3: Mastering CRUD Operations in Playwright API Testing
- 5Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
- 6Part 5: Building an Enterprise-Level Playwright API Automation Framework
- 7Part 6: API Models, Schema Validation & Test Data Management in Playwright
- 8Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
- 9Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
- 10Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
- 11Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
- 12API Testing with Playwright: Request Context, Auth, and Assertions