beginner19 min

Auto-Waiting and Web-First Assertions

The specific set of checks Playwright runs before every action, and why an expect() assertion in Playwright is fundamentally different from a plain equality check.

What you'll learn

  • List the actionability checks Playwright runs before performing an action
  • Explain why a web-first assertion retries instead of failing immediately
  • Distinguish a genuine flaky-timing bug from one auto-waiting already correctly handles

Prerequisites

Explanation

Before performing an action like .click(), Playwright runs a specific, documented set of actionability checks on the target element, retrying the whole check sequence until they all pass (or a timeout elapses): the element must be attached to the DOM, visible (has non-zero size, not display: none), stable (not still animating/moving between two consecutive frames), able to receive events (not obscured by another element on top of it), and enabled (not disabled). This is what "auto-waiting" concretely means — it is not a single generic sleep before every action, it's this specific sequence of real conditions, re-checked repeatedly until they hold or Playwright gives up.

A web-first assertion (await expect(locator).toBeVisible(), await expect(locator).toHaveText("Done")) is built on the same retrying mechanism: unlike a plain if (text === "Done") check, which evaluates once, immediately, expect(locator).toHaveText(...) polls the locator repeatedly until the condition becomes true or a timeout elapses. This is precisely why Playwright assertions must always be awaited — the expect call itself is asynchronous, actively retrying, not an instant true/false check — and forgetting the await is a genuine, common bug: without it, the assertion starts its retry loop but the test doesn't wait for the result, so a failure can be silently missed or reported in a confusing, disconnected way.

The practical consequence: auto-waiting already correctly handles most "the button wasn't ready yet" timing issues that would require an explicit manual wait in an older automation tool — you generally do not need to add your own sleep or wait call before a Playwright action or assertion. A test that's still flaky despite this should be treated as a real signal, not "just add another wait": the remaining common causes are a genuine race condition in the application itself (a request that hasn't resolved when the UI claims it has), a locator matching more than one element ambiguously, or a network response being asynchronous in a way the current assertion doesn't actually wait for (covered in this course's network module) — auto-waiting solves the "element not ready yet" class of problem, not every possible source of test flakiness.

Example

Modeling the actionability-check retry loop -- the real mechanism behind auto-waiting, not a generic sleep.

function isActionable(elementState) {
  return (
    elementState.attached &&
    elementState.visible &&
    elementState.stable &&
    elementState.receivesEvents &&
    elementState.enabled
  );
}

async function waitForActionable(getElementState, timeoutMs = 5000, intervalMs = 50) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (isActionable(getElementState())) return true;
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
  return false; // timed out -- this is what a real Playwright timeout error models
}

// Simulating an element that becomes actionable after a short delay:
let becameReady = false;
setTimeout(() => { becameReady = true; }, 200);
waitForActionable(() => ({ attached: true, visible: becameReady, stable: true, receivesEvents: true, enabled: true }))
  .then((result) => console.log("actionable within timeout:", result)); // true

Try it yourself

Change the element to never become visible, and observe waitForActionable correctly time out (false) instead of hanging forever.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write isActionable(state) implementing the five actionability checks from this lesson exactly (attached, visible, stable, receivesEvents, enabled -- all must be true).

Checks: all five conditions true means actionable · not visible means not actionable · disabled means not actionable

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write pollUntilTrue(checkFn, maxAttempts) modeling a web-first assertion's retry loop: call checkFn() repeatedly (up to maxAttempts times), returning true as soon as it returns true, or false if it never does within maxAttempts calls. checkFn takes no arguments and may return a different result on each call (simulating a condition that becomes true over time).

Checks: stops polling as soon as the condition becomes true · returns false after exhausting attempts on a never-true condition · returns true immediately when the first check already passes

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Forgetting `await` before an `expect(locator)...` assertion -- the assertion is asynchronous and actively retrying; without await, the test doesn't actually wait for its result, which can hide real failures or produce confusing, disconnected error reports.
  • Adding a manual `await page.waitForTimeout(1000)` before every action 'just in case' -- auto-waiting already handles the 'element not ready yet' class of problem; an extra fixed sleep only slows the suite down without fixing genuine flakiness.
  • Treating persistent flakiness as something a longer wait will eventually fix -- if auto-waiting isn't resolving it, the real cause is usually a genuine race condition, an ambiguous locator, or an async network response the current assertion doesn't actually account for.

Knowledge check

Knowledge check

1. What does Playwright's auto-waiting concretely check before performing an action like .click()?
2. Why must a Playwright web-first assertion always be awaited?
3. A test remains flaky even though the element in question is genuinely visible and enabled well before the assertion runs. What does this most likely indicate?

Takeaway

Auto-waiting is a specific, retried set of actionability checks, not a generic sleep, and web-first assertions poll rather than check once — both must genuinely be awaited, and persistent flakiness despite them is a real signal pointing to an actual bug, not a cue to add more waiting.

Summary

Before an action, Playwright checks attached/visible/stable/receives-events/enabled, retrying until they hold or timeout. Web-first assertions (expect(locator)...) poll rather than check once, and must be awaited. Auto-waiting solves 'not ready yet' timing issues; other flakiness causes need real diagnosis.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.