Stop writing waitFors β€” use web-first assertions

Learn how to add assertions and avoid all these "waitFor"s.

Thanks to auto-waiting mechanisms, your recorded test cases test many web functionality and critical user flows already. To nail down implementation details and test for data correctness, you need to add assertions.

Generic vs async assertions (web-first assertions)

Playwright Test provides an assertion library out of the box.

import { test, expect } from "@playwright/test";

expect provides generic and (!) async assertions.

Generic matchers are synchronous and are valuable for simple comparisons such as comparing two numbers.

// a synchronous generic assertion
expect(number).toBe(2);

To test web functionality, though, async assertions come as a handy alternative.

Playwright's asynchronous web-first assertions are tailored to the web. They're based on the same auto-waiting principles you already know about and wait / retry until a condition is met or the time out is reached.

// an asynchronous web-first assertion
// this assertion waits / retries until the located element becomes visible
await expect(page.getByText("welcome")).toBeVisible();

If you're testing websites, web-first assertions are more convenient to write and leverage PWT's core functionality.

import { test, expect } from "@playwright/test";

test("has title", async ({ page }) => {
  await page.goto("https://playwright.dev/");

  // πŸ‘Ž
  // test a condition at a single moment in time
  // this approach introduces flakiness
  expect(await page.getByText("welcome").isVisible()).toBe(true);

  // πŸ‘
  // wait for a condition to become truthy over time
  await expect(page.getByText("welcome")).toBeVisible();
});
Warning

Web-first assertions are async β€” you must await them. A missing await doesn't fail loudly: it returns a Promise (which is truthy), the assertion never actually runs, and the test passes silently.

Common web-first matchers

expect ships with a long list of matchers. The most common ones group into three buckets.

Element state
toBeVisibletoBeHidden
toBeAttachedin the DOM, even if not visible
toBeEnabledtoBeDisabled
toBeChecked
toBeFocused
toBeEmpty
toBeInViewport
Element content
toHaveTextexact match
toContainTextsubstring match
toHaveValueform fields
toHaveAttribute(name, value)
toHaveClass
toHaveCountnumber of matched elements
toHaveScreenshot
Page-level
toHaveURL
toHaveTitle
toHaveScreenshot

A handful of these matchers β€” toHaveText, toContainText, toHaveCount β€” work against locators that match multiple elements, no .first() / .nth() / .last() loop required.

Inline exercise

Assert on the whole product grid at onceSelect this container via test id: multi-element-assertions

The stage below renders three product cards. Instead of writing one assertion per card, confirm two things in a single line each:

  1. The grid renders exactly three products.
  2. The product names appear in this order: Product 1, Product 2, Product 3.

Products

  • Product 1

  • Product 2

  • Product 3

β–Έ Show the assertions

toHaveCount checks how many elements a locator resolved to. toHaveText accepts an array and compares it element-by-element against the matched set β€” content and order.

const container = page.getByTestId("multi-element-assertions");
const productsContainer = container.getByRole("region", { name: "Products" });
const products = productsContainer.getByRole("listitem");

await expect(products).toHaveCount(3);
await expect(products).toHaveText([/Product 1/, /Product 2/, /Product 3/]);

Re-shuffle the cards and the array assertion fails with a clear diff against the expected order.

There are some core things to know about assertions.

Configurable timeouts

Web-first assertions have a timeout config option if things take longer.

await expect(page.getByText("welcome")).toBeVisible({ timeout: 10_000 });
Note

The default timeout is 5s and can be changed on a project basis in your Playwright config under expect.timeout.

Soft assertions

Soft assertions (expect.soft) are a handy way to fail your test case but still try to run the following actions.

test("has title", async ({ page }) => {
  await page.goto("https://playwright.dev/");

  // If this assertion fails the test case will be marked as failed
  await expect.soft(page.getByTestId("status")).toHaveText("Success");

  // But all the following actions will still be executed and tested
  // ...
});

Soft assertion are particularly helpful when running longer tests. A soft assertion will lead to test failure but the test still continues running.

Soft assertion example in the HTML report

Assertions can be negated

Assertions also provide a quick way to flip around their meaning.

await expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();

Custom assertion messages

To make your assertions more readable in your test reports. You can also define a custom message.

await expect
  .soft(page, "should have an awesome title")
  .toHaveTitle("wrong title");

Custom assertion message

❗ Auto-waiting is the most important core principle in Playwright Test

With the built-in auto-waiting mechanisms you rarely have to implement manual waitFor statements.

// click() waits for the element to be actionable
// click() waits for a triggered navigation to complete
await locator.click();

// wait for the assertion to become truthy or time out
await expect(anotherLocator).toBeVisible();

Depending on the site you want to test, you might want to tweak the timeout configuration. These are Playwright's default timeouts for the mentioned auto-waiting concepts.

Test
30s
Globally
config.timeout
Per project
project.timeout
Per call
test.setTimeout(120_000)
expect
5s
Globally
config.expect.timeout
Per project
project.expect.timeout
Per call
expect(locator).toBeVisible({ timeout: 10_000 })
Action
no timeout
Globally
config.use.actionTimeout
Per project
project.use.actionTimeout
Per call
locator.click({ timeout: 10_000 })
Navigation
no timeout
Globally
config.use.navigationTimeout
Per project
project.use.navigationTimeout
Per call
page.goto('/', { timeout: 30_000 })

Hands on

Practice your assertions

Exercise 1 of 4

Assert the login worked

  1. Edit your previously recorded Login test.
  2. After the form submission, add a web-first assertion that confirms the logged-in user's name is visible
  3. After login out assert that the user name isn't visible
  4. Re-run the test and make sure it still passes.
Exercise 2 of 4

Lock down the homepage hero grid

  1. Navigate to /.
  2. Locate the hero products and assert there are exactly three.
  3. Ensure that all hero products are Snowboards.
Exercise 3 of 4

Search for products

  1. Create a new test
  2. Search for "Hydrogen"
  3. Create an add to cart flow from here
Exercise 4 of 4

Double check the cart handling

  1. Edit an existing test
  2. Validate that the cart calculations work
  3. Increase the quantity of an item
  4. Check the correct sum