Q1 of 17 · Framework design
What is the Page Object Model, and why is it the standard pattern for UI test automation?
Framework designJuniorframework-designpage-object-modelpomdesign-patterns
Short answer
Short answer: POM is a design pattern where each page or component of the UI has a corresponding class that encapsulates its locators and actions. Tests call PO methods rather than interacting with locators directly, making tests readable and locator changes a one-file fix.
Detail
Without POM — locators scattered across tests:
// test1.spec.ts
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'secret');
await page.click('#login-btn');
// test2.spec.ts — same locators duplicated
await page.fill('#email', 'admin@example.com');
await page.fill('#password', 'adminpass');
await page.click('#login-btn');
When the dev changes the email input to data-testid="email-field", you update 20 test files.
With POM — locators owned by one class:
// pages/LoginPage.ts
export class LoginPage {
private readonly emailInput = this.page.locator('#email');
private readonly passwordInput = this.page.locator('#password');
private readonly loginButton = this.page.locator('#login-btn');
constructor(private readonly page: Page) {}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
// test1.spec.ts
const loginPage = new LoginPage(page);
await loginPage.login('user@example.com', 'secret');
Benefits:
- Locator change → fix in one file, all tests updated automatically
- Tests read like business flows, not CSS selectors
- Intellisense autocomplete on PO methods speeds up test writing
- PO methods can be unit tested in isolation
Common rule: Page Objects should not contain assertions — return values or state, let the test assert. This keeps POs reusable across positive and negative tests.
// EXAMPLE
// pages/CheckoutPage.ts
export class CheckoutPage {
readonly orderConfirmation = this.page.locator('[data-testid="order-confirmation"]');
constructor(private readonly page: Page) {}
async completeCheckout(card: string) {
await this.page.fill('[data-testid="card-number"]', card);
await this.page.click('[data-testid="pay-btn"]');
}
}// WHAT INTERVIEWERS LOOK FOR
The 'one file to update when locators change' benefit. No assertions in POs. A before/after code comparison to illustrate the value.
// COMMON PITFALL
Putting assertions inside Page Object methods — this makes them untestable with different expected states and ties them to a single test scenario.
// Related questions