Learn Playwright - Custom Tooling & Extensions
Episode 18 of 23

Learn Playwright - Custom Tooling & Extensions

This episode covers custom test fixtures and helpers, extending Playwright with plugins and custom reporters, as well as sharing utilities across projects.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

As your suite grows, your needs will exceed Playwright's built-in capabilities. This episode 18 covers custom tooling: building a layer on top of Playwright — custom fixtures, helper methods, custom reporters, and plugins — so the suite reflects your project's specific needs.

These abilities mark the transition from being a user to being a builder of test infrastructure. Instead of repeating boilerplate in every test, you design a foundation that can be reused, tested itself, and shared across projects.

Creating Custom Test Fixtures and Helpers

Fixtures with Setup and Cleanup

Fixtures don't just supply objects — they also manage the lifecycle. Here's a fixture that creates a test user and then cleans it up:

JSFixture with setup and cleanup
import { test as base, expect } from '@playwright/test';
import { UserApi } from './api/UserApi';
 
export const test = base.extend({
  user: async ({ request }, use) => {
    const userApi = new UserApi(request);
    const user = await userApi.createTestUser();
    await use(user);
    await userApi.deleteUser(user.id);
  },
});
 
export { expect };

base.extend({ user: ... }) creates a user fixture that creates data, hands it to the test via use(user), then cleans it up after the test finishes — even if the test fails. This removes setup/cleanup boilerplate from every test.

Centralized Helper Methods

Helpers are pure functions that wrap repetitive logic. Separate them into their own module so they can be used without the test runner:

JShelpers/formatters.ts
export function formatIDR(nominal: number) {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency: 'IDR',
  }).format(nominal);
}
 
export async function waitForToast(page) {
  await page.getByRole('status').waitFor();
}

formatIDR(nominal) formats a number as Indonesian Rupiah, and waitForToast(page) waits for a notification. Centralized helpers make tests more concise and consistent.

Extending Playwright with Plugins and Reporters

Custom Reporter

A reporter determines how test results are presented. You can create a custom reporter that sends results to an internal system:

JScustom-reporter.ts
import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
 
class SlackReporter implements Reporter {
  onTestEnd(test: TestCase, result: TestResult) {
    if (result.status !== 'passed') {
      console.log(`[ALERT] ${test.title} ${result.status}`);
    }
  }
}
 
export default SlackReporter;

class SlackReporter implements Reporter uses the Reporter interface from @playwright/test/reporter. By using a custom reporter, suite results can be routed to Slack, a dashboard, or a database — whatever the team needs.

Enabling the Reporter in the Configuration

JSConfig with a custom reporter
export default defineConfig({
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['./custom-reporter.ts'],
  ],
});

reporter accepts a list of reporters that run in sequence. Combine built-in reporters (html, list) with a custom reporter for a complete output mix.

Plugins and Global Setup

For work before all tests — for example, preparing a database or tokens — use global setup:

JSGlobal setup config
export default defineConfig({
  globalSetup: './global-setup.ts',
  globalTeardown: './global-teardown.ts',
});

globalSetup runs code once before the whole suite. This is the right place to prepare a shared environment — seed data, build the app, or start a service.

Integration with Test Utilities and Codegen Scripts

Using Playwright in Automation Scripts

Playwright isn't only for tests — it can be used as a library in any script. This integration opens the door for internal tooling:

JSScript using the playwright library
import { chromium } from 'playwright';
 
(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'preview.png' });
  await browser.close();
})();

The script above uses playwright (not @playwright/test) as a pure library — ideal for scheduled jobs, preview generators, or internal pipelines.

Writing Scripts with Codegen Assistance

npx playwright codegen isn't only for creating tests — it also produces an interaction sketch you can refine into a script. Record a complex flow, then extract the important logic into an automation script or a test helper.

Combining into a Custom Pipeline

Because it runs as plain Node.js, the Playwright library can be called from scripts run by cron, Lambda, or data pipelines. The same chromium.launch() pattern works anywhere Node.js runs.

Sharing Utilities Across Projects

Packaging as an NPM Module

When several repositories use the same helpers, package them into an internal NPM module:

Internal module structure
playwright-utils/
  ├── fixtures/
  │     └── index.ts
  ├── helpers/
  │     └── index.ts
  └── index.ts

This module is published to an internal registry or used as a git dependency. All test projects then import helpers from a single source — changes only need to be made once.

Importing in a Test Project

JSImport helpers from an internal module
import { test, expect } from '@acme/playwright-utils';
 
test('using a shared fixture', async ({ user, page }) => {
  await page.goto('/profile');
  await expect(page.getByText(user.email)).toBeVisible();
});

import { test, expect } from '@acme/playwright-utils' replaces the import from @playwright/test. All shared fixtures and helpers are available directly — consistency is guaranteed across every repository.

Documentation and Versioning

A shared module is only useful if it's well-documented and versioned. Write documentation for every fixture and helper, release with semantic versioning, and make sure exports stay compatible via type-checking (npx tsc --noEmit) before publishing. Consumers follow changes through the changelog, not by guessing.

Closing

Episode 18 lifted you from being a user to being a tooling builder: custom fixtures manage the data lifecycle, centralized helpers reduce duplication, custom reporters and global setup extend the runner's behavior, and internal module packaging enables sharing utilities across projects in a structured way.

Key takeaways:

  • Custom fixtures with use manage setup and cleanup automatically.
  • Pure helpers separated from the test runner are easier to reuse and test.
  • Custom reporters route suite results to whatever system you need.
  • playwright as a library opens automation scripts outside the test runner.
  • Package shared utilities as an internal NPM module for cross-project reuse.

In the next episode we'll discuss operational readiness and runbooks — runbooks for flaky tests, environment drift, and browser failures, managing suite health and team ownership, recovery strategies when baselines change, and maintenance and cleanup schedules.