skip to content

When is Playwright's locator.waitFor() still the right call rather than a retrying assertion?

level: middleimportance: should knowfreq 48%

answer

  1. Four states, one of them the default
  2. It returns at once when satisfied
  3. A matcher polls the same condition
  4. Preconditions here, expectations in assertions
  5. Detached is the precise removal state

basics

~20 s

Use locator.waitFor() when an element must reach a state but there is nothing to assert - a precondition, or before a raw read. Where the test would assert, a retrying matcher is better: it reports the actual value.

solid answer

~40 s

`locator.waitFor({ state })` blocks until the located element is `'attached'`, `'detached'`, `'visible'` (the default) or `'hidden'`, returning immediately if the condition already holds and throwing on the `timeout`. It overlaps heavily with `expect(locator).toBeVisible()` and friends, which retry the same way but produce a real assertion failure with the observed state. So the split is by intent: use `waitFor()` when the state is a **precondition** for something else - waiting for a saving overlay on the comment composer to reach `'hidden'` before you read text out of the form, or for a row to be `'detached'` after a delete before a follow-up step - and use a matcher whenever the state is the thing the test is checking. `waitFor()` is also strict, so it throws if the locator matches several elements.

code

typescript · 7 lines
typescript
const overlay = page.getByRole('status', { name: 'Saving comment' });

await page.getByRole('button', { name: 'Add comment' }).click();
await overlay.waitFor({ state: 'hidden', timeout: 10_000 });

const draft = await page.getByRole('textbox', { name: 'Comment' }).inputValue();
expect(draft).toBe('');

go deeper

for a junior

Know the four states and that visible is the default. In everyday tests you rarely need this call at all, because actions auto-wait and expect matchers retry, so reach for it only when a helper needs a precondition.

for a middle

Explain that the matcher and waitFor poll the same condition, and that the difference is the failure message and the report. Be able to name why display none counts as hidden, and how hidden differs from detached.

for a senior

Show where it genuinely belongs: before a raw textContent or inputValue read, in fixtures arranging state, and for detached after a delete. Flag defensive waitFor calls before actions as noise that hides the real condition.

for a principal

Own the boundary in the team's conventions: waits express preconditions, matchers express expectations, and reviews push a waitFor that reads like a check back into an assertion so failures stay diagnosable in reports.

`locator.waitFor()` waits until the element a locator resolves to satisfies its `state` option. If the element already satisfies it the call returns immediately; otherwise it polls until the `timeout` elapses and then throws. Because the locator is re-resolved on each check, it tolerates the element being re-rendered while waiting, which is the same property that makes locators safe to hold across a re-render. ## The four states | `state` | Satisfied when | Typical use on an issue tracker | |---|---|---| | `'attached'` | The node is present in the DOM | The comment composer has been mounted, before reading its value | | `'visible'` *(default)* | Non-empty bounding box and no `visibility: hidden` | The issue detail panel has opened | | `'hidden'` | Detached, or empty box, or `visibility: hidden` | The saving overlay has cleared | | `'detached'` | The node is not in the DOM at all | The deleted row is gone from the board | Two details trip people up. An element with `display: none`, or with no content at all, has an empty bounding box and therefore counts as **hidden, not visible**. And `'hidden'` is deliberately broader than `'detached'`: a node that has been removed satisfies `'hidden'` too, so `'hidden'` is the forgiving choice and `'detached'` the precise one. ## Why an assertion is usually better `expect(locator).toBeVisible()` polls the same condition on the same re-resolving locator, so it is not a question of one waiting and the other not. The difference is what happens on failure and what the code communicates. - A failing matcher says *expected visible, received hidden* and names the locator; a failing `waitFor()` says a timeout was exceeded, which reads as an infrastructure problem rather than a product one. - A matcher is a **checked expectation** in the report and shows as an assertion step in the trace; a `waitFor()` is a silent precondition. - Matchers cover far more conditions than the four states - text, value, count, enabled, editable, attribute - so most waits people write as `waitFor()` want a matcher anyway. - Soft assertions and custom matchers compose on top of `expect`; nothing composes on top of `waitFor()`. ## Where waitFor still earns its place 1. **Before a raw read.** `locator.textContent()` and `locator.inputValue()` wait only for the element to be attached, not for it to be settled. Waiting for the saving overlay on the comment composer to be `'hidden'` first makes the subsequent read meaningful. 2. **As a precondition with nothing to assert.** In a fixture or a helper that arranges state, the test's assertions come later; a `waitFor()` there says *set-up, not verification*, and keeps the report honest. 3. **For `'detached'` specifically.** Asserting that a node has left the DOM is awkward with matchers, whereas `waitFor({ state: 'detached' })` states it directly. 4. **When the timeout must differ from the assertion budget.** `waitFor({ timeout })` takes its own value, useful for one genuinely slow step without loosening the expect timeout for the whole suite. ## Gotchas - `waitFor()` obeys **strict mode**. If the locator matches more than one element it throws a strict mode violation rather than picking one, so scope the locator before waiting. - Waiting for `'visible'` and then acting is redundant: every action already performs its own actionability checks on the same element. - `waitFor()` returns nothing. Chaining a read after it is a separate call, and the element can change in between - which is precisely why the matcher-plus-action ordering is usually simpler. - Playwright 1.62 added `locator.waitForFunction()`, which waits for an arbitrary predicate evaluated with the matching element as its argument and re-resolves the locator on each retry. It fills the gap when the condition is real but none of the four states expresses it. The heuristic that survives review: **if the sentence you would write in the test report is an expectation, use a matcher; if it is a precondition, use `waitFor()`.**

  • What is the difference between waitFor({ state: 'hidden' }) and waitFor({ state: 'detached' })?
    `'detached'` requires the node to be absent from the DOM. `'hidden'` is satisfied by that too, and also by a node that is present with an empty bounding box or `visibility: hidden`. Use `'detached'` when removal is the point, `'hidden'` when disappearance in any form will do.
  • What happens if the locator passed to waitFor() matches three elements?
    It throws a strict mode violation rather than waiting on one of them. `waitFor()` follows the same one-match rule as actions, so narrow the locator - or opt out explicitly - before waiting.

saying these in an interview costs you the question

  • Calls waitFor before every click as a habit
  • Thinks waitFor is needed because assertions do not retry
  • Believes an element with display none counts as visible
  • Uses hidden when the test really requires removal
  • Assumes waitFor picks the first of several matches