Implementing Page Object Model in Playwright With TypeScript: 3 Complete Worked Examples
Page Object Model is one of the cleanest and most scalable ways to organize test automation code. Here are 3 complete worked examples in Playwright TypeScript — from page class to test file — that you can copy into your project today.
🎭 Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Contents
Example 1: Login Page
// src/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorAlert: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Sign in' });
this.errorAlert = 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.loginButton.click();
}
async expectError(message: string) {
await expect(this.errorAlert).toContainText(message);
}
async expectRedirectToDashboard() {
await expect(this.page).toHaveURL(/dashboard/);
}
}
Test File
// src/tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Login Page', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('should login with valid credentials', async () => {
await loginPage.login('admin@test.com', 'password123');
await loginPage.expectRedirectToDashboard();
});
test('should show error for invalid password', async () => {
await loginPage.login('admin@test.com', 'wrong');
await loginPage.expectError('Invalid credentials');
});
test('should show error for empty email', async () => {
await loginPage.login('', 'password123');
await loginPage.expectError('Email is required');
});
});
🚀 Level Up Your Playwright
From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.
Example 2: Product Listing Page
// src/pages/ProductListPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class ProductListPage {
readonly page: Page;
readonly searchInput: Locator;
readonly productCards: Locator;
readonly sortDropdown: Locator;
readonly cartBadge: Locator;
constructor(page: Page) {
this.page = page;
this.searchInput = page.getByPlaceholder('Search products');
this.productCards = page.getByTestId('product-card');
this.sortDropdown = page.getByLabel('Sort by');
this.cartBadge = page.getByTestId('cart-count');
}
async search(query: string) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
}
async getProductCount(): Promise<number> {
return await this.productCards.count();
}
async addToCart(productName: string) {
const card = this.productCards.filter({ hasText: productName });
await card.getByRole('button', { name: 'Add to cart' }).click();
}
async sortBy(option: string) {
await this.sortDropdown.selectOption({ label: option });
}
async expectProductVisible(name: string) {
await expect(this.productCards.filter({ hasText: name })).toBeVisible();
}
}
Example 3: Checkout Page
// src/pages/CheckoutPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class CheckoutPage {
readonly page: Page;
readonly addressInput: Locator;
readonly cityInput: Locator;
readonly zipInput: Locator;
readonly placeOrderButton: Locator;
readonly orderConfirmation: Locator;
readonly orderTotal: Locator;
constructor(page: Page) {
this.page = page;
this.addressInput = page.getByLabel('Street address');
this.cityInput = page.getByLabel('City');
this.zipInput = page.getByLabel('ZIP code');
this.placeOrderButton = page.getByRole('button', { name: 'Place order' });
this.orderConfirmation = page.getByTestId('order-confirmation');
this.orderTotal = page.getByTestId('order-total');
}
async fillShippingAddress(address: string, city: string, zip: string) {
await this.addressInput.fill(address);
await this.cityInput.fill(city);
await this.zipInput.fill(zip);
}
async placeOrder() {
await this.placeOrderButton.click();
}
async expectOrderConfirmed() {
await expect(this.orderConfirmation).toContainText('Order confirmed');
}
async expectTotal(amount: string) {
await expect(this.orderTotal).toContainText(amount);
}
}
Common POM Anti-Patterns to Avoid
- Too many layers: Page -> BasePage -> AbstractPage -> PageInterface — keep it flat, one level deep
- Business logic in pages: Page Objects should only interact with the UI, not contain test assertions or business rules
- God Page Objects: A 500-line LoginPage class — split into components (LoginForm, Navbar, Footer)
- Returning page objects from methods: In Playwright, return void and let the test navigate — page objects are not fluent builders
🎓 Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
