Page Object Model
Basic Page Object
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
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 getErrorMessage() {
return this.errorMessage.textContent();
}
}
Using Page Objects in Tests
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('successful login redirects to dashboard', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'password123');
await expect(page).toHaveURL(/dashboard/);
});
test('invalid credentials show error', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'wrongpassword');
await expect(loginPage.errorMessage).toBeVisible();
});
Custom Fixtures
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
type Fixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: Page;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
authenticatedPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'password123');
await page.waitForURL(/dashboard/);
await use(page);
},
});
export { expect } from '@playwright/test';
Using Fixtures
// tests/dashboard.spec.ts
import { test, expect } from '../fixtures';
test('shows user profile', async ({ authenticatedPage, dashboardPage }) => {
await expect(dashboardPage.userProfile).toBeVisible();
});
Component Page Objects
// components/NavBar.ts
export class NavBar {
constructor(private page: Page) {}
readonly homeLink = () => this.page.getByRole('link', { name: 'Home' });
readonly profileLink = () => this.page.getByRole('link', { name: 'Profile' });
readonly logoutButton = () => this.page.getByRole('button', { name: 'Logout' });
async logout() {
await this.logoutButton().click();
}
}
// pages/DashboardPage.ts
export class DashboardPage {
readonly navBar: NavBar;
constructor(private page: Page) {
this.navBar = new NavBar(page);
}
}
Quick Reference
| Pattern |
Purpose |
| Page Object |
Encapsulate page interactions |
| Fixture |
Share setup across tests |
| Component PO |
Reusable UI components |
| Locator methods |
Lazy evaluation |
| Best Practice |
Reason |
| Methods for actions |
Readable tests |
| Locators as getters |
Lazy evaluation |
| No assertions in PO |
Flexibility |
| Fixtures for setup |
DRY, maintainable |