# Playwright Hooks · PW-07

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:

```ts
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:

```ts
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](/standards/engineering/coding-standards/playwright/writing-tests#use-nested-playwright-describe-blocks-to-scope-set-up-needed-by-only-some-tests), rather than running it before every test.

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

```ts
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:

```ts
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:

```ts
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:

```ts
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.

```ts
// 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](/standards/engineering/coding-standards/playwright/writing-tests#use-nested-playwright-describe-blocks-to-scope-set-up-needed-by-only-some-tests), 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](/standards-assets/resources/named-hooks-in-playwright-runner.png)

## 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](/standards/engineering/coding-standards/playwright/hooks#playwright-tests-must-have-their-own-data-creation) and [removing any data it creates](/standards/engineering/coding-standards/playwright/hooks#playwright-tests-must-remove-any-data-created).

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

```ts
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](#playwright-tests-should-use-as-few-hooks-as-possible) 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:

```ts
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](/standards/engineering/coding-standards/playwright/hooks#playwright-ui-tests-should-create-and-remove-their-data-through-the-api) 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](/standards/engineering/coding-standards/playwright/api-testing) 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.

```ts
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:

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

See the [UI test data example](/standards/engineering/coding-standards/playwright/hooks#playwright-ui-tests-should-create-and-remove-their-data-through-the-api) 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.
