IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Beginner·8 / 16

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

Before every click, Playwright runs this checklist automatically

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:

  1. Visible — the element has a non-zero size and isn't hidden
  2. Stable — not currently animating or transitioning
  3. Receives events — not covered by another element like a modal or spinner
  4. 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.

ts
// Пропустити перевірку "отримує події" для кнопки під оверлеєм
await page.getByRole('button', { name: 'Зберегти' }).click({ force: true })

// Те саме для fill
await page.getByLabel('Email').fill('test@example.com', { force: true })

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.

ts
// Чекає поки кнопка стане видимою (наприклад після завантаження)
await expect(page.getByRole('button', { name: 'Готово' })).toBeVisible()

// Чекає поки таблиця завантажить всі 12 рядків
await expect(page.getByRole('row')).toHaveCount(12)

// Чекає поки URL зміниться після редиректу
await expect(page).toHaveURL('/dashboard')

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:

  1. Elements with display: nonenot visible
  2. Elements with zero width or zero height — not visible
  3. Elements with opacity: 0are 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:

  1. It's a <button>, <input>, <select> or <textarea> with the [disabled] attribute
  2. It's inside a <fieldset disabled>
  3. 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.