A collection of utilities for Playwright tests at SEON Technologies, designed to make testing more efficient and maintainable.
All utilities can be used as Playwright fixtures by importing the test object.
Why this library was created:
- To bring consistent reusable Playwright utilities to projects at SEON.
- To implement common testing patterns as standardized fixtures, to avoid duplication and boilerplate.
- To follow a functional-first design: the core logic is always a standalone function that can be used directly, while fixtures provide convenience.
- To support typed API requests, polling patterns, auth session management, logging, and network interception with clear APIs.
- To make it easy to adopt and extend the utilities in other projects, without coupling tightly to any single app.
Design patterns used:
- Fixture pattern: all utilities can be consumed as fixtures to provide maximum flexibility.
- Functional core, fixture shell: utilities can be used both directly and as fixtures.
- Decoupled logging and reporting: logging is built to integrate cleanly into Playwright reports.
- Composable auth sessions: auth session utilities can handle complex multi-user auth in a reusable way.
- Test-focused network interception: network interception is designed for real-world test needs, not just simple mocking.
- Typed API request utility: apiRequest provides a reusable, typed client for API tests and Playwright request fixture usage.
This library is not a general-purpose Playwright wrapper. It is designed to cover the most common test automation needs at SEON and to serve as a foundation for further project-specific extensions.
- Playwright Utils
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.
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'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 UIThe 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.
The library provides the following utilities, each with both direct function imports and Playwright fixtures:
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'
})
})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 }
})
})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'
})
})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
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
- 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
authStoragePath: 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 withsetAuthProvider()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')
})β Auth Session Documentation
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
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
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!
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.
# 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.tgzThis package is published to the public npm registry under the @seontechnologies scope.
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
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