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
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
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
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);
}
}