Skip to content
🤖 Consolidated, AI-optimized BMAD docs: llms-full.txt. Fetch this plain text file for complete context.
🚀 Build your own BMad modules and share them with the community! Get started or submit to the marketplace.

Integrate Playwright Utils with TEA

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

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

  • 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
  • Test framework setup complete (Playwright)
  • Node.js v18 or later

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

Terminal window
npm install -D @seontechnologies/playwright-utils

Edit _bmad/tea/config.yaml:

tea_use_playwright_utils: true

Note: If you enabled this during BMad installation, it’s already set.

Terminal window
# Check package installed
npm list @seontechnologies/playwright-utils
# Check TEA config
grep tea_use_playwright_utils _bmad/tea/config.yaml

Should show (4.4.0 is the current release; yours may be newer):

└── @seontechnologies/playwright-utils@4.4.0
tea_use_playwright_utils: true

The Short Version: It Becomes the Default, Not an Option

Section titled “The Short Version: It Becomes the Default, Not an Option”

tea_use_playwright_utils: true does not mean “the library is available if you ask for it.” It makes @seontechnologies/playwright-utils the implementation TEA reaches for on every capability the package covers, without you naming a utility in your prompt. The rule lives in the playwright-utils-mandate knowledge fragment, which every generating and reviewing workflow loads first.

Two enforcement levels:

REQUIRED — drop-in, nothing to wire. Generating the vanilla equivalent instead is a defect:

You needTEA emitsNot
Observe or stub an app API call in a UI testinterceptNetworkCall({ url, fulfillResponse? })page.route, page.waitForResponse
An HTTP call from a testapiRequest({ method, path, body? })request.get/post/..., await response.json()
Wait for an eventually consistent conditionrecurse(fn, predicate, { timeout })page.waitForTimeout, a while loop, bare expect.poll
Output the report should showlog.step / log.infoconsole.log
A test’s entry point (spec files)import { test } from '../support/merged-fixtures'import { test } from '@playwright/test'
Reading a downloaded CSV/XLSX/PDF/ZIPhandleDownload plus readCSV / readXLSX / …a parser per format
Catching a 4xx/5xx a green UI hidesthe network-error-monitor fixtureper-spec page.on('response') handlers

RECOMMENDED — needs project-side wiring, so TEA proposes it and scaffolds the wiring when the workflow’s scope covers setup: auth-session (an auth provider), network-recorder (a HAR directory), the webhook module (a mock provider), burn-in (a config file and a script). TEA never silently drops to the vanilla equivalent without saying so in the output.

Real exceptions still ship. page.route blocking analytics, fonts, or third-party scripts is correct and is not a deviation. Where a genuine gap exists, the generated code carries // playwright-utils deviation: <reason> and the workflow’s summary lists it, so nothing slips through unexplained.

The entry-point rule is about spec files. The merged-fixtures module itself imports mergeTests and re-exports expect from @playwright/test, and the examples below show exactly that; it is the one file that must reach for Playwright directly.

Scope. The mandate covers JavaScript/TypeScript suites on the Playwright runner, browser and API alike. Cypress, Maestro flows, Pact/Vitest contract suites, and backend suites in pytest, JUnit, Go test, xUnit, or RSpec are untouched by it.

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 (Merged Fixtures):

// Merge the fixtures your project needs into one test object
import { expect, mergeTests } from '@playwright/test';
import { log } from '@seontechnologies/playwright-utils';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
// Auth fixture built in your project (setAuthProvider + createAuthFixtures)
import { test as authFixture } from './support/auth/auth-fixture';
const test = mergeTests(authFixture, apiRequestFixture);
test('api test', async ({ apiRequest, authToken }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users',
headers: { Authorization: `Bearer ${authToken}` },
});
await log.info('Fetched users');
expect(status).toBe(200);
});

With Playwright Utils (Selective Merge):

import { expect, mergeTests } from '@playwright/test';
import { log } from '@seontechnologies/playwright-utils';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as interceptNetworkCallFixture } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
export const test = mergeTests(apiRequestFixture, interceptNetworkCallFixture);
export { expect, log };
test('api test', async ({ apiRequest }) => {
await log.info('Fetching users');
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users',
});
expect(status).toBe(200);
});

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 }
});

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

Without Playwright Utils:

  • Parallel sharding
  • Burn-in loops (basic shell scripts)
  • CI triggers (PR, push, schedule)
  • Artifact collection

With Playwright Utils: Enhanced with smart testing:

  • Burn-in utility (git diff-based, volume control)
  • Selective testing (skip config/docs/types changes)
  • Test prioritization by file changes

Typed HTTP client with schema validation.

Official Docs: https://seontechnologies.github.io/playwright-utils/api-request.html

Why Use This?

Vanilla Playwrightapi-request Utility
Manual await response.json()Automatic JSON parsing
response.status() + separate body parsingReturns { status, body } structure
No built-in retryAutomatic retry for 5xx errors
No schema validationSingle-line .validateSchema()
Verbose status checkingClean destructuring

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); // Chained method (can await separately if needed)
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

Authentication session management with token persistence.

Official Docs: https://seontechnologies.github.io/playwright-utils/auth-session.html

Why Use This?

Vanilla Playwright Authauth-session
Re-authenticate every test run (slow)Authenticate once, persist to disk
Single user per setupMulti-user support (roles, accounts)
No token expiration handlingAutomatic token renewal
Manual session managementProvider pattern (flexible auth)

Usage:

