The Page Object Model (POM) is a design pattern that creates an object-oriented abstraction layer between your test code and the UI. By organizing UI elements and interactions into reusable classes, POM improves test maintainability, reduces duplication, and makes tests more readable. This guide will show you how to implement a robust POM architecture for your test automation.
Prerequisites#
- Node.js (v16+ recommended)
- TypeScript (4.5+)
- Testing framework:
- Playwright or Selenium WebDriver
- Basic understanding of object-oriented programming
- Development environment (VS Code recommended)
- Estimated setup time: 30-45 minutes
Understanding Page Object Pattern#
The Page Object Pattern encapsulates web page elements and their interactions within a class, providing a clean interface for tests. Each page in your application gets its own class, containing locators and methods that represent user actions.
💡 Pro Tip: Page Objects should only expose methods that represent user actions, not implementation details. Think "what" the user does, not "how" it's done.
Benefits of Page Object Model#
- Reduced code duplication
- Improved test maintenance
- Better readability
- Easier refactoring
- Separation of concerns
- Type safety with TypeScript
- Reusable components
Basic Page Object Implementation#
// pages/BasePage.tsimport { Page } from "@playwright/test";export class BasePage {constructor(protected page: Page) {}async navigate(url: string) {await this.page.goto(url);}async getTitle(): Promise<string> {return await this.page.title();}async waitForPageLoad() {await this.page.waitForLoadState("networkidle");}}
// pages/BasePage.tsimport { Page } from "@playwright/test";export class BasePage {constructor(protected page: Page) {}async navigate(url: string) {await this.page.goto(url);}async getTitle(): Promise<string> {return await this.page.title();}async waitForPageLoad() {await this.page.waitForLoadState("networkidle");}}
Login Page Example#
// pages/LoginPage.tsimport { Page, Locator } from "@playwright/test";import { BasePage } from "./BasePage";export class LoginPage extends BasePage {private readonly usernameInput: Locator;private readonly passwordInput: Locator;private readonly loginButton: Locator;private readonly errorMessage: Locator;constructor(page: Page) {super(page);this.usernameInput = page.locator("#username");this.passwordInput = page.locator("#password");this.loginButton = page.locator('button[type="submit"]');this.errorMessage = page.locator(".error-message");}async navigateToLogin() {await this.navigate("/login");}async login(username: string, password: string) {await this.usernameInput.fill(username);await this.passwordInput.fill(password);await this.loginButton.click();}async getErrorMessage(): Promise<string> {return (await this.errorMessage.textContent()) || "";}async isErrorVisible(): Promise<boolean> {return await this.errorMessage.isVisible();}}
// pages/LoginPage.tsimport { Page, Locator } from "@playwright/test";import { BasePage } from "./BasePage";export class LoginPage extends BasePage {private readonly usernameInput: Locator;private readonly passwordInput: Locator;private readonly loginButton: Locator;private readonly errorMessage: Locator;constructor(page: Page) {super(page);this.usernameInput = page.locator("#username");this.passwordInput = page.locator("#password");this.loginButton = page.locator('button[type="submit"]');this.errorMessage = page.locator(".error-message");}async navigateToLogin() {await this.navigate("/login");}async login(username: string, password: string) {await this.usernameInput.fill(username);await this.passwordInput.fill(password);await this.loginButton.click();}async getErrorMessage(): Promise<string> {return (await this.errorMessage.textContent()) || "";}async isErrorVisible(): Promise<boolean> {return await this.errorMessage.isVisible();}}
Advanced Page Object Patterns#
Component-Based Approach#
// components/NavigationComponent.tsimport { Page, Locator } from "@playwright/test";export class NavigationComponent {private readonly nav: Locator;private readonly homeLink: Locator;private readonly profileLink: Locator;private readonly logoutButton: Locator;constructor(private page: Page) {this.nav = page.locator("nav.main-navigation");this.homeLink = this.nav.locator('a[href="/home"]');this.profileLink = this.nav.locator('a[href="/profile"]');this.logoutButton = this.nav.locator("button.logout");}async goToHome() {await this.homeLink.click();}async goToProfile() {await this.profileLink.click();}async logout() {await this.logoutButton.click();}}
// components/NavigationComponent.tsimport { Page, Locator } from "@playwright/test";export class NavigationComponent {private readonly nav: Locator;private readonly homeLink: Locator;private readonly profileLink: Locator;private readonly logoutButton: Locator;constructor(private page: Page) {this.nav = page.locator("nav.main-navigation");this.homeLink = this.nav.locator('a[href="/home"]');this.profileLink = this.nav.locator('a[href="/profile"]');this.logoutButton = this.nav.locator("button.logout");}async goToHome() {await this.homeLink.click();}async goToProfile() {await this.profileLink.click();}async logout() {await this.logoutButton.click();}}
Using Page Objects in Tests#
// tests/login.spec.tsimport { test, expect } from "@playwright/test";import { LoginPage } from "../pages/LoginPage";import { DashboardPage } from "../pages/DashboardPage";test.describe("Login Tests", () => {test("successful login redirects to dashboard", async ({ page }) => {const loginPage = new LoginPage(page);const dashboardPage = new DashboardPage(page);await loginPage.navigateToLogin();await expect(page).toHaveURL("/dashboard");await expect(dashboardPage.welcomeMessage).toBeVisible();});test("invalid credentials show error message", async ({ page }) => {const loginPage = new LoginPage(page);await loginPage.navigateToLogin();expect(await loginPage.isErrorVisible()).toBe(true);expect(await loginPage.getErrorMessage()).toContain("Invalid credentials");});});
// tests/login.spec.tsimport { test, expect } from "@playwright/test";import { LoginPage } from "../pages/LoginPage";import { DashboardPage } from "../pages/DashboardPage";test.describe("Login Tests", () => {test("successful login redirects to dashboard", async ({ page }) => {const loginPage = new LoginPage(page);const dashboardPage = new DashboardPage(page);await loginPage.navigateToLogin();await expect(page).toHaveURL("/dashboard");await expect(dashboardPage.welcomeMessage).toBeVisible();});test("invalid credentials show error message", async ({ page }) => {const loginPage = new LoginPage(page);await loginPage.navigateToLogin();expect(await loginPage.isErrorVisible()).toBe(true);expect(await loginPage.getErrorMessage()).toContain("Invalid credentials");});});
⚠️ Warning: Avoid adding assertions within Page Object methods. Keep assertions in your test files to maintain clear separation between test logic and page interactions.
Troubleshooting Common Issues#
Best Practices for Page Object Model#
- Keep page objects focused on a single page or component
- Use TypeScript for type safety and better IDE support
- Avoid exposing internal implementation details
- Use descriptive method names that reflect user actions
- Return page objects from methods to enable method chaining
- Keep assertions out of page objects
- Use data-testid attributes for stable element selection
- Create a base page class for common functionality
- Document complex interactions with comments
Next Steps#
- Implement page objects for your critical user flows
- Refactor existing tests to use the POM pattern
- Create reusable component objects for common UI elements
- Explore advanced patterns like the Screenplay pattern
- Set up a consistent project structure for page objects
By implementing the Page Object Model, you'll create a more maintainable and scalable test automation framework. This pattern is widely adopted in the industry and will make your tests easier to understand and maintain as your application grows.