Auto-waiting
Tests that fail only on CI are almost always a timing issue. Playwright's auto-waiting solves most of them without a single sleep() — it checks that an element is ready before every action.
How auto-waiting works
Flaky tests that pass locally but fail on CI are usually a timing problem — the test clicks something before it's ready. Auto-waiting is Playwright's answer: before every action, it silently runs a checklist. No manual waits, no arbitrary sleeps.
For locator.click(), Playwright checks four things in sequence:
- Visible — the element has a non-zero size and isn't hidden
- Stable — not currently animating or transitioning
- Receives events — not covered by another element like a modal or spinner
- Enabled — not disabled via HTML attribute or ARIA
If any check fails, Playwright waits and retries until the default 30-second timeout expires. When it does, you get a descriptive TimeoutError — not a mystery crash.
Which checks run for which actions
Not every action needs all four checks. fill() doesn't care about stability — it just needs the field to be visible, enabled and editable. hover() doesn't require the element to be enabled. Here's the reference:
| Action | Visible | Stable | Receives Events | Enabled | Editable |
| :- | :-: | :-: | :-: | :-: | :-: |
| locator.check() | Yes | Yes | Yes | Yes | - |
| locator.click() | Yes | Yes | Yes | Yes | - |
| locator.dblclick() | Yes | Yes | Yes | Yes | - |
| locator.tap() | Yes | Yes | Yes | Yes | - |
| locator.hover() | Yes | Yes | Yes | - | - |
| locator.dragTo() | Yes | Yes | Yes | - | - |
| locator.screenshot() | Yes | Yes | - | - | - |
| locator.fill() | Yes | - | - | Yes | Yes |
| locator.clear() | Yes | - | - | Yes | Yes |
| locator.selectOption() | Yes | - | - | Yes | - |
| locator.selectText() | Yes | - | - | - | - |
| locator.blur() | - | - | - | - | - |
| locator.press() | - | - | - | - | - |
Bypassing checks with force
If you need to click an element that's technically covered by an overlay, pass { force: true }. This skips the "receives events" check and fires the click directly at the element's coordinates. Use it sparingly — if you're reaching for force: true often, the test is probably fighting the UI instead of working with it.
Assertions auto-wait too
The same retry logic applies to expect() assertions. If the condition isn't met yet, Playwright keeps retrying until the assertion passes or the timeout expires. You rarely need explicit waits before assertions — just write what you expect and let Playwright handle the timing.
What counts as visible
An element is visible when it has a non-zero bounding box and its computed style is not visibility: hidden. Three edge cases worth knowing:
- Elements with
display: none— not visible - Elements with zero width or zero height — not visible
- Elements with
opacity: 0— are visible (they have a box, just transparent)
What counts as stable
An element is stable when its bounding box hasn't moved for at least two consecutive animation frames. In practice this means Playwright waits for CSS transitions and animations to finish before acting. If a dropdown slides in from the top, Playwright clicks only after it stops moving — no manual waitForSelector needed.
What counts as enabled
An element is disabled — and Playwright won't interact with it — in three cases:
- It's a
<button>,<input>,<select>or<textarea>with the[disabled]attribute - It's inside a
<fieldset disabled> - It has an ancestor with
[aria-disabled=true]
Practical case: the Submit button is disabled until the form is valid. Playwright waits for validation to pass before clicking — the test just works.
What counts as receiving events
Even a visible and enabled button might not receive clicks if something is sitting on top of it — a loading spinner, a modal backdrop, or a cookie banner. Playwright checks that the element at the exact click coordinates is the target (or a descendant of it), not an interceptor.
Real example: you click 'Save' but a loading overlay is still showing. Playwright waits for the overlay to disappear, then clicks. Without this check the click would hit the overlay silently and nothing would happen.