import { test as base, expect } from '@playwright/test';
import { createAuthFixtures, setAuthProvider } from '@seontechnologies/playwright-utils/auth-session';
import myCustomProvider from './support/auth/provider';
setAuthProvider(myCustomProvider); // Register before creating the fixtures
const test = base.extend(createAuthFixtures());
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

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

Official Docs: https://seontechnologies.github.io/playwright-utils/network-recorder.html

Why Use This?

Vanilla Playwright HARnetwork-recorder
Manual routeFromHAR() configurationAutomatic HAR management with PW_NET_MODE
Separate record/playback test filesSame test, switch env var
No CRUD detectionStateful mocking (POST/PUT/DELETE work)
Manual HAR file pathsAuto-organized by test name

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:

Terminal window
# 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)

Spy or stub network requests with automatic JSON parsing.

Official Docs: https://seontechnologies.github.io/playwright-utils/intercept-network-call.html

Why Use This?

Vanilla PlaywrightinterceptNetworkCall
Route setup + response waiting (separate steps)Single declarative call
Manual await response.json()Automatic JSON parsing (responseJson)
Complex filter predicatesSimple glob patterns (**/api/**)
Verbose syntaxConcise, readable API

Usage:

import { expect } from '@playwright/test';
import { test } from '@seontechnologies/playwright-utils/intercept-network-call/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 }

Async polling for eventual consistency (Cypress-style).

Official Docs: https://seontechnologies.github.io/playwright-utils/recurse.html

Why Use This?

Manual Pollingrecurse Utility
while loops with waitForTimeoutSmart polling with exponential backoff
Hard-coded retry logicConfigurable timeout/interval
No logging visibilityOptional logging with custom messages
Verbose, error-proneClean, readable API

Usage:

import { expect, 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';
const test = mergeTests(apiRequestFixture, recurseFixture);
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

Structured logging that integrates with Playwright reports.

Official Docs: https://seontechnologies.github.io/playwright-utils/log.html

Why Use This?

Console.log / printlog Utility
Not in test reportsIntegrated with Playwright reports
No step visualization.step() shows in Playwright UI
Manual object formattingLogs objects seamlessly
No structured outputJSON artifacts for debugging

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
  • Logs objects seamlessly (no special handling needed)
  • Trace test execution

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

Official Docs: https://seontechnologies.github.io/playwright-utils/file-utils.html

Why Use This?

Vanilla Playwrightfile-utils
~80 lines per CSV flow~10 lines end-to-end
Manual download event handlinghandleDownload() encapsulates all
External parsing librariesAuto-parsing (CSV, XLSX, PDF, ZIP)
No validation helpersBuilt-in validation (headers, row count)

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 }

Smart test selection with git diff analysis for CI optimization.

Official Docs: https://seontechnologies.github.io/playwright-utils/burn-in.html

Why Use This?

Playwright --only-changedburn-in Utility
Config changes trigger all testsSmart filtering (skip configs, types, docs)
All or nothingVolume control (run percentage)
No customizationCustom dependency analysis
Slow CI on minor changesFast CI with intelligent selection

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:

  • Ensure flake-free tests upfront - Never deal with test flake again
  • Smart filtering (skip config, types, docs changes)
  • Volume control (run percentage of affected tests)
  • Git diff-based test selection
  • Faster CI feedback

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

Official Docs: https://seontechnologies.github.io/playwright-utils/network-error-monitor.html

Why Use This?

Vanilla Playwrightnetwork-error-monitor
UI passes, backend 500 ignoredAuto-fails on any 4xx/5xx
Manual error checkingZero boilerplate (auto-enabled)
Silent failures slip throughActs like Sentry for tests
No domino effect preventionLimits cascading failures

Usage:

import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
// Network monitoring is enabled from here on
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 (500, 503, 504)
  • Prevents domino effect (limits cascading failures from one bad endpoint)
  • Opt-out with annotations for validation tests
  • Structured error reporting (JSON artifacts)
// Merge the fixtures this spec needs
import { expect, mergeTests } from '@playwright/test';
import { log } from '@seontechnologies/playwright-utils';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as interceptNetworkCallFixture } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
const test = mergeTests(apiRequestFixture, interceptNetworkCallFixture);
test('api test', async ({ apiRequest, interceptNetworkCall }) => {
await log.info('Fetching users');
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users',
});
expect(status).toBe(200);
});
import { test as base, mergeTests } from '@playwright/test';
import { test as apiRequest } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as interceptNetworkCall } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
import { test as networkErrorMonitor } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
import { log } from '@seontechnologies/playwright-utils';
// Merge only what you need
export const test = mergeTests(base, apiRequest, interceptNetworkCall, networkErrorMonitor);
export const expect = base.expect;
export { log };
import { test, expect, log } from '../support/merged-fixtures';
test('api test', async ({ apiRequest, interceptNetworkCall }) => {
await log.info('Fetching users');
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users',
});
expect(status).toBe(200);
});

Contrast:

  • Option 1: All utilities available, zero setup
  • Option 2: Pick utilities you need, one central file

See working examples: https://github.com/seontechnologies/playwright-utils/tree/main/playwright/support

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

Solution:

Terminal window
# 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

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:

Terminal window
# Check config
grep tea_use_playwright_utils _bmad/tea/config.yaml
# Should show: tea_use_playwright_utils: true
# Start fresh chat (TEA loads config at start)

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 }

Getting Started:

Workflow Guides:

Other Customization: