Every E2E suite starts out small and organized. Over time, without discipline, it becomes a web of duplicated selectors where changing one button breaks thirty tests. The Page Object Model (POM) is the pattern that prevents that chaos.
The core idea
Each page (or component) of the application becomes a class that encapsulates how to interact with it. The test describes what to do; the Page Object knows where the elements are.
class LoginPage {
constructor(page) { this.page = page; }
async open() { await this.page.goto('/login'); }
async login(u, p) {
await this.page.getByLabel('Email').fill(u);
await this.page.getByLabel('Password').fill(p);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
}
The test stays clean and speaks the language of the business:
const login = new LoginPage(page);
await login.open();
await login.login('qa@example.com', 's3cr3t');
await expect(page.getByText('Welcome')).toBeVisible();
Rules that keep the POM healthy
- No asserts inside the Page Object — it acts on the page; the test does the validating.
- One selector, one place — did the HTML change? Fix it in a single class.
- Methods by intent (
login()), not by mechanics (clickButton()). - Reusable components (menu, modal) become their own objects, composed into pages.
When a redesign breaks the whole suite in a few files rather than in all of them, the POM is doing its job.
Conclusion
POM isn't bureaucracy: it's what makes automation scale along with the product. Combined with accessible locators and fixtures, it keeps the suite readable and cheap to maintain even after hundreds of tests.