Files
FancyWordle/.agents/skills/playwright-best-practices/core/assertions-waiting.md
T
2026-05-16 13:57:02 -04:00

9.2 KiB

Assertions & Waiting

Table of Contents

  1. Web-First Assertions
  2. Generic Assertions
  3. Soft Assertions
  4. Waiting Strategies
  5. Polling & Retrying
  6. Custom Matchers

Web-First Assertions

Auto-retry until condition is met or timeout. Always prefer these over generic assertions.

Locator Assertions

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

// Visibility
await expect(page.getByRole("button")).toBeVisible();
await expect(page.getByRole("button")).toBeHidden();
await expect(page.getByRole("button")).not.toBeVisible();

// Enabled/Disabled
await expect(page.getByRole("button")).toBeEnabled();
await expect(page.getByRole("button")).toBeDisabled();

// Text content
await expect(page.getByRole("heading")).toHaveText("Welcome");
await expect(page.getByRole("heading")).toHaveText(/welcome/i);
await expect(page.getByRole("heading")).toContainText("Welcome");

// Count
await expect(page.getByRole("listitem")).toHaveCount(5);

// Attributes
await expect(page.getByRole("link")).toHaveAttribute("href", "/home");
await expect(page.getByRole("img")).toHaveAttribute("alt", /logo/i);

// CSS
await expect(page.getByRole("button")).toHaveClass(/primary/);
await expect(page.getByRole("button")).toHaveCSS("color", "rgb(0, 0, 255)");

// Input values
await expect(page.getByLabel("Email")).toHaveValue("user@example.com");
await expect(page.getByLabel("Email")).toBeEmpty();

// Focus
await expect(page.getByLabel("Email")).toBeFocused();

// Checked state
await expect(page.getByRole("checkbox")).toBeChecked();
await expect(page.getByRole("checkbox")).not.toBeChecked();

// Editable state
await expect(page.getByLabel("Name")).toBeEditable();

Page Assertions

// URL
await expect(page).toHaveURL("/dashboard");
await expect(page).toHaveURL(/\/dashboard/);

// Title
await expect(page).toHaveTitle("Dashboard - MyApp");
await expect(page).toHaveTitle(/dashboard/i);

Response Assertions

const response = await page.request.get("/api/users");
await expect(response).toBeOK();
await expect(response).not.toBeOK();

Generic Assertions

Use for non-UI values. Do NOT retry - execute immediately.

// Equality
expect(value).toBe(5);
expect(object).toEqual({ name: "Test" });
expect(array).toContain("item");

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

// Numbers
expect(value).toBeGreaterThan(5);
expect(value).toBeLessThanOrEqual(10);
expect(value).toBeCloseTo(5.5, 1);

// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain("substring");

// Arrays/Objects
expect(array).toHaveLength(3);
expect(object).toHaveProperty("key", "value");

// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow("error message");
await expect(asyncFn()).rejects.toThrow();

Soft Assertions

Continue test execution after failure, report all failures at end.

test("check multiple elements", async ({ page }) => {
  await page.goto("/dashboard");

  // Won't stop on first failure
  await expect.soft(page.getByRole("heading")).toHaveText("Dashboard");
  await expect.soft(page.getByRole("button", { name: "Save" })).toBeEnabled();
  await expect.soft(page.getByText("Welcome")).toBeVisible();

  // Test continues; all failures reported at end
});

Soft Assertions with Early Exit

test("check form", async ({ page }) => {
  await expect.soft(page.getByRole("form")).toBeVisible();

  // Exit early if form not visible (pointless to check fields)
  if (expect.soft.hasFailures()) {
    return;
  }

  await expect.soft(page.getByLabel("Name")).toBeVisible();
  await expect.soft(page.getByLabel("Email")).toBeVisible();
});

Waiting Strategies

Auto-Waiting (Default)

Actions automatically wait for:

  • Element to be attached to DOM
  • Element to be visible
  • Element to be stable (no animations)
  • Element to be enabled
  • Element to receive events
