Back to Sanity

Test Suite Structure

.agents/skills/playwright-best-practices/core/test-suite-structure.md

5.20.09.2 KB
Original Source

Test Suite Structure

Table of Contents

  1. Configuration
  2. E2E Tests
  3. Component Tests
  4. API Tests
  5. Visual Regression Tests
  6. Directory Structure
  7. Tagging & Filtering

Project Setup

bash
npm init playwright@latest

Configuration

Essential Configuration

typescript
// playwright.config.ts
import {defineConfig, devices} from '@playwright/test'

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [['html'], ['list']],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {name: 'setup', testMatch: /.*\.setup\.ts/},
    {
      name: 'chromium',
      use: {...devices['Desktop Chrome']},
      dependencies: ['setup'],
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

E2E Tests

Full user journey tests through the browser.

Structure

typescript
// tests/e2e/checkout.spec.ts
import {test, expect} from '@playwright/test'

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

  test('complete purchase as guest', async ({page}) => {
    // Add to cart
    await page.getByRole('button', {name: 'Add to Cart'}).first().click()
    await expect(page.getByTestId('cart-count')).toHaveText('1')

    // Go to checkout
    await page.getByRole('link', {name: 'Cart'}).click()
    await page.getByRole('button', {name: 'Checkout'}).click()

    // Fill shipping
    await page.getByLabel('Email').fill('[email protected]')
    await page.getByLabel('Address').fill('123 Test St')
    await page.getByRole('button', {name: 'Continue'}).click()

    // Payment
    await page.getByLabel('Card Number').fill('4242424242424242')
    await page.getByRole('button', {name: 'Pay Now'}).click()

    // Confirmation
    await expect(page.getByRole('heading')).toHaveText('Order Confirmed')
  })

  test('apply discount code', async ({page}) => {
    await page.getByRole('button', {name: 'Add to Cart'}).first().click()
    await page.getByRole('link', {name: 'Cart'}).click()

    await page.getByLabel('Discount Code').fill('SAVE10')
    await page.getByRole('button', {name: 'Apply'}).click()

    await expect(page.getByText('10% discount applied')).toBeVisible()
  })
})

Best Practices

  • Test critical user journeys
  • Keep tests independent
  • Use realistic data
  • Clean up test data in teardown

Component Tests

Test individual components in isolation using Playwright Component Testing.

bash
npm init playwright@latest -- --ct

For comprehensive component testing patterns including mounting, props, events, slots, mocking, and framework-specific examples (React, Vue, Svelte), see component-testing.md.

API Tests

Test backend APIs without browser.

API Mocking Patterns

For E2E tests that need to mock API responses:

typescript
// Mock single endpoint
test('displays mocked users', async ({page}) => {
  await page.route('**/api/users', (route) =>
    route.fulfill({
      status: 200,
      json: [{id: 1, name: 'Test User'}],
    }),
  )

  await page.goto('/users')
  await expect(page.getByText('Test User')).toBeVisible()
})

// Mock with different responses
test('handles API errors', async ({page}) => {
  await page.route('**/api/users', (route) =>
    route.fulfill({
      status: 500,
      json: {error: 'Server error'},
    }),
  )

  await page.goto('/users')
  await expect(page.getByText('Server error')).toBeVisible()
})

// Conditional mocking
test('mocks based on request', async ({page}) => {
  await page.route('**/api/users', (route, request) => {
    if (request.method() === 'GET') {
      route.fulfill({json: [{id: 1, name: 'User'}]})
    } else {
      route.continue()
    }
  })
})

// Mock with delay (simulate slow network)
test('handles slow API', async ({page}) => {
  await page.route('**/api/data', (route) =>
    route.fulfill({
      json: {data: 'test'},
      delay: 2000, // 2 second delay
    }),
  )

  await page.goto('/dashboard')
  await expect(page.getByText('Loading...')).toBeVisible()
  await expect(page.getByText('test')).toBeVisible()
})

