← Back to libraryQuestion 169 of 273
🎭PlaywrightBeginner

Web-First Assertions (auto-retrying expect)

📌 Definition:

Web-first assertions are Playwright expect() checks on a Locator or Page that automatically retry until the condition is met or a timeout expires. They wait for the UI to reach the expected state instead of asserting once against a snapshot.

📖 Detailed Explanation:

A classic Selenium bug is asserting immediately after an async action: element.getText() runs before the DOM updates, so the assertion fails intermittently. Playwright's expect(locator).toHaveText('Saved') polls the DOM until the text matches or the assertion timeout (default 5s) elapses. This applies to a whole family: toBeVisible, toBeEnabled, toHaveText, toHaveValue, toHaveCount, toContainText, toHaveURL, toHaveScreenshot and more. Because they retry, you rarely need explicit waits before assertions. It's important to distinguish auto-retrying assertions on locators (which poll) from non-retrying assertions on plain values (expect(2 + 2).toBe(4)), which evaluate once.

🔑 Key Points:
  • Assertions on Locator/Page auto-retry until true or the (separate) expect timeout expires
  • Removes 'assert too early' flakiness common in Selenium
  • Rich matchers: toBeVisible, toHaveText, toHaveValue, toHaveCount, toHaveURL, toHaveScreenshot
  • Assertions on plain values do NOT retry — only web-first assertions poll
  • expect timeout is configured separately from the action/test timeout
🌍 Real-World Example:

After clicking 'Save', a toast appears ~800ms later once the API responds. expect(page.getByRole('status')).toHaveText('Changes saved') polls until the toast text appears, passing as soon as it does — no sleep, no manual wait, and no intermittent failure from asserting before the toast renders.

📎 Code Example:
// Auto-retrying — waits for the state to arrive
await expect(page.getByRole('status')).toHaveText('Changes saved');
await expect(page.getByRole('row')).toHaveCount(5);
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();

// Override the retry timeout for one slow assertion
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 15_000 });

// NON-retrying — evaluates once (plain value)
expect(user.role).toBe('admin');
🎯 Scenario-Based Interview Question:

Scenario: A test does await page.locator('.toast').textContent() then expect(text).toBe('Saved'). It passes locally but fails ~15% of the time on CI. Diagnose and fix it using web-first assertions.