Lesson 9 of 9

Lesson 09 — Page Object Model with strong typing

Title: Encapsulate selectors and flows behind typed classes

Description: Wrap Playwright Page interactions inside cohesive classes (LoginPage, DashboardPage) so specs read like scenarios.

Why it matters for QA: Maintenance drops when selectors live one place — typings force constructors to receive real Playwright pages, not vague any.


1. Prefer Page from Playwright

typescripttypescript
import type { Page } from "@playwright/test";

class LoginPage {
  constructor(private readonly page: Page) {}

  async open(): Promise<void> {
    await this.page.goto("/login");
  }

  async login(email: string, password: string): Promise<void> {
    await this.page.getByLabel("Email").fill(email);
    await this.page.getByLabel("Password").fill(password);
    await this.page.getByRole("button", { name: "Sign in" }).click();
  }
}

2. Why avoid constructor(page: any)

any hides misspelled methods until runtime — precisely when CI is red and you lack a local debugger.


3. Returning locators for flexible assertions

typescripttypescript
import type { Locator, Page } from "@playwright/test";

class DashboardPage {
  constructor(private readonly page: Page) {}

  greeting(): Locator {
    return this.page.getByTestId("dashboard-greeting");
  }
}

4. Composition over inheritance starter

typescripttypescript
class WorkspacePage {
  constructor(
    private readonly page: Page,
    readonly login: LoginPage,
  ) {}

  async bootstrap(email: string, password: string): Promise<void> {
    await this.login.open();
    await this.login.login(email, password);
  }
}