BMAD-METHOD/docs/how-to/customization/integrate-playwright-utils.md

20 KiB

title description
Integrate Playwright Utils with TEA Add production-ready fixtures and utilities to your TEA-generated tests

Integrate Playwright Utils with TEA

Integrate @seontechnologies/playwright-utils with TEA to get production-ready fixtures, utilities, and patterns in your test suite.

What is Playwright Utils?

A production-ready utility library that provides:

  • Typed API request helper
  • Authentication session management
  • Network recording and replay (HAR)
  • Network request interception
  • Async polling (recurse)
  • Structured logging
  • File validation (CSV, PDF, XLSX, ZIP)
  • Burn-in testing utilities
  • Network error monitoring

Repository: https://github.com/seontechnologies/playwright-utils

npm Package: @seontechnologies/playwright-utils

When to Use This

  • You want production-ready fixtures (not DIY)
  • Your team benefits from standardized patterns
  • You need utilities like API testing, auth handling, network mocking
  • You want TEA to generate tests using these utilities
  • You're building reusable test infrastructure

Don't use if:

  • You're just learning testing (keep it simple first)
  • You have your own fixture library
  • You don't need the utilities

Prerequisites

  • BMad Method installed
  • TEA agent available
  • Test framework setup complete (Playwright)
  • Node.js v18 or later

Note: Playwright Utils is for Playwright only (not Cypress).

Installation

Step 1: Install Package

npm install -D @seontechnologies/playwright-utils

Step 2: Enable in TEA Config

Edit _bmad/bmm/config.yaml:

tea_use_playwright_utils: true

Note: If you enabled this during installation (npx bmad-method@alpha install), it's already set.

Step 3: Verify Installation

# Check package installed
npm list @seontechnologies/playwright-utils

# Check TEA config
grep tea_use_playwright_utils _bmad/bmm/config.yaml

Should show:

@seontechnologies/playwright-utils@2.x.x
tea_use_playwright_utils: true

What Changes When Enabled

*framework Workflow

Vanilla Playwright:

// Basic Playwright fixtures only
import { test, expect } from '@playwright/test';

test('api test', async ({ request }) => {
  const response = await request.get('/api/users');
  const users = await response.json();
  expect(response.status()).toBe(200);
});

With Playwright Utils (Combined Fixtures):

// All utilities available via single import
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';

test('api test', async ({ apiRequest, authToken, log }) => {
  const { status, body } = await apiRequest({
    method: 'GET',
    path: '/api/users',
    headers: { Authorization: `Bearer ${authToken}` }
  });

  log.info('Fetched users', body);
  expect(status).toBe(200);
});

With Playwright Utils (Selective Merge):

import { mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as logFixture } from '@seontechnologies/playwright-utils/log/fixtures';

export const test = mergeTests(apiRequestFixture, logFixture);
export { expect } from '@playwright/test';

test('api test', async ({ apiRequest, log }) => {
  log.info('Fetching users');
  const { status, body } = await apiRequest({
    method: 'GET',
    path: '/api/users'
  });
  expect(status).toBe(200);
});

*atdd and *automate Workflows

Without Playwright Utils:

// Manual API calls
test('should fetch profile', async ({ page, request }) => {
  const response = await request.get('/api/profile');
  const profile = await response.json();
  // Manual parsing and validation
});

With Playwright Utils:

import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';

test('should fetch profile', async ({ apiRequest }) => {
  const { status, body } = await apiRequest({
    method: 'GET',
    path: '/api/profile'  // 'path' not 'url'
  }).validateSchema(ProfileSchema);  // Chained validation

  expect(status).toBe(200);
  // body is type-safe: { id: string, name: string, email: string }
});

*test-review Workflow

Without Playwright Utils: Reviews against generic Playwright patterns

With Playwright Utils: Reviews against playwright-utils best practices:

  • Fixture composition patterns
  • Utility usage (apiRequest, authSession, etc.)
  • Network-first patterns
  • Structured logging

*ci Workflow

Without Playwright Utils: Basic CI configuration

With Playwright Utils: Enhanced CI with:

  • Burn-in utility for smart test selection
  • Selective testing based on git diff
  • Test prioritization

Available Utilities

api-request

