This is the single most important concept in Playwright assertions. Get this wrong and you will write flaky tests forever. Playwright has two categories of assertions: auto-retrying and non-retrying. The difference is simple but critical.
When you write expect(locator).toBeVisible(), Playwright does not check once and fail. It keeps checking again and again and again -- every 100ms -- until the element becomes visible or the timeout expires. Default timeout is 5 seconds. This means if your element appears after 2 seconds because of an API call, the assertion still passes. No sleep needed. No explicit waits. This is auto-retrying.
import { test, expect } from '@playwright/test';
test('auto-retrying assertions', async ({ page }) => {
await page.goto('/banking');
// These ALL auto-retry until condition is met or timeout expires:
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('balance')).toHaveText('$5,000.00');
await expect(page.getByRole('button', { name: 'Transfer' })).toBeEnabled();
await expect(page).toHaveURL(/\/dashboard/);
// Auto-retrying works because the argument is a LOCATOR.
// Playwright can re-query the DOM each time it retries.
});When you pass a plain value -- not a locator -- to expect(), there is nothing to retry. The value is already resolved. It is a snapshot in time. If the value is wrong, it fails immediately. No retries.
import { test, expect } from '@playwright/test';
test('non-retrying assertions', async ({ page }) => {
await page.goto('/shopping');
// These do NOT retry -- the value is resolved once:
const title = await page.title();
expect(title).toBe('Shopping Portal');
const url = page.url();
expect(url).toContain('/shopping');
const count = await page.locator('.product-card').count();
expect(count).toBeGreaterThan(0);
const text = await page.getByTestId('total').textContent();
expect(text).not.toBeNull();
});| Feature | Auto-Retrying | Non-Retrying |
|---|---|---|
| Syntax | await expect(locator).toBeVisible() | expect(value).toBe(...) |
| Input | Locator or Page object | Plain value (string, number, boolean) |
| Retries? | Yes -- polls until pass or timeout | No -- checks once, pass or fail |
| Needs await? | Yes -- it is async | No -- it is sync |
| Flaky? | Rarely -- handles timing naturally | Often -- race conditions with async UI |
| Use when? | Checking element/page state | Checking already-resolved values |
// BAD -- snapshot approach. Grabs text once. No retry.
const text = await page.getByTestId('status').textContent();
expect(text).toBe('Approved'); // Fails if status updates after 1 second
// GOOD -- auto-retrying. Keeps checking until text matches.
await expect(page.getByTestId('status')).toHaveText('Approved');
// Passes even if status updates after 1-4 secondsNever do expect(await locator.textContent()).toBe("something"). You are grabbing a snapshot and asserting on dead data. Use expect(locator).toHaveText("something") instead. The first approach is the #1 source of flaky Playwright tests.
Key Point: If the argument to expect() is a locator or page, the assertion auto-retries. If the argument is a plain value like a string or number, it checks once and is done. Always prefer auto-retrying assertions for element checks.
Q: What is the difference between auto-retrying and non-retrying assertions in Playwright?
A: Auto-retrying assertions take a locator or page object as input. Playwright re-queries the DOM repeatedly -- every 100ms -- until the condition is met or the timeout expires (default 5 seconds). Examples: expect(locator).toBeVisible(), expect(locator).toHaveText(). Non-retrying assertions take a plain value -- string, number, boolean. They check once and either pass or fail immediately. Example: expect(someString).toBe("hello"). The rule is simple: for anything on the page, always use the auto-retrying version. Only use non-retrying when you have already extracted a value and need to check it.
Key Point: expect(locator) auto-retries. expect(value) does not. Always use auto-retrying assertions for checking page elements.