11.9 — Page Object Model
Your suite is growing — and twenty spec files now contain the same getByLabel("Coupon"). One UI rename away from twenty edits. The Page Object Model: one class per screen — locators as properties, user verbs as methods — so page knowledge lives in exactly ONE place. 5.6’s classes finally meet their killer app.
// pages/checkout-page.ts
export class CheckoutPage {
constructor(page) {
this.page = page;
this.coupon = page.getByLabel("Coupon");
this.total = page.getByTestId("total");
}
async goto() {
await this.page.goto("/checkout");
}
async applyCoupon(code) {
await this.coupon.fill(code);
await this.page
.getByRole("button", { name: "Apply" }).click();
}
}
// in the spec — reads as a user story:
const checkout = new CheckoutPage(page);
await checkout.goto();
await checkout.applyCoupon("SAVE10");
await expect(checkout.total).toHaveText("900");The smell, concretely: twenty specs each build getByLabel("Coupon") and click the Apply button themselves. The knowledge “how the checkout screen works” is smeared across the whole suite — every spec knows the DOM personally.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
In TypeScript (8.5, and your scaffold’s choice) the class gets typed: constructor(private readonly page: Page), properties as readonly coupon: Locator. The types buy the usual: autocomplete on every page object across the suite and typo-death at the desk.
POMs pair beautifully with fixtures (11.7): define a checkoutPage fixture that builds new CheckoutPage(page), and specs just declare { checkoutPage } — injection all the way down. Most mature suites you’ll join wire POMs through fixtures exactly this way.
Methods that navigate somewhere often return the NEXT page object (login() { …; return new DashboardPage(this.page) }) — chaining screens the way users flow through them. Nice when natural; forced fluent-chaining everywhere is a style disease. Judgment, as always.
Job note: “explain the Page Object Model” is a top-three automation interview question. The answer that lands: locator duplication is a maintenance bug; one class per screen makes page knowledge single-sourced so UI change costs one edit; specs read as user stories; assertions stay in specs. Four sentences, and you’ve out-answered most candidates — because you can also say when NOT to use it.
⌨️ build a page object
The starter fakes a checkout screen. Wrap it in a page object — locate once, act with user verbs — and write a spec that reads as a user story.
requirements:
- Keep the starter’s
app— the fake screen with anapply(code)behavior and atotal. - Write
class CheckoutPage(5.6!): the constructor takes the app and stores it onthis; anapplyCoupon(code)method delegates to the app’s apply; areadTotal()method returns the app’s total. - The spec:
new CheckoutPage(app), apply"SAVE10", printtotal: 900(built from readTotal). Then a second instance on a fresh app — apply"BOGUS", printtotal: 1000.
when you press RUN, the console must show exactly:
✏️ Quick check 1
The Coupon label is renamed. With a POM, how many FILES do you edit?
✏️ Quick check 2
Where do expect() assertions belong — in the page object’s methods, or in the spec?
✏️ Quick check 3
Storing twenty locators as properties in a constructor — expensive or cheap, and why? (one word for the reason)
🗣️ Now teach it back
Explain the POM to a friend: the duplication problem, the class shape (what becomes a property, what becomes a method), what specs look like after, the one-edit-heals-all payoff (name the principle), and when POM is overkill.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.