Typed HTTP client with schema validation.

Usage:

import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { expect } from '@playwright/test';
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email()
});

test('should create user', async ({ apiRequest }) => {
  const { status, body } = await apiRequest({
    method: 'POST',
    path: '/api/users',  // Note: 'path' not 'url'
    body: { name: 'Test User', email: 'test@example.com' }  // Note: 'body' not 'data'
  }).validateSchema(UserSchema);  // Note: chained method

  expect(status).toBe(201);
  expect(body.id).toBeDefined();
  expect(body.email).toBe('test@example.com');
});

Benefits:

  • Returns { status, body } structure
  • Schema validation with .validateSchema() chained method
  • Automatic retry for 5xx errors
  • Type-safe response body

auth-session

Authentication session management with token persistence.

Usage:

import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { expect } from '@playwright/test';

test('should access protected route', async ({ page, authToken }) => {
  // authToken automatically fetched and persisted
  // No manual login needed - handled by fixture

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

  // Token is reused across tests (persisted to disk)
});

Configuration required (see auth-session docs for provider setup):

// global-setup.ts
import { authStorageInit, setAuthProvider, authGlobalInit } from '@seontechnologies/playwright-utils/auth-session';

async function globalSetup() {
  authStorageInit();
  setAuthProvider(myCustomProvider);  // Define your auth mechanism
  await authGlobalInit();  // Fetch token once
}

Benefits:

  • Token fetched once, reused across all tests
  • Persisted to disk (faster subsequent runs)
  • Multi-user support via authOptions.userIdentifier
  • Automatic token renewal if expired

network-recorder

Record and replay network traffic (HAR) for offline testing.

Usage:

import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures';

// Record mode: Set environment variable
process.env.PW_NET_MODE = 'record';

test('should work with recorded traffic', async ({ page, context, networkRecorder }) => {
  // Setup recorder (records or replays based on PW_NET_MODE)
  await networkRecorder.setup(context);

  // Your normal test code
  await page.goto('/dashboard');
  await page.click('#add-item');

  // First run (record): Saves traffic to HAR file
  // Subsequent runs (playback): Uses HAR file, no backend needed
});

Switch modes:

# Record traffic
PW_NET_MODE=record npx playwright test

# Playback traffic (offline)
PW_NET_MODE=playback npx playwright test

Benefits:

  • Offline testing (no backend needed)
  • Deterministic responses (same every time)
  • Faster execution (no network latency)
  • Stateful mocking (CRUD operations work)

intercept-network-call

Spy or stub network requests with automatic JSON parsing.

Usage:

import { test } from '@seontechnologies/playwright-utils/fixtures';

test('should handle API errors', async ({ page, interceptNetworkCall }) => {
  // Stub API to return error (set up BEFORE navigation)
  const profileCall = interceptNetworkCall({
    method: 'GET',
    url: '**/api/profile',
    fulfillResponse: {
      status: 500,
      body: { error: 'Server error' }
    }
  });

  await page.goto('/profile');

  // Wait for the intercepted response
  const { status, responseJson } = await profileCall;

  expect(status).toBe(500);
  expect(responseJson.error).toBe('Server error');
  await expect(page.getByText('Server error occurred')).toBeVisible();
});

Benefits:

  • Automatic JSON parsing (responseJson ready to use)
  • Spy mode (observe real traffic) or stub mode (mock responses)
  • Glob pattern URL matching
  • Returns promise with { status, responseJson, requestJson }

recurse

Async polling for eventual consistency (Cypress-style).

Usage:

import { test } from '@seontechnologies/playwright-utils/fixtures';

test('should wait for async job completion', async ({ apiRequest, recurse }) => {
  // Start async job
  const { body: job } = await apiRequest({
    method: 'POST',
    path: '/api/jobs'
  });

  // Poll until complete (smart waiting)
  const completed = await recurse(
    () => apiRequest({ method: 'GET', path: `/api/jobs/${job.id}` }),
    (result) => result.body.status === 'completed',
    {
      timeout: 30000,
      interval: 2000,
      log: 'Waiting for job to complete'
    }
  });

  expect(completed.body.status).toBe('completed');
});

