Q11 of 42 · Playwright
Explain auto-waiting in Playwright.
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:
- Attached to the DOM.
- Visible (non-zero size, not
display: none). - Stable (no animation in flight; bounding box hasn't moved between two consecutive frames).
- Receiving events (no transparent overlay intercepting clicks).
- Enabled (not
disabledoraria-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(...).toHaveTexton 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]');