automationplaywright

Playwright auto-wait: why you don't need explicit waits

If you came to Playwright from Selenium — your first instinct is to write await page.waitForSelector(...) before every action. 90% of the time it’s redundant work: Playwright already waits for you. But many don’t know this and drag old habits along.

What auto-wait is

Every action on a Locator (click, fill, type, check) runs a series of actionability checks before executing — verifying that the element:

  • is attached to the DOM
  • is visible
  • is stable (not moving, not animating)
  • receives events (not covered by another element)
  • is enabled

If the condition isn’t met — Playwright waits up to 30 seconds by default, then fails. No waitFor needed.

What breaks auto-wait

— Using the old page.click('selector') instead of page.locator('selector').click(). Works, but without the full set of actionability checks. Always go through Locator API.

— A custom overlay (loading spinner with pointer-events: none) covers the button, and Playwright “thinks” the element is clickable. Auto-wait doesn’t help — be explicit: await page.locator('.spinner').waitFor({ state: 'hidden' }).

— Animation makes the element “unstable” for more than 30 seconds. Solution: animation: none !important in the test environment, or a local { timeout: 60_000 }.

What to use instead of waitFor

expect(locator).toBeVisible() — built-in retry up to timeout. Cleaner than waitForSelector. — expect(locator).toHaveText('...') — waits until the text matches. No manual polling. — page.waitForResponse(/api\/data/) — wait for a specific network response. — page.waitForURL('/dashboard') — wait for navigation.

Antipatterns

page.waitForTimeout(2000) — this is Thread.sleep in Playwright clothing. In CI it’s always either too short or too long. Use only for debugging, remove before commit.

❌ Custom poll loop via setInterval — Playwright does this natively.

expect(await locator.textContent()).toBe(...) — this is a synchronous check without retry. Replace with await expect(locator).toHaveText(...).

What to do right now

✅ Grep the project for waitForSelector and waitForTimeout( — these are candidates for removal or replacement.

✅ Switch to web-first assertions (expect(locator)) wherever you have expect(await locator.x()).

✅ Enable trace: 'on-first-retry' in playwright.config.ts — gives offline debugging with a timeline of every auto-wait.

More: Playwright — Auto-waiting, Best Practices.