Playwright Hooks

A hook is a special function that runs before or after a test is executed. This document describes standards relating to hooks in Playwright.

In Playwright the hooks available are:

  • beforeAll() which runs once before all tests
  • beforeEach() which runs before every test block
  • afterEach() which runs after each test block
  • afterAll() which runs once all tests have been performed

Standard Metadata

  • Area: Playwright
  • Owner: Technical Leadership group
  • Last reviewed: 2026-10-07
  • Next review due from: 2026-12-01
  • Enforcement: ESLint / code review
  • Status: Active

Playwright tests must not duplicate logic which can be abstracted into a hook · PW-07.1 · MUST · TEST

Avoid duplication of logic, such as each test navigating to a particular URL and clicking on a series of buttons. In most cases the Arrange part of Arrange-Act-Assert, where a test sets up its data and state, can be included in a beforeEach() or beforeAll() hook.

❌ Every test repeats the same log-in and navigation:

test.describe('Customers page', { tag: '@customers' }, () => {
  test('As an admin, I can add a customer', { tag: '@high' }, async ({ loginPage, customersPage }) => {
    await loginPage.loginAs(Env.USERNAME, Env.PASSWORD);
    await customersPage.goto();
    ...
  });

  test('As an admin, I can edit a customer', { tag: '@high' }, async ({ loginPage, customersPage }) => {
    await loginPage.loginAs(Env.USERNAME, Env.PASSWORD);
    await customersPage.goto();
    ...
  });
});

✔️ The shared steps are in a hook:

test.describe('Customers page', { tag: '@customers' }, () => {
  test.beforeEach('Log in and open the customers page', async ({ loginPage, customersPage }) => {
    await loginPage.loginAs(Env.USERNAME, Env.PASSWORD);
    await customersPage.goto();
  });

  test('As an admin, I can add a customer', { tag: '@high' }, async ({ customersPage }) => { ... });

  test('As an admin, I can edit a customer', { tag: '@high' }, async ({ customersPage }) => { ... });
});

Playwright tests should use as few hooks as possible · PW-07.2 · SHOULD · TEST

Every hook hides steps from the tests it runs for, so a reader has to look above a test to understand what it does. Keep the number of hooks to a minimum:

  • Combine hooks of the same type at the same level into a single hook.
  • Place set-up needed by only some tests in a nested describe block, rather than running it before every test.

❌ Two beforeEach() hooks at the same level, one of which is only needed by the last test:

test.describe('Customers page', () => {
  test.beforeEach('Navigate to the customers page', async ({ customersPage }) => { ... });

  test.beforeEach('Create a customer', async ({ api }) => { ... });

  test('As an admin, I can add a customer', async ({ customersPage }) => { ... });

  test('As an admin, I can remove a customer', async ({ customersPage }) => { ... });
});

✔️ One hook for shared set-up, and the customer is only created for the test that needs it:

test.describe('Customers page', () => {
  test.beforeEach('Navigate to the customers page', async ({ customersPage }) => { ... });

  test('As an admin, I can add a customer', async ({ customersPage }) => { ... });

  test.describe(() => {
    test.beforeEach('Create a customer', async ({ api }) => { ... });

    test('As an admin, I can remove a customer', async ({ customersPage }) => { ... });
  });
});

Playwright hooks must be declared above the tests that use them · PW-07.3 · MUST · TEST

List all hooks at the top of their describe block, before any tests, so that steps run by the hooks, such as data clean-up in an afterEach() hook, are not missed when reading the tests.

✔️ All hooks are declared above the tests:

test.describe('Customers page', () => {
  test.beforeEach('Log in as an admin', async ({ loginPage }) => { ... });

  test.afterEach('Remove the created customer', async ({ api }) => { ... });

  test('As an admin, I can add a customer', async ({ customersPage }) => { ... });

  test('As an admin, I can edit a customer', async ({ customersPage }) => { ... });
});

❌ The afterEach() hook is declared below the tests, so its data clean-up is easy to miss:

test.describe('Customers page', () => {
  test.beforeEach('Log in as an admin', async ({ loginPage }) => { ... });

  test('As an admin, I can add a customer', async ({ customersPage }) => { ... });

  test('As an admin, I can edit a customer', async ({ customersPage }) => { ... });

  test.afterEach('Remove the created customer', async ({ api }) => { ... });
});

