Q11 of 42 · Playwright

Explain auto-waiting in Playwright.

PlaywrightMidplaywrightauto-waitfundamentalsmid

Short answer

Short answer: Before every action, Playwright waits for the element to be **actionable** — attached, visible, stable (not animating), enabled, and receiving events. `expect` matchers auto-retry until they pass or time out. You almost never need explicit `waitFor` calls.

Detail

Auto-waiting is the feature that makes Playwright tests look declarative. Internally, every action (click, fill, check) runs a multi-step actionability check before performing the action:

  1. Attached to the DOM.
  2. Visible (non-zero size, not display: none).
  3. Stable (no animation in flight; bounding box hasn't moved between two consecutive frames).
  4. Receiving events (no transparent overlay intercepting clicks).
  5. Enabled (not disabled or aria-disabled).

If any check fails, Playwright keeps polling until the test timeout expires. So page.getByRole('button', { name: 'Submit' }).click() doesn't need a separate waitFor — it'll wait up to ~30s for the button to become clickable.

Assertions auto-retry too. expect(locator).toHaveText('Welcome') polls the locator's textContent until it matches or the timeout expires. No waitForText needed.

When auto-wait doesn't help:

  • Network completion. The element exists but the data hasn't loaded. Use page.waitForResponse(...) or just assert on the post-load text — that asserts will retry.
  • State that's only correct after server confirmation. expect(...).toHaveText on a value that requires a backend round-trip will wait for the round-trip implicitly.
  • Genuinely tricky timing (third-party widgets, race conditions). page.waitForFunction(...) for custom JS conditions.

The anti-pattern to avoid: page.waitForTimeout(2000) (a hard sleep). Playwright supports it for debugging but it's a smell — almost always replaceable with an actionability check or assertion-driven retry.

The senior signal: knowing the actionability checks by name and being able to reason about why a click might silently wait (overlay? animation?).

// EXAMPLE

// ✅ Auto-wait handles this — no explicit wait needed
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');

// ✅ Wait for a specific network response when needed
await Promise.all([
  page.waitForResponse((res) => res.url().includes('/api/cart') && res.ok()),
  page.getByRole('button', { name: 'Add to cart' }).click(),
]);

// ❌ Anti-pattern — masks the real issue
await page.waitForTimeout(2000);
await page.click('[data-test=submit]');

// MODEL ANSWER

Auto-waiting means that before Playwright performs any action — a click, a fill, a check — it runs a series of actionability checks: is the element attached to the DOM, visible, stable (meaning the animation has settled), able to receive pointer events without an overlay blocking it, and enabled? Only when all five pass does the action execute. If any check fails, Playwright keeps polling until the timeout expires and then fails with a message saying exactly which check did not pass. Assertions auto-retry too: expect(locator).toHaveText produces the same polling behaviour until the text matches or times out, so you almost never need explicit waitFor calls. The one anti-pattern worth flagging is page.waitForTimeout, which is a hard sleep. Every time you are tempted to add one, the real question is what is blocking actionability — usually an animation that has not settled or an overlay that is still visible. Auto-waiting would handle both if you remove the sleep and let Playwright tell you what is actually wrong.

// WHAT INTERVIEWERS LOOK FOR

Naming the actionability checks (attached, visible, stable, receiving events, enabled), the assertion-retry behaviour, and the anti-pattern of `waitForTimeout`.

// COMMON PITFALL

Adding `waitForTimeout` because 'a click sometimes misses' — that masks an animation or overlay that the actionability check would have surfaced.