// These auto-wait
await page.click("button");
await page.fill("input", "text");
await page.getByRole("button").click();

Wait for Navigation

// Wait for URL change
await page.waitForURL("/dashboard");
await page.waitForURL(/\/dashboard/);

// Wait for navigation after action
await Promise.all([
  page.waitForURL("**/dashboard"),
  page.click('a[href="/dashboard"]'),
]);

// Or without Promise.all
const urlPromise = page.waitForURL("**/dashboard");
await page.click("a");
await urlPromise;

Wait for Network

// Wait for specific response
const responsePromise = page.waitForResponse("**/api/users");
await page.click("button");
const response = await responsePromise;
expect(response.status()).toBe(200);

// Wait for request
const requestPromise = page.waitForRequest("**/api/submit");
await page.click("button");
const request = await requestPromise;

// Wait for no network activity
await page.waitForLoadState("networkidle");

Wait for Element State

// Wait for element to appear
await page.getByRole("dialog").waitFor({ state: "visible" });

// Wait for element to disappear
await page.getByText("Loading...").waitFor({ state: "hidden" });

// Wait for element to be attached
await page.getByTestId("result").waitFor({ state: "attached" });

// Wait for element to be detached
await page.getByTestId("modal").waitFor({ state: "detached" });

Wait for Function

// Wait for arbitrary condition
await page.waitForFunction(() => {
  return document.querySelector(".loaded") !== null;
});

// With arguments
await page.waitForFunction(
  (selector) => document.querySelector(selector)?.textContent === "Ready",
  ".status",
);

Polling & Retrying

toPass() for Polling

Retry until block passes or times out:

await expect(async () => {
  const response = await page.request.get("/api/status");
  expect(response.status()).toBe(200);

  const data = await response.json();
  expect(data.ready).toBe(true);
}).toPass({
  intervals: [1000, 2000, 5000], // Retry intervals
  timeout: 30000,
});

expect.poll()

Poll a function until assertion passes:

// Poll API until condition met
await expect
  .poll(
    async () => {
      const response = await page.request.get("/api/job/123");
      return (await response.json()).status;
    },
    {
      intervals: [1000, 2000, 5000],
      timeout: 30000,
    },
  )
  .toBe("completed");

// Poll DOM value
await expect.poll(() => page.getByTestId("counter").textContent()).toBe("10");

Custom Matchers

// playwright.config.ts or fixtures
import { expect } from "@playwright/test";

expect.extend({
  async toHaveDataLoaded(page: Page) {
    const locator = page.getByTestId("data-container");
    let pass = false;
    let message = "";

    try {
      await expect(locator).toBeVisible();
      await expect(locator).not.toContainText("Loading");
      pass = true;
    } catch (e) {
      message = `Expected data to be loaded but found loading state`;
    }

    return { pass, message: () => message };
  },
});

// Extend TypeScript types
declare global {
  namespace PlaywrightTest {
    interface Matchers<R> {
      toHaveDataLoaded(): Promise<R>;
    }
  }
}

// Usage
await expect(page).toHaveDataLoaded();

Timeouts

Configure Timeouts

// playwright.config.ts
export default defineConfig({
  timeout: 30000, // Test timeout
  expect: {
    timeout: 5000, // Assertion timeout
  },
});

// Per-test timeout
test("long test", async ({ page }) => {
  test.setTimeout(60000);
  // ...
});

// Per-assertion timeout
await expect(page.getByRole("button")).toBeVisible({ timeout: 10000 });

Best Practices

Do Don't
Use web-first assertions Use generic assertions for DOM
Let auto-waiting work Add unnecessary explicit waits
Use toPass() for polling Write manual retry loops
Configure appropriate timeouts Use waitForTimeout()
Check specific conditions Wait for arbitrary time

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
await page.waitForTimeout(5000) Slow, flaky, arbitrary timing Use auto-waiting or waitForResponse
await new Promise(resolve => setTimeout(resolve, 1000)) Same as above Use waitForResponse or element state waits
Generic assertions on DOM elements No auto-retry, flaky Use web-first assertions with expect()