Benefits:

  • Smart polling with configurable interval
  • Handles async jobs, background tasks
  • Optional logging for debugging
  • Better than hard waits or manual polling loops

log

Structured logging that integrates with Playwright reports.

Usage:

import { log } from '@seontechnologies/playwright-utils';
import { test, expect } from '@playwright/test';

test('should login', async ({ page }) => {
  await log.info('Starting login test');

  await page.goto('/login');
  await log.step('Navigated to login page');  // Shows in Playwright UI

  await page.getByLabel('Email').fill('test@example.com');
  await log.debug('Filled email field');

  await log.success('Login completed');
  // Logs appear in test output and Playwright reports
});

Benefits:

  • Direct import (no fixture needed for basic usage)
  • Structured logs in test reports
  • .step() shows in Playwright UI
  • Supports object logging with .debug()
  • Trace test execution

file-utils

Read and validate CSV, PDF, XLSX, ZIP files.

Usage:

import { handleDownload, readCSV } from '@seontechnologies/playwright-utils/file-utils';
import { expect } from '@playwright/test';
import path from 'node:path';

const DOWNLOAD_DIR = path.join(__dirname, '../downloads');

test('should export valid CSV', async ({ page }) => {
  // Handle download and get file path
  const downloadPath = await handleDownload({
    page,
    downloadDir: DOWNLOAD_DIR,
    trigger: () => page.click('button:has-text("Export")')
  });

  // Read and parse CSV
  const csvResult = await readCSV({ filePath: downloadPath });
  const { data, headers } = csvResult.content;

  // Validate structure
  expect(headers).toEqual(['Name', 'Email', 'Status']);
  expect(data.length).toBeGreaterThan(0);
  expect(data[0]).toMatchObject({
    Name: expect.any(String),
    Email: expect.any(String),
    Status: expect.any(String)
  });
});

Benefits:

  • Handles downloads automatically
  • Auto-parses CSV, XLSX, PDF, ZIP
  • Type-safe access to parsed data
  • Returns structured { headers, data }

burn-in

Smart test selection with git diff analysis for CI optimization.

Usage:

// scripts/burn-in-changed.ts
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in';

async function main() {
  await runBurnIn({
    configPath: 'playwright.burn-in.config.ts',
    baseBranch: 'main'
  });
}

main().catch(console.error);

Config:

// playwright.burn-in.config.ts
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in';

const config: BurnInConfig = {
  skipBurnInPatterns: [
    '**/config/**',
    '**/*.md',
    '**/*types*'
  ],
  burnInTestPercentage: 0.3,
  burnIn: {
    repeatEach: 3,
    retries: 1
  }
};

export default config;

Package script:

{
  "scripts": {
    "test:burn-in": "tsx scripts/burn-in-changed.ts"
  }
}

Benefits:

  • Smart filtering (skip config, types, docs changes)
  • Volume control (run percentage of affected tests)
  • Git diff-based test selection
  • Faster CI feedback

network-error-monitor

Automatically detect HTTP 4xx/5xx errors during tests.

Usage:

import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';

// That's it! Network monitoring is automatically enabled
test('should not have API errors', async ({ page }) => {
  await page.goto('/dashboard');
  await page.click('button');

  // Test fails automatically if any HTTP 4xx/5xx errors occur
  // Error message shows: "Network errors detected: 2 request(s) failed"
  //   GET 500 https://api.example.com/users
  //   POST 503 https://api.example.com/metrics
});

Opt-out for validation tests:

// When testing error scenarios, opt-out with annotation
test('should show error message on 404',
  { annotation: [{ type: 'skipNetworkMonitoring' }] },  // Array format
  async ({ page }) => {
    await page.goto('/invalid-page');  // Will 404
    await expect(page.getByText('Page not found')).toBeVisible();
    // Test won't fail on 404 because of annotation
  }
);

// Or opt-out entire describe block
test.describe('error handling',
  { annotation: [{ type: 'skipNetworkMonitoring' }] },
  () => {
    test('handles 404', async ({ page }) => {
      // Monitoring disabled for all tests in block
    });
  }
);

Benefits:

  • Auto-enabled (zero setup)
  • Catches silent backend failures
  • Opt-out with annotations
  • Structured error reporting

Fixture Composition

Combine utilities using mergeTests:

