Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Without structure, a test suite of 100 Playwright tests becomes unmaintainable: the same page.locator('#email').fill(email) appears in 30 different test files, and when the login form changes, you update 30 places. The Page Object Model (POM) solves this by encapsulating page interactions in a class: the LoginPage class has a login(email, password) method, and all 30 tests call loginPage.login(...) instead. When the form changes, you fix one file. POM also makes tests readable: await checkoutPage.addToCart('Widget'); await checkoutPage.checkout() reads like a user story, not a soup of CSS selectors.
Page Object Model centralizes all selector and interaction logic for a page into a dedicated class, so when the underlying HTML changes, only one file needs updating rather than every test that touches that page. Without POM, a 100-test suite with raw selectors scattered across files becomes unmaintainable within weeks of its first design iteration. The pattern also makes tests read like user stories — loginPage.login(email, password) communicates intent far more clearly than a sequence of fill and click calls.
// ── Without POM (hard to maintain) ──────────────────────────────────────────
// Every test has raw selectors — change one element, update 20+ test files
// test('place order', async ({ page }) => {
// await page.goto('/login');
// await page.locator('#email').fill('user@test.com');
// await page.locator('#password').fill('secret');
// await page.getByRole('button', { name: 'Sign in' }).click();
// await page.goto('/products');
// await page.getByTestId('add-to-cart-widget').click();
// ...
// ── With POM (readable and maintainable) ─────────────────────────────────────
import { Page, Locator, expect } from '@playwright/test';
class LoginPage {
constructor(private page: Page) {}
readonly emailInput: Locator = this.page.getByLabel('Email');
readonly passwordInput: Locator = this.page.getByLabel('Password');
readonly submitBtn: Locator = this.page.getByRole('button', { name: 'Sign in' });
async goto() { await this.page.goto('/login'); }
async login(email: string, password: string) {
await this.goto();
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitBtn.click();
}
}
class ProductPage {
constructor(private page: Page) {}
async goto() { await this.page.goto('/products'); }
async addToCart(productName: string) {
await this.page.getByTestId(`add-to-cart-${productName.toLowerCase()}`).click();
}
async cartCount(): Promise<number> {
const text = await this.page.getByTestId('cart-count').innerText();
return parseInt(text, 10);
}
}
// ── Test file (reads like a user story) ─────────────────────────────────────
import { test } from '@playwright/test';
test('user can add product to cart', async ({ page }) => {
const login = new LoginPage(page);
const products = new ProductPage(page);
await login.login('user@test.com', 'secret');
await products.goto();
await products.addToCart('widget');
expect(await products.cartCount()).toBe(1);
});node main.jsNavigationComponent POM class for a site's nav bar — with clickHome(), clickAbout(), clickSignIn() methods. Use this in three different test files. Now imagine the 'Sign In' button text changes to 'Log In' — verify you only need to update one place.LoginPage.login() method: after clicking submit, check if an error message appears (page.locator('.error-message')) and throw a descriptive error if it does. Now your tests get clear failure messages: 'Login failed: Invalid credentials' instead of 'Expected /dashboard, got /login'.CheckoutPage POM class for a demo e-commerce site. It should have: fillShippingAddress(), selectPaymentMethod(), placeOrder(), getOrderConfirmationNumber(). Write one test that uses all four methods.Use these three in order. Each builds on the one before.
In one paragraph, explain the Page Object Model: what problem it solves, what a Page Object class contains (locators and methods), and how it makes tests more readable.
Walk me through what happens when a developer renames the 'Sign in' button to 'Log in' in a test suite that uses POM vs one that doesn't. How many files need to change in each case? What's the maintenance multiplier?
My team has 200 Playwright tests with no POM — raw selectors everywhere, tests 200 lines long, massive duplication. I have 2 weeks to improve this. Walk me through a prioritization strategy: which tests to refactor into POM first, how to prevent developers from writing new tests without POM, and a measurable quality metric that should improve as a result.