EU-Utility/skills/playwright/testing.md

3.5 KiB

Testing Patterns

Test Structure

import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/products');
  });

  test('completes purchase with valid card', async ({ page }) => {
    await page.getByTestId('product-card').first().click();
    await page.getByRole('button', { name: 'Add to Cart' }).click();
    await page.getByRole('link', { name: 'Checkout' }).click();
    
    await expect(page.getByRole('heading', { name: 'Order Summary' })).toBeVisible();
  });
});

Page Object Model

// pages/checkout.page.ts
export class CheckoutPage {
  constructor(private page: Page) {}

  readonly cartItems = this.page.getByTestId('cart-item');
  readonly checkoutButton = this.page.getByRole('button', { name: 'Checkout' });
  readonly totalPrice = this.page.getByTestId('total-price');

  async removeItem(name: string) {
    await this.cartItems
      .filter({ hasText: name })
      .getByRole('button', { name: 'Remove' })
      .click();
  }

  async expectTotal(amount: string) {
    await expect(this.totalPrice).toHaveText(amount);
  }
}

// tests/checkout.spec.ts
test('removes item from cart', async ({ page }) => {
  const checkout = new CheckoutPage(page);
  await checkout.removeItem('Product A');
  await checkout.expectTotal('$0.00');
});

Fixtures

// fixtures.ts
import { test as base } from '@playwright/test';
import { CheckoutPage } from './pages/checkout.page';

type Fixtures = {
  checkoutPage: CheckoutPage;
};

export const test = base.extend<Fixtures>({
  checkoutPage: async ({ page }, use) => {
    await page.goto('/checkout');
    await use(new CheckoutPage(page));
  },
});

API Mocking

test('shows error on API failure', async ({ page }) => {
  await page.route('**/api/checkout', route => {
    route.fulfill({
      status: 500,
      body: JSON.stringify({ error: 'Payment failed' }),
    });
  });

  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Pay' }).click();
  await expect(page.getByText('Payment failed')).toBeVisible();
});

Visual Regression

test('matches snapshot', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page).toHaveScreenshot('dashboard.png', {
    maxDiffPixels: 100,
  });
});

// Component snapshot
await expect(page.getByTestId('header')).toHaveScreenshot();

Parallelization

// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 4 : undefined,
  fullyParallel: true,
});

// Per-file control
test.describe.configure({ mode: 'parallel' });
test.describe.configure({ mode: 'serial' });  // dependent tests

Authentication State

// Save auth state
await page.context().storageState({ path: 'auth.json' });

// Reuse across tests
test.use({ storageState: 'auth.json' });

Assertions

// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeAttached();

// Content
await expect(locator).toHaveText('Expected');
await expect(locator).toContainText('partial');
await expect(locator).toHaveValue('input value');

// State
await expect(locator).toBeEnabled();
await expect(locator).toBeChecked();
await expect(locator).toHaveAttribute('href', '/path');

// Polling (for async state)
await expect.poll(async () => {
  return await page.evaluate(() => window.dataLoaded);
}).toBe(true);