Q1 of 17 · Framework design

What is the Page Object Model, and why is it the standard pattern for UI test automation?

Framework designJuniorframework-designpage-object-modelpomdesign-patterns

Short answer

Short answer: POM is a design pattern where each page or component of the UI has a corresponding class that encapsulates its locators and actions. Tests call PO methods rather than interacting with locators directly, making tests readable and locator changes a one-file fix.

Detail

Without POM — locators scattered across tests:

// test1.spec.ts
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'secret');
await page.click('#login-btn');

// test2.spec.ts — same locators duplicated
await page.fill('#email', 'admin@example.com');
await page.fill('#password', 'adminpass');
await page.click('#login-btn');

When the dev changes the email input to data-testid="email-field", you update 20 test files.

With POM — locators owned by one class:

// pages/LoginPage.ts
export class LoginPage {
  private readonly emailInput = this.page.locator('#email');
  private readonly passwordInput = this.page.locator('#password');
  private readonly loginButton = this.page.locator('#login-btn');

  constructor(private readonly page: Page) {}

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}

// test1.spec.ts
const loginPage = new LoginPage(page);
await loginPage.login('user@example.com', 'secret');

Benefits:

  • Locator change → fix in one file, all tests updated automatically
  • Tests read like business flows, not CSS selectors
  • Intellisense autocomplete on PO methods speeds up test writing
  • PO methods can be unit tested in isolation

Common rule: Page Objects should not contain assertions — return values or state, let the test assert. This keeps POs reusable across positive and negative tests.

// EXAMPLE

// pages/CheckoutPage.ts
export class CheckoutPage {
  readonly orderConfirmation = this.page.locator('[data-testid="order-confirmation"]');

  constructor(private readonly page: Page) {}

  async completeCheckout(card: string) {
    await this.page.fill('[data-testid="card-number"]', card);
    await this.page.click('[data-testid="pay-btn"]');
  }
}

// WHAT INTERVIEWERS LOOK FOR

The 'one file to update when locators change' benefit. No assertions in POs. A before/after code comparison to illustrate the value.

// COMMON PITFALL

Putting assertions inside Page Object methods — this makes them untestable with different expected states and ties them to a single test scenario.