Web-First Assertions vs Generic Assertions
expect(locator).toBeVisible() is a web-first assertion: internally, Playwright polls the DOM repeatedly, up to a default 5-second timeout, until the condition becomes true or the timeout expires, rather than checking once and immediately failing. This matters enormously for asynchronous UIs, where a success message might take a few hundred milliseconds to render after an API call resolves. By contrast, expect(value).toBe(x) — asserting on a plain JavaScript value rather than a locator — evaluates exactly once, immediately, with no retry logic at all, because there's no live DOM state to poll against.
Cricket analogy: expect(locator).toBeVisible() is like the third umpire reviewing replay angles for a set time until conclusive evidence appears, rather than ruling instantly off one blurry frame — a generic expect(value).toBe(x) is a single, immediate check, like an on-field umpire's instant call with no review.
Common Locator Assertions
Playwright ships a rich set of locator-scoped matchers, each targeting one precise fact about the DOM: toHaveText() and toContainText() check exact or substring text content, toHaveValue() checks a form input's current value, toHaveCount() checks how many elements a locator matches, toBeEnabled() and toBeChecked() check interactive and checked state, and toHaveAttribute() and toHaveClass() check a specific attribute or CSS class. All of these are web-first and auto-retry, so chaining several of them after a form submission gives you both precise diagnostics and resilience against timing.
Cricket analogy: toHaveText, toHaveValue, toHaveCount, toBeEnabled, and toHaveAttribute each check one specific dimension of the DOM, much like a scorecard tracks runs, wickets, overs, and extras as separate stat lines rather than one vague 'performance' number — a solid test chains the specific assertion that matches exactly what changed.
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByRole('status')).toHaveText('Profile saved successfully');
await expect(page.getByLabel('Email')).toHaveValue('user@example.com');
await expect(page.getByRole('listitem')).toHaveCount(3);
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page.getByRole('alert')).not.toBeVisible();Soft Assertions and Negation
expect.soft(locator).toHaveText('...') records a failure without immediately stopping the test, so a single test.step can accumulate multiple independent findings — a summary error, a highlighted field, a disabled button state — and report all of them together at the end of the test instead of stopping at the very first mismatch. A regular, 'hard' expect() throws immediately on failure and aborts the rest of the test. Every matcher also supports a .not modifier, such as expect(locator).not.toBeVisible(), which checks that the opposite condition holds and retries the same way until it's confirmed true or the timeout expires.
Cricket analogy: expect.soft() is like a match referee noting every minor over-rate or fielding infringement throughout the game without stopping play immediately, then issuing a full report at the end — a normal hard expect() is like an instant no-ball call that halts the over; .not.toBeVisible() checks the opposite condition, like confirming a batter is NOT given out.
Wrap related assertions in test.step('Verify saved profile fields', async () => { ... }) to group them meaningfully in the HTML report. This makes it much easier to see at a glance which logical part of the test failed, especially when combined with expect.soft() to collect multiple findings from one step.
Custom Timeouts and Polling with expect.poll()
Any locator assertion accepts an explicit { timeout } option, such as expect(locator).toBeVisible({ timeout: 10000 }), to extend the retry window beyond the 5-second default for a known-slow element. expect.poll(async () => { ... }, { timeout: 10000 }).toBe(x) generalizes this same retry-until-true behavior to any arbitrary async function — not just locators — letting you repeatedly poll a database record, a REST API endpoint, or a message queue until it reflects the expected state, which is invaluable for end-to-end tests that need to verify backend side effects triggered by a UI action.
Cricket analogy: expect(locator).toBeVisible({ timeout: 10000 }) extends the umpire's patience window for a tricky decision, like allowing extra replay time for a marginal stumping — expect.poll() goes further, letting you repeatedly query the official scoring API until the final total is confirmed, not just a DOM element.
Avoid the pattern expect(await locator.textContent()).toBe('Saved'). Calling textContent() up front takes a single, immediate snapshot of the DOM and defeats Playwright's built-in auto-retry entirely — if the text hasn't updated yet, the assertion fails outright instead of waiting. Use expect(locator).toHaveText('Saved') instead, which polls the locator directly.
- Web-first assertions like expect(locator).toBeVisible() auto-retry against the live DOM until the timeout expires.
- Generic assertions like expect(value).toBe(x) evaluate a captured value once, immediately, with no retry.
- toHaveText, toHaveValue, toHaveCount, toBeEnabled, toBeChecked, toHaveAttribute, and toHaveClass each verify one precise fact.
- expect.soft() collects multiple assertion failures without stopping the test immediately; hard expect() stops on first failure.
- The .not modifier checks the opposite condition and retries the same way as a positive assertion.
- expect.poll() generalizes retry-until-true polling to arbitrary async functions beyond locators, like APIs or databases.
- Avoid expect(await locator.textContent()).toBe(...) — it defeats auto-retry by snapshotting the DOM once.
Practice what you learned
1. What is the key advantage of expect(locator).toBeVisible() over expect(value).toBe(x)?
2. What is the main difference between expect.soft() and a regular hard expect()?
3. Why is expect(await locator.textContent()).toBe('Saved') considered an anti-pattern?
4. What does expect.poll() add beyond standard locator assertions?
Was this page helpful?
You May Also Like
Locators: getByRole, getByText, and CSS
Learn how Playwright locators find elements using role-based, text-based, and CSS selector strategies, and why role-based locators are the recommended default.
Auto-Waiting and Actionability Checks
Understand how Playwright automatically waits for elements to become actionable before interacting with them, eliminating most manual sleeps and flaky waits.
Interacting with Forms and Inputs
Master Playwright's methods for filling text inputs, selecting options, checking boxes, and uploading files in real-world forms.