@seontechnologies/playwright-utils
v3.13.1
Published
A collection of utilities for Playwright.
Maintainers
Readme
Playwright Utils
A collection of utilities for Playwright tests, designed to make testing more efficient and maintainable.
📚 View the full documentation on GitHub Pages or browse the docs folder
One Pattern, Two Ways to Use
Every utility follows the same design: functional core, fixture shell.
// Direct function - explicit dependencies
import { apiRequest } from '@seontechnologies/playwright-utils/api-request'
const result = await apiRequest({ request, method: 'GET', path: '/api/users' })
// Playwright fixture - injected, ready to use
test('example', async ({ apiRequest }) => {
const result = await apiRequest({ method: 'GET', path: '/api/users' })
})Use functions for scripts and simple cases. Use fixtures for test suites.
Utilities
| Category | Utilities | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Agnostic | API Request, Auth Session, Recurse, Log, File Utils, Burn-in | | Frontend | Network Interception, Network Recorder, Network Error Monitor |
- Playwright Utils
Installation
npm i -D @seontechnologies/playwright-utils
pnpm i -D @seontechnologies/playwright-utilsNote: This package requires
@playwright/testas a peer dependency. It should already be installed in your repository.
Development
Quick start (this repo):
git clone https://github.com/seontechnologies/playwright-utils.git
cd playwright-utils
nvm use
npm install
# start docker
# running the app initially may require docker to download things for a few minutes
npm run start:sample-app
# open a new tab, and run a test
# run with UI, with headless, and if you want also the IDE
npm run test:pw-ui
npm run test:pw# Install dependencies
npm i
# Development commands
npm run lint # Run ESLint
npm run typecheck # Run TypeScript checks
npm run fix:format # Fix code formatting with Prettier
npm run test # Run unit tests
npm run validate # Run all the above in parallel
# Start the sample app (for testing apiRequest, recurse, auth-session)
npm run start:sample-app
# Playwright tests
npm run test:pw # Run Playwright tests
npm run test:pw-ui # Run Playwright tests with UIAvailable Utilities
The library provides the following utilities, each with both direct function imports and Playwright fixtures:
API Request
A typed, flexible HTTP client for making API requests in tests.
// Direct import
import { apiRequest } from '@seontechnologies/playwright-utils'
test('example', async ({ request }) => {
const { status, body } = await apiRequest({
request, // need to pass in request context when using this way
method: 'GET',
path: '/api/users/123'
})
})
// As a fixture
import { test } from '@seontechnologies/playwright-utils/fixtures'
// or use your own main fixture (with mergeTests) and import from there
test('example', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users/123'
})
})Recurse (Polling)
A powerful polling utility for waiting on asynchronous conditions.
// note that there is no need to pass in request or page context from Playwright
// Direct import
import { recurse } from '@seontechnologies/playwright-utils/recurse'
test('example', async ({}) => {
const result = await recurse(
() => fetchData(),
(data) => data.status === 'ready',
{ timeout: 30000 }
)
})
// As a fixture
import { test } from '@seontechnologies/playwright-utils/fixtures'
// or use your own main fixture (with mergeTests) and import from there
test('example', async ({ recurse }) => {
const result = await recurse({
command: () => fetchData(),
predicate: (data) => data.status === 'ready',
options: { timeout: 30000 }
})
})Logging
A specialized logging utility that integrates with Playwright's test reports.
// Direct import
import { log } from '@seontechnologies/playwright-utils'
await log.info('Information message')
await log.step('Starting a new test step')
await log.error('Something went wrong', false) // Disable console output// As a fixture
import { test } from '@seontechnologies/playwright-utils/log/fixtures'
test('example', async ({ log }) => {
await log({
message: 'Starting test',
level: 'step'
})
})Network Interception
A powerful utility for intercepting, observing, and mocking network requests in Playwright tests.
// Direct import
import { interceptNetworkCall } from '@seontechnologies/playwright-utils'
test('Spy on the network', async ({ page }) => {
// Set up the interception before navigating
const networkCall = interceptNetworkCall({
page,
method: 'GET', // GET is optional
url: '**/api/users'
})
await page.goto('/users-page')
// Wait for the intercepted response and access the result
const { responseJson, status } = await networkCall
expect(responseJson.length).toBeGreaterThan(0)
expect(status).toBe(200)
})// As a fixture
import { test } from '@seontechnologies/playwright-utils/fixtures'
test('Stub the network', async ({ page, interceptNetworkCall }) => {
// With fixture, you don't need to pass the page object
const mockResponse = interceptNetworkCall({
method: 'GET',
url: '**/api/users',
fulfillResponse: {
status: 200,
body: { data: [{ id: 1, name: 'Test User' }] }
}
})
await page.goto('/users-page')
// Wait for the intercepted response
await mockResponse
expect(responseJson.data[0].name).toBe('Test User')
})// Conditional request handling
test('Modify responses', async ({ page, interceptNetworkCall }) => {
await interceptNetworkCall({
url: '/api/data',
handler: async (route, request) => {
if (request.method() === 'POST') {
// Handle POST requests
await route.fulfill({
status: 200,
body: JSON.stringify({ success: true })
})
} else {
// Continue with other requests
await route.continue()
}
}
})
})→ Network Interception Documentation
Auth Session
An authentication session management system for Playwright tests that persists tokens between test runs:
- Faster tests with persistent token storage
- User-based, on the fly authentication support
- Support for both UI and API testing
Implementation Steps
- Configure Global Setup - Create
playwright/support/global-setup.tsand add it to your Playwright config- Sets up authentication storage and initializes the auth provider
- Nearly identical across all applications
// 1. Configure Global Setup (playwright/support/global-setup.ts)
import {
authStorageInit,
setAuthProvider,
configureAuthSession,
authGlobalInit
} from '@seontechnologies/playwright-utils/auth-session'
import myCustomProvider from './auth/custom-auth-provider'
async function globalSetup() {
// Ensure storage directories exist
authStorageInit()
// STEP 1: Configure auth storage settings
configureAuthSession({
// store auth tokens anywhere you want, and remember to gitignore the directory
storageDir: process.cwd() + '/playwright/auth-sessions',
debug: true
})
// STEP 2: Set up custom auth provider
// This defines HOW authentication tokens are acquired and used
setAuthProvider(myCustomProvider)
// Optional: pre-fetch all tokens in the beginning
await authGlobalInit()
}
export default globalSetup⚠️ IMPORTANT: The order of function calls in your global setup is critical. Always register your auth provider with
setAuthProvider()after configuring the session. This ensures the auth provider is properly initialized.
- Create Auth Fixture - Add
playwright/support/auth/auth-fixture.tsto your merged fixtures- Provides standardized Playwright test fixtures for authentication
- Generally reusable across applications without modification
- CRITICAL: Register auth provider early to ensure it's always available
Add playwright/support/auth/auth-fixture.ts to your merged fixtures
// 1. Create Auth Fixture (playwright/support/auth/auth-fixture.ts)
import { test as base } from '@playwright/test'
import {
createAuthFixtures,
type AuthOptions,
type AuthFixtures,
setAuthProvider // Import the setAuthProvider function
} from '@seontechnologies/playwright-utils/auth-session'
// Import your custom auth provider
import myCustomProvider from './custom-auth-provider'
// Register the auth provider early
setAuthProvider(myCustomProvider)
export const test = base.extend<AuthFixtures>({
// For authOptions, we need to define it directly using the Playwright array format
authOptions: [defaultAuthOptions, { option: true }],
// Use the other fixtures directly
...createAuthFixtures()
// In your tests, use the auth token
test('authenticated API request', async ({ authToken, request }) => {
const response = await request.get('https://api.example.com/protected', {
headers: { Authorization: `Bearer ${authToken}` }
})
expect(response.ok()).toBeTruthy()
})- Create Custom Auth Provider - Implement token management with modular utilities:
// playwright/support/auth/custom-auth-provider.ts
import {
type AuthProvider,
authStorageInit,
getTokenFilePath,
saveStorageState
} from '@seontechnologies/playwright-utils/auth-session'
import { log } from '@seontechnologies/playwright-utils/log'
import { acquireToken } from './token/acquire'
import { checkTokenValidity } from './token/check-validity'
import { isTokenExpired } from './token/is-expired'
import { extractToken, extractCookies } from './token/extract'
import { getEnvironment } from './get-environment'
import { getUserIdentifier } from './get-user-identifier'
const myCustomProvider: AuthProvider = {
// Get the current environment to use
getEnvironment,
// Get the current user identifier to use
getUserIdentifier,
// Extract token from storage state
extractToken,
// Extract cookies from token data for browser context
extractCookies,
// Check if token is expired
isTokenExpired,
// Main token management method
async manageAuthToken(request, options = {}) {
const environment = this.getEnvironment(options)
const userIdentifier = this.getUserIdentifier(options)
const tokenPath = getTokenFilePath({
environment,
userIdentifier,
tokenFileName: 'storage-state.json'
})
// Check for existing valid token
const validToken = await checkTokenValidity(tokenPath)
if (validToken) return validToken
// Initialize storage and acquire new token if needed
authStorageInit({ environment, userIdentifier })
const storageState = await acquireToken(
request,
environment,
userIdentifier,
options
)
// Save and return the new token
saveStorageState(tokenPath, storageState)
return storageState
},
// Clear token when needed
clearToken(options = {}) {
const environment = this.getEnvironment(options)
const userIdentifier = this.getUserIdentifier(options)
const storageDir = getStorageDir({ environment, userIdentifier })
const authManager = AuthSessionManager.getInstance({ storageDir })
authManager.clearToken()
return true
}
}
export default myCustomProvider- Use the Auth Session in Your Tests
import { test } from '../support/auth/auth-fixture'
test('access protected resources', async ({ page, authToken }) => {
// API calls with token
const response = await request.get('/api/protected', {
headers: { Authorization: `Bearer ${authToken}` }
})
// Or use the pre-authenticated page
await page.goto('/protected-area')
})
// Ephemeral user authentication
import { applyUserCookiesToBrowserContext } from '@seontechnologies/playwright-utils/auth-session'
test('ephemeral user auth', async ({ context, page }) => {
// Apply user auth directly to browser context (no disk persistence)
const user = await createTestUser({ userIdentifier: 'admin' })
await applyUserCookiesToBrowserContext(context, user)
// Page is now authenticated with the user's token
await page.goto('/protected-page')
})File Utilities
A comprehensive set of utilities for reading, validating, and waiting for files (CSV, XLSX, PDF, ZIP).
// Direct import
import { readCSV } from '@seontechnologies/playwright-utils/file-utils'
test('example', async () => {
const result = await readCSV({ filePath: '/path/to/data.csv' })
})
// As a fixture
import { test } from '@seontechnologies/playwright-utils/file-utils/fixtures'
test('example', async ({ fileUtils }) => {
const isValid = await fileUtils.validateCSV({
filePath: '/path/to/data.csv',
expectedRowCount: 10
})
})→ File Utilities Documentation
Network Recorder
A HAR-based network traffic recording and playback utility that enables frontend tests to run in complete isolation from backend services. Features intelligent stateful CRUD detection for realistic API behavior.
// Control mode in your test file (recommended)
process.env.PW_NET_MODE = 'record' // or 'playback'
// As a fixture (recommended)
import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures'
test('CRUD operations work offline', async ({
page,
context,
networkRecorder
}) => {
// Setup - automatically records or plays back based on PW_NET_MODE
await networkRecorder.setup(context)
await page.goto('/')
// First time: records all network traffic to HAR file
// Subsequent runs: plays back from HAR file (no backend needed!)
await page.fill('#movie-name', 'Inception')
await page.click('#add-movie')
// Intelligent CRUD detection ensures the movie appears in the list
// even though we're running offline!
await expect(page.getByText('Inception')).toBeVisible()
})# Alternative: Environment-based mode switching
PW_NET_MODE=record npm run test:pw # Record network traffic to HAR files
PW_NET_MODE=playback npm run test:pw # Playback from existing HAR files→ Network Recorder Documentation
Burn-in
A smart test burn-in utility that enhances Playwright's --only-changed using a process of elimination to reduce unnecessary test runs.
Key Benefits:
- 🚫 Skip irrelevant changes: Config/type files don't trigger tests
- 📊 Volume control: Run a percentage of tests AFTER filtering
- 🎯 Process of elimination: Start with all, filter out irrelevant, control volume
Quick Setup:
- Create a burn-in script:
// scripts/burn-in-changed.ts
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in'
async function main() {
await runBurnIn()
}
main().catch(console.error)- Add package.json script:
{
"scripts": {
"test:pw:burn-in-changed": "tsx scripts/burn-in-changed.ts"
}
}- Create configuration:
// config/.burn-in.config.ts (recommended location)
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in'
const config: BurnInConfig = {
// Files that should never trigger tests (first filter)
skipBurnInPatterns: [
'**/config/**',
'**/*constants*',
'**/*types*',
'**/*.md'
],
// Control test volume AFTER skip filtering (0.3 = 30% of remaining tests)
burnInTestPercentage: process.env.CI ? 0.2 : 0.3,
// Burn-in repetition settings
burnIn: {
repeatEach: process.env.CI ? 2 : 3,
retries: process.env.CI ? 0 : 1
}
}
export default configHow it works (Custom Dependency Analysis):
- Git diff analysis identifies all changed files (e.g., 21 files)
- Skip patterns filter out irrelevant files (e.g., 6 config files → 15 remaining files)
- Custom dependency analyzer finds tests that actually depend on those 15 files (e.g., 3 tests)
- Volume control runs a percentage of the found tests (e.g., 100% of 3 = 3 tests)
Result: Run 3 targeted tests instead of 147 with Playwright's --only-changed!
Network Error Monitor
Automatically detects and reports HTTP 4xx/5xx errors during test execution. Tests fail if network errors occur, even when UI appears correct.
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures'
test('my test', async ({ page }) => {
await page.goto('/dashboard')
// Fails if any HTTP 4xx/5xx errors occur
})Features: Auto-enabled for all tests, catches silent backend failures, attaches JSON artifacts, respects test status (skipped/interrupted/failed).
Integration:
import { test as networkErrorMonitorFixture } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures'
export const test = mergeTests(authFixture, networkErrorMonitorFixture)Opt-out for tests expecting errors:
test(
'validation',
{ annotation: [{ type: 'skipNetworkMonitoring' }] },
async ({ page }) => {
// Monitoring disabled
}
)Inspired by Checkly's network monitoring pattern. See full docs.
Module Format Support
This package supports both CommonJS and ES Modules formats:
- CommonJS: For projects using
require()syntax or CommonJS module resolution - ES Modules: For projects using
importsyntax with ES modules
The package automatically detects which format to use based on your project's configuration. This means:
- You can use this package in both legacy CommonJS projects and modern ESM projects
- No need to change import paths or add file extensions
- TypeScript type definitions work for both formats
Example usage:
// Works in both CommonJS and ESM environments
import { log } from '@seontechnologies/playwright-utils'
// Subpath imports also work in both formats
import { recurse } from '@seontechnologies/playwright-utils/recurse'Testing Strategy in this repository
We showcase how to use the utilities in this repository through tests, often comparing and contrasting to vanilla Playwright patterns. We are using a backend and a frontend application to demonstrate more complex scenarios.
The overall testing approach:
Deployed Apps Tests - Some tests use Playwright's deployed apps to keep things familiar (
log,interceptNetworkCall):playwright/tests/network-mock-original.spec.tsplaywright/tests/todo-with-logs.spec.tsplaywright/tests/network-mock-intercept-network-call.spec.ts
Sample App Tests - The
./sample-appprovides a more complex environment to test:- API request automation
- Recursion and retry patterns
- Authentication flows
- Future: feature flag testing, email testing, etc.
To start the sample app backend and frontend; npm run start:sample-app.
The sample app uses "@seontechnologies/playwright-utils": "*" in its package.json so that changes to the library are immediately available for testing without requiring republishing or package updates.
Testing the Package Locally
# Build the package
npm run build
# Create a tarball package
npm pack
# Install in a target repository (change the version according to the file name)
# For npm projects:
npm install ../playwright-utils/seontechnologies-playwright-utils-1.0.1.tgz
# For pnpm projects:
pnpm add file:/path/to/playwright-utils-1.0.1.tgzRelease and Publishing
This package is published to the public npm registry under the @seontechnologies scope.
Publishing via GitHub UI (Recommended)
You can trigger a release directly from GitHub's web interface:
- Go to the repository → Actions → "Publish Package" workflow
- Click "Run workflow" button (dropdown on the right)
- Select options in the form:
- Branch: main
- Version type: patch/minor/major/custom
- Custom version: Only needed if you selected "custom" type
- Click "Run workflow"
Important:
- Requires
NPM_TOKENsecret to be configured in GitHub repository settings - You must review and merge the PR to complete the process
Publishing Locally
You can also publish the package locally using the provided script:
# 1. Set your npm token as an environment variable
# Get your token from: https://www.npmjs.com/settings/~/tokens
export NPM_TOKEN=your_npm_token
# 2. Run the publish script
npm run publish:local