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.