Claude
Skills
Sign in
Back

playwright-page-object-model

Included with Lifetime
$97 forever

Use when creating page objects or refactoring Playwright tests for better maintainability with Page Object Model patterns.

General

What this skill does


# Playwright Page Object Model

Master the Page Object Model (POM) pattern to create maintainable, reusable,
and scalable test automation code. This skill covers modern Playwright
patterns including component-based architecture, locator strategies, and
app actions.

## Core POM Principles

### Single Responsibility

Each page object should represent one page or component with a single,
well-defined responsibility.

### Encapsulation

Hide implementation details and expose only meaningful actions and
assertions.

### Reusability

Create reusable components that can be composed into larger page objects.

### Maintainability

When UI changes, update page objects in one place rather than across
multiple tests.

## Basic Page Object Pattern

### Simple Page Object

```typescript
// pages/login-page.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
    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.loginButton.click();
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent();
  }
}
```

### Using Page Object in Tests

```typescript
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';

test.describe('Login', () => {
  test('should login successfully', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('[email protected]', 'password123');

    await expect(page).toHaveURL('/dashboard');
  });

  test('should show error on invalid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('[email protected]', 'wrongpassword');

    const error = await loginPage.getErrorMessage();
    expect(error).toContain('Invalid credentials');
  });
});
```

## Locator Strategies

### Recommended Locator Priority

1. User-visible locators (getByRole, getByText, getByLabel)
2. Test IDs (getByTestId)
3. CSS/XPath (only as last resort)

### User-Visible Locators

```typescript
export class HomePage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // By role (ARIA role)
  get searchButton() {
    return this.page.getByRole('button', { name: 'Search' });
  }

  // By label (form inputs)
  get searchInput() {
    return this.page.getByLabel('Search products');
  }

  // By text
  get welcomeMessage() {
    return this.page.getByText('Welcome back');
  }

  // By placeholder
  get emailInput() {
    return this.page.getByPlaceholder('Enter your email');
  }

  // By alt text (images)
  get logo() {
    return this.page.getByAltText('Company Logo');
  }

  // By title
  get helpIcon() {
    return this.page.getByTitle('Help');
  }
}
```

### Test ID Locators

```typescript
// Component with test IDs
// <button data-testid="submit-button">Submit</button>

export class FormPage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  get submitButton() {
    return this.page.getByTestId('submit-button');
  }

  get formContainer() {
    return this.page.getByTestId('form-container');
  }
}
```

### Locator Chaining

```typescript
export class ProductPage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // Chain locators for specificity
  get priceInCart() {
    return this.page
      .getByTestId('shopping-cart')
      .getByRole('cell', { name: 'Price' });
  }

  // Filter locators
  getProductByName(name: string) {
    return this.page
      .getByRole('listitem')
      .filter({ hasText: name });
  }

  // Nth element
  get firstProduct() {
    return this.page.getByRole('article').nth(0);
  }
}
```

## Component-Based Architecture

### Reusable Component Objects

```typescript
// components/navigation.ts
export class Navigation {
  readonly page: Page;
  readonly homeLink: Locator;
  readonly productsLink: Locator;
  readonly cartLink: Locator;
  readonly profileMenu: Locator;

  constructor(page: Page) {
    this.page = page;
    this.homeLink = page.getByRole('link', { name: 'Home' });
    this.productsLink = page.getByRole('link', { name: 'Products' });
    this.cartLink = page.getByRole('link', { name: 'Cart' });
    this.profileMenu = page.getByRole('button', { name: 'Profile' });
  }

  async navigateToHome() {
    await this.homeLink.click();
  }

  async navigateToProducts() {
    await this.productsLink.click();
  }

  async navigateToCart() {
    await this.cartLink.click();
  }

  async openProfileMenu() {
    await this.profileMenu.click();
  }
}
```

### Composing Page Objects with Components

```typescript
// pages/base-page.ts
import { Page } from '@playwright/test';
import { Navigation } from '../components/navigation';
import { Footer } from '../components/footer';

export class BasePage {
  readonly page: Page;
  readonly navigation: Navigation;
  readonly footer: Footer;

  constructor(page: Page) {
    this.page = page;
    this.navigation = new Navigation(page);
    this.footer = new Footer(page);
  }
}
```

```typescript
// pages/product-page.ts
import { BasePage } from './base-page';
import { Page } from '@playwright/test';

export class ProductPage extends BasePage {
  readonly addToCartButton: Locator;
  readonly productTitle: Locator;
  readonly productPrice: Locator;

  constructor(page: Page) {
    super(page);
    this.addToCartButton = page.getByRole('button', { name: 'Add to Cart' });
    this.productTitle = page.getByRole('heading', { level: 1 });
    this.productPrice = page.getByTestId('product-price');
  }

  async goto(productId: string) {
    await this.page.goto(`/products/${productId}`);
  }

  async addToCart() {
    await this.addToCartButton.click();
    // Wait for cart update
    await this.page.waitForResponse(
      (response) => response.url().includes('/api/cart')
    );
  }

  async getProductTitle() {
    return await this.productTitle.textContent();
  }

  async getProductPrice() {
    const text = await this.productPrice.textContent();
    return parseFloat(text?.replace('$', '') || '0');
  }
}
```

### Modal and Dialog Components

```typescript
// components/modal.ts
export class Modal {
  readonly page: Page;
  readonly container: Locator;
  readonly closeButton: Locator;
  readonly title: Locator;

  constructor(page: Page) {
    this.page = page;
    this.container = page.getByRole('dialog');
    this.closeButton = this.container.getByRole('button', { name: 'Close' });
    this.title = this.container.getByRole('heading');
  }

  async isVisible() {
    return await this.container.isVisible();
  }

  async getTitle() {
    return await this.title.textContent();
  }

  async close() {
    await this.closeButton.click();
    await this.container.waitFor({ state: 'hidden' });
  }
}
```

```typescript
// components/confirmation-modal.ts
import { Modal } from './modal';
import { Page } from '@playwright/test';

export class ConfirmationModal extends Modal {
  readonly confirmButton: Locator;
  readonly cancelButton: Locator;
  readonly message: Locator;

  constructor(page: Page) {
    super(page);
    this.confirmButton = this.container.getByRole('button', {
      name: 'Confirm',
    });
    this.cancelButton = this.container.getByRole('button', {
      name: 'Cancel',
    });
    this.message = this.container.getByTestId('modal-message');
  }

  async confirm() {
    await this.confirmButton.click(

Related in General