|

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.