POM with Selenium & Playwright
POM works equally well with both Selenium and Playwright — the pattern is framework-agnostic. Playwright's built-in TypeScript support and fixture system make POM implementation even cleaner than Selenium. This topic shows production-ready POM implementation for both frameworks and demonstrates why page objects are the foundation of every enterprise automation framework.
POM with Playwright (TypeScript)
// ── pages/LoginPage.ts ────────────────────────────────────────
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
// ✅ Locators as readonly properties (defined once, used everywhere)
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email address');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign In' });
this.errorMessage = page.locator('[data-testid="error-message"]');
}
async navigate() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
// ── pages/DashboardPage.ts ────────────────────────────────────
import { Page, Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly welcomeMessage: Locator;
readonly logoutButton: Locator;
constructor(private page: Page) {
this.welcomeMessage = page.getByTestId('welcome-message');
this.logoutButton = page.getByRole('button', { name: 'Log Out' });
}
async assertLoaded() {
await expect(this.page).toHaveURL(/.*dashboard/);
await expect(this.welcomeMessage).toBeVisible();
}
async getWelcomeText(): Promise<string> {
return this.welcomeMessage.textContent() as Promise<string>;
}
}
// ── tests/login.spec.ts ────────────────────────────────────────
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test.describe('Login Suite', () => {
test('valid credentials — redirect to dashboard', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboard = new DashboardPage(page);
await loginPage.navigate();
await loginPage.login('alice@test.com', 'Test@1234');
await dashboard.assertLoaded();
const welcomeText = await dashboard.getWelcomeText();
expect(welcomeText).toContain('Alice');
});
test('invalid password — show error', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('alice@test.com', 'wrongpassword');
await loginPage.expectError('Invalid email or password');
});
});Common Mistakes
- Defining locators in test files — locators belong in page objects; if a locator changes, you should update exactly one file
- Duplicating navigation logic — include navigate() in each page class; test files shouldn't hardcode URLs
- Pages calling other pages — LoginPage should not create and return a DashboardPage; let tests handle page transitions for clarity
- Over-engineering — small projects (3-5 pages) don't need complex POM; start simple and grow the pattern as the suite grows
Tip
Tip
Practice POM with Selenium Playwright in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Testing pyramid: many unit tests, fewer integration, fewest E2E
Practice Task
Note
Practice Task — (1) Write a working example of POM with Selenium Playwright from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with POM with Selenium Playwright is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready software testing code.
Key Takeaways
- POM works equally well with both Selenium and Playwright — the pattern is framework-agnostic.
- Defining locators in test files — locators belong in page objects; if a locator changes, you should update exactly one file
- Duplicating navigation logic — include navigate() in each page class; test files shouldn't hardcode URLs
- Pages calling other pages — LoginPage should not create and return a DashboardPage; let tests handle page transitions for clarity