Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
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.
NavigationComponent 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.// ── 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.js