Playwright Assertions

An assertion is a comparison that is evaluated to check whether code functions as expected or not. Based on one or more assertions, a Playwright test is marked as passed or failed. This document describes standards relating to assertions in Playwright.

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 assertions must only be made in the test body or hooks · PW-02.1 · MUST · TEST

Keep assertions about the behaviour under test in the test itself, so it is clear what each test checks.

Hooks may also make assertions, but only to check that their own set-up or clean-up succeeded, for example that an API call to create or delete data returned the expected status. A failed set-up is then reported where it happened, rather than causing a confusing failure later in the test.

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);
});

Do not make assertions in page object or helper functions. An assertion hidden in a function makes it unclear what the test checks, and stops the function being reused by tests that expect a different outcome.

❌ The page function asserts the outcome, so every test that calls it makes this check:

// pages/customers.page.ts
public async addCustomer(name: string) {
  await this.addButton().click();
  await this.addCustomerModal.nameInput().fill(name);
  await this.addCustomerModal.submitButton().click();
  await expect(this.customerAddedToast()).toBeVisible();
}

✔️ The page function performs the actions, and the test asserts the outcome:

// pages/customers.page.ts
public async addCustomer(name: string) {
  await this.addButton().click();
  await this.addCustomerModal.nameInput().fill(name);
  await this.addCustomerModal.submitButton().click();
}

// tests/e2e-tests/customers.spec.ts
test('As an admin, I can add a customer', async ({ customersPage }) => {
  await customersPage.addCustomer(Customer.NAME);
  await expect(customersPage.customerAddedToast()).toBeVisible();
});

Playwright tests must have a strong assertion · PW-02.2 · MUST · TEST

Following the Arrange-Act-Assert method of testing, where a test sets up its data and state, performs the action under test, then checks the outcome, every test must include at least one assertion. Without an assertion, a test passes whatever happens and adds no value.

The assertion must also be strong: it must fail if the application does not behave as expected. A weak assertion checks something that is true even when the behaviour under test is broken.

Test Title Success Condition Weak Assertion Strong Assertion
Users can login with a valid username and password combination User achieves logged-in status Ensure the user is directed away from the /login URL 200 status from /login endpoint AND the user is redirected to the home page
Users can search for products by manufacturer Results table only shows results from the selected manufacturer The results table has less rows in it, indicating the filter was applied All rows of the manufacturer column contain the manufacturer name searched for
Users can add new team members to the group ‘Test Engineers’ The new team member has the group named ‘Test Engineers’ After clicking save, a “user added” toast message was shown Performing an API request for the new team members details shows they are in the group ‘Test Engineers’

Playwright tests must assert against the behaviour named in the test title · PW-02.3 · MUST · TEST

The assertion in each test must directly verify the behaviour described in the test title.

Make one ‘logical assertion’ per test. It may use more than one expect(), as long as together they check that single behaviour. For example, in the strong assertion examples, the login test checks both the /login response status and the redirect to the home page, because together they show that the user logged in.

Playwright tests must use the strictest assertions available · PW-02.4 · MUST · TEST

Use the assertion that fails for the widest range of wrong outcomes. A loose assertion can pass while the application is misbehaving, because the expected value is present alongside something that shouldn’t be.

❌ Loose ✔️ Strict Why the loose assertion can pass wrongly
toContainText('Internal - Payroll') toHaveText('Internal - Payroll') It also passes for ‘Internal - Payroll (DO NOT USE)’.
not.toHaveCount(0) toHaveCount(3) It passes whether one row or a hundred rows are shown.
toBeTruthy() toBe(true) It also passes for 1, 'false' or any non-empty object.
toMatchObject(customer) toEqual(customer) It ignores extra properties. Only use toMatchObject() when some values, such as generated IDs, cannot be known in advance.

Playwright tests must use toHaveText() over toContainText() · PW-02.5 · MUST · TEST

In this example, the expected department name is ‘Internal - Payroll’ not ‘Internal - Payroll (DO NOT USE)’. By using toHaveText(), we are able to guarantee that the expected data was shown on its own, without also including an error message or other undesired information.

// The department name 'Internal - Payroll (DO NOT USE)' would create a false positive
await expect(departmentTable.tableNextCellOf(name)).toContainText(Department.INTERNAL_PAYROLL);

// The department name 'Internal - Payroll (DO NOT USE)' would fail this check
await expect(departmentTable.tableNextCellOf(name)).toHaveText(Department.INTERNAL_PAYROLL);

Playwright tests must not assert against magic values · PW-02.6 · MUST · TEST

Follow the TypeScript standard TS-02.5 and do not use magic numbers or strings in assertions. An unexplained value hides what the test is checking, and has to be found and updated in every test if it changes. Use a named constant instead.

❌ Where do 26 and ‘Awaiting approval’ come from?

await expect(customersPage.customerRows()).toHaveCount(26);
await expect(customersPage.statusText()).toHaveText('Awaiting approval');

✔️ Each value has a name that explains it, and is defined once:

export const CUSTOMERS_PER_PAGE = 25;

export const CustomerStatus = {
  AWAITING_APPROVAL: 'Awaiting approval',
  APPROVED: 'Approved',
} as const;

export type CustomerStatus = typeof CustomerStatus[keyof typeof CustomerStatus];

// The table shows one page of customers, plus the header row
await expect(customersPage.customerRows()).toHaveCount(CUSTOMERS_PER_PAGE + 1);
await expect(customersPage.statusText()).toHaveText(CustomerStatus.AWAITING_APPROVAL);

Playwright tests should include custom expect messages to improve readability · PW-02.7 · SHOULD · TEST

Specify custom expect messages, as a second argument to the expect function, to document the expected behaviour of the test should the assertion fail.

// pages/home.page.ts
accountMenuButton = () => this.page.getByRole('button', { name: 'Account' });

// tests/e2e-tests/login.spec.ts
test('Users can log in with a valid username and password', async ({ loginPage, homePage }) => {
  await loginPage.loginAs(Env.USERNAME, Env.PASSWORD);

  // The custom message is shown if the account menu is not visible after logging in
  await expect(homePage.accountMenuButton(), 'should be logged in').toBeVisible();
});

If the expect passes, the successful step looks like this:

✅ should be logged in @login.spec.ts:6

This practice ensures that failures provide actionable context directly in the test logs, making debugging faster and more efficient. If the test fails, the resulting error would look like this:

Error: should be logged in

Call log:
  - expect.toBeVisible with timeout 5000ms
  - waiting for getByRole('button', { name: 'Account' })