As automation projects grow, maintaining hundreds or even thousands of test scripts can become challenging. Duplicate code, hard-coded locators, and repeated business logic make test suites difficult to maintain. The Page Object Model (POM) is a design pattern that solves these problems by separating page interactions from test logic. In this guide, you’ll learn what the Page Object Model is, why it’s important, and how to implement it in both TypeScript and Java.
What is the Page Object Model?
The Page Object Model (POM) is a design pattern in which each web page is represented as a separate class.
Instead of writing locators and actions directly inside test scripts, they are organized into page classes.
For example:
Login Page│├── Username field├── Password field├── Login button└── login() method
The test simply calls methods from the page class, making the test more readable and easier to maintain.
Why Use the Page Object Model?
Without POM, test scripts often contain repeated locators and duplicated logic.
Without POM
await page.goto("https://example.com/login");await page.getByLabel("Username").fill("admin");await page.getByLabel("Password").fill("admin123");await page.getByRole("button", { name: "Login" }).click();
If the login button changes, every test using it must be updated.
With POM
await loginPage.login("admin", "admin123");
Only the page class needs to be updated when the UI changes.
Benefits of the Page Object Model
Using POM provides several advantages:
- Better code organization
- Reduced code duplication
- Easier maintenance
- Improved readability
- Reusable page methods
- Simplified debugging
- Scalable framework design
Recommended Project Structure
TypeScript
playwright-project│├── pages│ ├── LoginPage.ts│ ├── HomePage.ts│ └── CartPage.ts│├── tests│ └── login.spec.ts│├── utils├── playwright.config.ts└── package.json
Java
playwright-java│├── pages│ ├── LoginPage.java│ ├── HomePage.java│ └── CartPage.java│├── tests├── utils└── pom.xml
Creating a Page Object in TypeScript
LoginPage.ts
import { Page } from '@playwright/test';export class LoginPage { constructor(private page: Page) {} async navigate() { await this.page.goto('https://example.com/login'); } async login(username: string, password: string) { await this.page.getByLabel('Username').fill(username); await this.page.getByLabel('Password').fill(password); await this.page.getByRole('button', { name: 'Login' }).click(); }}
The page class contains all locators and page-specific actions.
Using the Page Object in a Test
import { test } from '@playwright/test';import { LoginPage } from '../pages/LoginPage';test('Valid Login', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.navigate(); await loginPage.login('admin', 'admin123');});
The test focuses only on business logic.
Creating a Page Object in Java
import com.microsoft.playwright.Page;public class LoginPage { private final Page page; public LoginPage(Page page) { this.page = page; } public void navigate() { page.navigate("https://example.com/login"); } public void login(String username, String password) { page.getByLabel("Username").fill(username); page.getByLabel("Password").fill(password); page.getByRole( AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login") ).click(); }}
Using the Java Page Object
LoginPage loginPage = new LoginPage(page);loginPage.navigate();loginPage.login("admin", "admin123");
The test remains concise and easy to understand.
Separating Locators and Actions
A Page Object should contain:
- Element locators
- Page-specific methods
- Navigation methods
- Validation methods
It should not contain test assertions or business workflows spanning multiple pages.
Base Page Pattern
In larger frameworks, common functionality can be moved to a base page.
Example:
export class BasePage { constructor(protected page: Page) {} async takeScreenshot() { await this.page.screenshot(); } async getTitle() { return await this.page.title(); }}
Other page classes can extend this base class.
Combining POM with Fixtures
Playwright fixtures can simplify page creation.
Example:
test.beforeEach(async ({ page }) => { loginPage = new LoginPage(page);});
This avoids creating page objects repeatedly in every test.
Common Mistakes
Adding Assertions Inside Page Objects
Avoid mixing verification with page interaction.
Incorrect:
async verifyLogin() { await expect(this.page).toHaveTitle("Dashboard");}
Assertions belong in test files, not page classes.
Creating Large Page Classes
Keep page objects focused on a single page or component.
Duplicating Common Methods
Move shared functionality into a base page or utility class.
Hard-Coding Test Data
Use configuration files or test data providers instead of embedding values in page classes.
Best Practices
- One class per page
- Keep locators private when possible
- Expose meaningful business methods
- Avoid duplicate locators
- Use Playwright locators (
getByRole,getByLabel,getByTestId) - Store reusable methods in a base page
- Keep assertions in test classes
- Follow consistent naming conventions
POM vs Tests
| Page Object | Test File |
|---|---|
| Locators | Test scenarios |
| Page actions | Assertions |
| Navigation | Business flow |
| Reusable methods | Test data |
This separation improves readability and maintainability.
Frequently Asked Questions
What is the Page Object Model?
The Page Object Model is a design pattern that represents each web page as a class containing locators and reusable methods.
Why is POM important?
It reduces code duplication, improves maintainability, and keeps test scripts clean.
Should assertions be placed inside Page Objects?
Generally, no. Assertions should remain in the test layer, while page objects focus on interactions.
Can Playwright use POM with TypeScript?
Yes. TypeScript is the most common language used for implementing Playwright Page Object Models.
Can I use POM with Playwright Java?
Yes. Playwright’s Java API fully supports the Page Object Model and is widely used in enterprise automation frameworks.
Conclusion
The Page Object Model is one of the most valuable design patterns for building scalable Playwright automation frameworks. By separating page interactions from test logic, teams can create test suites that are easier to read, maintain, and extend.
Whether you’re using TypeScript or Java, implementing POM early in your project will help reduce duplication, simplify maintenance, and support long-term growth as your automation suite expands.
Related Articles
✓ Playwright Test Runner Explained
✓ Playwright UI Elements Interactions
✓ Playwright Architecture Explained
✓ Playwright Setup with TypeScript
✓ Playwright Locator Strategies
✓ Playwright Auto-Waiting Mechanism
✓ Complete Playwright Tutorial
Discover more from Rotebit
Subscribe to get the latest posts sent to your email.

Pingback: Playwright CI/CD Integration - Rotebit
Pingback: Playwright Cloud Execution using BrowserStack vs TestMu AI - Rotebit