Option 1: Use Combined Fixtures (Simplest)

// Import all utilities at once
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { log } from '@seontechnologies/playwright-utils';
import { expect } from '@playwright/test';

test('full test', async ({ apiRequest, authToken, interceptNetworkCall }) => {
  await log.info('Starting test');  // log is direct import

  const { status, body } = await apiRequest({
    method: 'GET',
    path: '/api/data',
    headers: { Authorization: `Bearer ${authToken}` }
  });

  await log.info('Data fetched', body);
  expect(status).toBe(200);
});

Note: log is imported directly (not a fixture). authToken requires auth-session provider setup.

Option 2: Merge Individual Fixtures (Selective)

import { test as base } from '@playwright/test';
import { mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
import { log } from '@seontechnologies/playwright-utils';

// Merge only the fixtures you need
export const test = mergeTests(
  apiRequestFixture,
  recurseFixture
);

export { expect } from '@playwright/test';

// Use merged utilities in tests
test('selective test', async ({ apiRequest, recurse }) => {
  await log.info('Starting test');  // log is direct import, not fixture

  const { status, body } = await apiRequest({
    method: 'GET',
    path: '/api/data'
  });

  await log.info('Data fetched', body);
  expect(status).toBe(200);
});

Note: log is a direct utility (not a fixture), so import it separately.

Recommended: Use Option 1 (combined fixtures) unless you need fine control over which utilities are included.

Configuration

Environment Variables

# .env
PLAYWRIGHT_UTILS_LOG_LEVEL=debug  # debug | info | warn | error
PLAYWRIGHT_UTILS_RETRY_ATTEMPTS=3
PLAYWRIGHT_UTILS_TIMEOUT=30000

Playwright Config

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Playwright Utils works with standard Playwright config
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    extraHTTPHeaders: {
      // Add headers used by utilities
    }
  }
});

Troubleshooting

Import Errors

Problem: Cannot find module '@seontechnologies/playwright-utils/api-request'

Solution:

# Verify package installed
npm list @seontechnologies/playwright-utils

# Check package.json has correct version
"@seontechnologies/playwright-utils": "^2.0.0"

# Reinstall if needed
npm install -D @seontechnologies/playwright-utils

TEA Not Using Utilities

Problem: TEA generates tests without playwright-utils.

Causes:

  1. Config not set: tea_use_playwright_utils: false
  2. Workflow run before config change
  3. Package not installed

Solution:

# Check config
grep tea_use_playwright_utils _bmad/bmm/config.yaml

# Should show: tea_use_playwright_utils: true

# Start fresh chat (TEA loads config at start)

Type Errors with apiRequest

Problem: TypeScript errors on apiRequest response.

Cause: No schema validation.

Solution:

// Add Zod schema for type safety
import { z } from 'zod';

const ProfileSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email()
});

const { status, body } = await apiRequest({
  method: 'GET',
  path: '/api/profile'  // 'path' not 'url'
}).validateSchema(ProfileSchema);  // Chained method

expect(status).toBe(200);
// body is typed as { id: string, name: string, email: string }

Migration Guide

Migrating Existing Tests

Before (Vanilla Playwright):

test('should access protected route', async ({ page, request }) => {
  // Manual auth token fetch
  const response = await request.post('/api/auth/login', {
    data: { email: 'test@example.com', password: 'pass' }
  });
  const { token } = await response.json();

  // Manual token storage
  await page.goto('/dashboard');
  await page.evaluate((token) => {
    localStorage.setItem('authToken', token);
  }, token);

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

After (With Playwright Utils):

import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures';

test('should access protected route', async ({ page, authToken }) => {
  // authToken automatically fetched and persisted by fixture
  await page.goto('/dashboard');

  // Token is already in place (no manual storage needed)
  await expect(page).toHaveURL('/dashboard');
});

Benefits:

  • Token fetched once, reused across all tests (persisted to disk)
  • No manual token storage or management
  • Automatic token renewal if expired
  • Multi-user support via authOptions.userIdentifier
  • 10 lines → 5 lines (less code)

Getting Started:

Workflow Guides:

Other Customization:

Understanding the Concepts

Reference


Generated with BMad Method - TEA (Test Architect)