Playwright hooks should be given descriptions to improve readability · PW-07.4 · SHOULD · TEST

Hooks can accept either a function, or a description and a function. Use descriptions wherever possible to improve the overall readability of the code.

// Example Playwright beforeEach hook, with a description
test.beforeEach('Navigate to the new booking form', async ({ bookingPage }) => { ... });

Hook descriptions are shown in the Playwright UI. For the nested describe block example, the running order of each named hook can be seen like so:

Image showing the running order of a test where each hook is named

Playwright tests must be atomic and idempotent · PW-07.5 · MUST · TEST

Each test must be able to run on its own, in any order and as many times as needed, without relying on the outcome or side effects of other tests. Playwright can run tests in parallel and in a different order from the file, so a test that depends on another may pass locally and fail in the pipeline.

Do not use test.describe.serial() or test.describe.configure({ mode: 'serial' }) to make tests depend on each other. Give each test its own data set-up and clean-up instead, by creating its own data and removing any data it creates.

❌ The second test relies on the customer created by the first:

test('As an admin, I can add a customer', async ({ customersPage }) => { ... });

test('As an admin, I can remove the added customer', async ({ customersPage }) => { ... });

The fewer hooks example shows the fix: the remove test creates its own customer in a nested describe block.

Playwright tests must have their own data creation · PW-07.6 · MUST · TEST

Use a beforeEach() or beforeAll() hook to ensure tests create all the data needed to complete their checks. Do not rely on existing data to perform tests.

❌ The test relies on data that already exists in the environment:

test('As an admin, I can edit a customer', async ({ customersPage }) => {
  // Fails, or edits the wrong record, if 'Acme Ltd' is changed or removed
  await customersPage.searchForCustomer('Acme Ltd');
  ...
});

See the UI test data example for a test that creates its own customer in a beforeEach() hook.

EXCEPTION For projects that run tests against an ephemeral environment, created and seeded fresh for each pipeline run, this is a SHOULD. Tests may use the seeded data, but tests that change seeded records should still create their own, so that tests running in parallel don’t interfere with each other.

Playwright UI tests should create and remove their data through the API · PW-07.7 · SHOULD · TEST

In UI tests, create the data a test needs, and remove it afterwards, by calling the API endpoint functions from hooks, rather than through the UI. This makes tests faster and more reliable, and keeps each UI test focused on the behaviour it checks.

test.describe('Customers page', { tag: '@customers' }, () => {
  let customerId: number;

  test.beforeEach('Create a customer through the API', async ({ api }) => {
    const createCustomerResponse = await api.customers.createCustomer(Customer.NAME, Customer.HOUSE_NO, Customer.POSTCODE);
    expect(createCustomerResponse.status(), 'customer should be created').toBe(201);
    const createdCustomer: ApiTypes.Customers.SuccessfullyCreateCustomer = await createCustomerResponse.json();
    customerId = createdCustomer.id;
  });

  test.afterEach('Remove the customer through the API', async ({ api }) => {
    const deleteCustomerResponse = await api.customers.deleteCustomer(customerId);
    expect(deleteCustomerResponse.status(), 'customer should be deleted').toBe(204);
  });

  test('As an admin, I can edit a customer', { tag: '@high' }, async ({ customersPage }) => { ... });
});

Playwright tests must remove any data created · PW-07.8 · MUST · TEST

Ensure tests have their own teardown step, such as an afterEach() or afterAll() hook, to clear any data created.

Clean up in an afterEach() or afterAll() hook rather than at the end of the test, because hooks still run when the test fails.

❌ The clean-up is skipped if any earlier step fails:

test('As an admin, I can edit a customer', async ({ api, customersPage }) => {
  ...
  await api.customers.deleteCustomer(customerId);
});

See the UI test data example for clean-up in an afterEach() hook.

EXCEPTION For projects that run tests against an ephemeral environment, torn down after each pipeline run, this is a SHOULD. Clean-up is still recommended so tests can be re-run locally or against a shared environment without failing.