Ai Driven TestingIntermediate

Page Object Model Implementation

Implement maintainable page object pattern for scalable test architecture

ObserveOne Team
4 min read

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#

  1. Reduced code duplication
  2. Improved test maintenance
  3. Better readability
  4. Easier refactoring
  5. Separation of concerns
  6. Type safety with TypeScript
  7. Reusable components

Basic Page Object Implementation#

// pages/BasePage.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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 loginPage.login("[email protected]", "securePassword123!");
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();
await loginPage.login("[email protected]", "wrongpassword");
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#

Problem
Page Objects becoming too large and unwieldy
Solution
Break down complex pages into smaller component objects and compose them together using the component pattern
Problem
Duplicate locators across multiple page objects
Solution
Extract common locators into a shared constants file or create a base component class for reusable UI elements
Problem
Tests breaking when UI structure changes
Solution
Use data-testid attributes for stable selectors and centralize locator definitions in Page Objects for easy updates

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#

  1. Implement page objects for your critical user flows
  2. Refactor existing tests to use the POM pattern
  3. Create reusable component objects for common UI elements
  4. Explore advanced patterns like the Screenplay pattern
  5. 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.

Ready for AI-Powered Testing?

ObserveOne monitors your selectors 24/7 and automatically heals them when websites change. Never deal with broken tests again.

Start Free Trial