For advanced patterns (GraphQL mocking, HAR recording, request modification, network throttling), see network-advanced.md.

Visual Regression Tests

Compare screenshots to detect visual changes.

Basic Visual Test

typescript
// tests/visual/homepage.spec.ts
import {test, expect} from '@playwright/test'

test('homepage visual', async ({page}) => {
  await page.goto('/')
  await expect(page).toHaveScreenshot('homepage.png')
})

test('component visual', async ({page}) => {
  await page.goto('/components')

  const button = page.getByRole('button', {name: 'Primary'})
  await expect(button).toHaveScreenshot('primary-button.png')
})

Visual Test Options

typescript
test('dashboard visual', async ({page}) => {
  await page.goto('/dashboard')

  await expect(page).toHaveScreenshot('dashboard.png', {
    fullPage: true, // Capture entire scrollable page
    maxDiffPixels: 100, // Allow up to 100 different pixels
    maxDiffPixelRatio: 0.01, // Or 1% difference
    threshold: 0.2, // Pixel comparison threshold
    animations: 'disabled', // Disable animations
    mask: [page.getByTestId('date')], // Mask dynamic content
  })
})

Handling Dynamic Content

typescript
test('page with dynamic content', async ({page}) => {
  await page.goto('/profile')

  // Mask elements that change
  await expect(page).toHaveScreenshot('profile.png', {
    mask: [page.getByTestId('timestamp'), page.getByTestId('avatar'), page.getByRole('img')],
  })
})

// Or hide elements via CSS
test('page hiding dynamic elements', async ({page}) => {
  await page.goto('/profile')

  await page.addStyleTag({
    content: `
      .dynamic-content { visibility: hidden !important; }
      [data-testid="ad-banner"] { display: none !important; }
    `,
  })

  await expect(page).toHaveScreenshot('profile-stable.png')
})

Visual Test Configuration

typescript
// playwright.config.ts
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 50,
      animations: 'disabled',
    },
  },
  projects: [
    {
      name: 'visual-chrome',
      use: {
        ...devices['Desktop Chrome'],
        viewport: {width: 1280, height: 720},
      },
      testMatch: /.*visual.*\.spec\.ts/,
    },
  ],
})

Update Snapshots

bash
# Update all snapshots
npx playwright test --update-snapshots

# Update specific test
npx playwright test homepage.spec.ts --update-snapshots

Directory Structure

tests/
├── e2e/                    # End-to-end tests
│   ├── auth.spec.ts
│   ├── checkout.spec.ts
│   └── dashboard.spec.ts
├── component/              # Component tests
│   ├── Button.spec.tsx
│   └── Modal.spec.tsx
├── api/                    # API tests
│   ├── users.spec.ts
│   └── products.spec.ts
├── visual/                 # Visual regression tests
│   └── homepage.spec.ts
├── fixtures/               # Custom fixtures
│   ├── auth.fixture.ts
│   └── api.fixture.ts
└── pages/                  # Page objects
    ├── login.page.ts
    └── dashboard.page.ts

Anti-Patterns to Avoid

Anti-PatternProblemSolution
Long test filesHard to maintain, slow to navigateSplit by feature, use POM
Tests depend on execution orderFlaky, hard to debugKeep tests independent
Testing multiple features in one testHard to debug failuresOne feature per test

Tagging & Filtering

Using Tags

typescript
test('user login @smoke @auth', async ({page}) => {
  // ...
})

test('checkout flow @e2e @critical', async ({page}) => {
  // ...
})

test.describe('API tests @api', () => {
  test('create user', async ({request}) => {
    // ...
  })
})

Running Tagged Tests

bash
# Run smoke tests
npx playwright test --grep @smoke

# Run all except slow tests
npx playwright test --grep-invert @slow

# Combine tags
npx playwright test --grep "@smoke|@critical"

For project-based filtering and advanced project configuration, see projects-dependencies.md.