Playwright interview question
How does Playwright auto-waiting work, and when is it not enough?
Short answer
Before acting, Playwright waits for actionability checks (attached, visible, stable, receives events, enabled, editable as relevant). Web-first assertions retry until timeout. It is not enough for app-level state like background jobs or data that changes without a DOM signal.
Answer
Playwright waits before every action until the target element is actionable, and its web-first assertions retry until the expected state appears. Together, these remove most manual waits. Auto-waiting only knows about what is observable in the page, though, so it is not enough when the condition you care about lives in the backend, in a non-DOM value, or in a UI that looks ready before it really is.
Actionability checks
When you call an action on a locator, Playwright resolves the locator to exactly one element (strictness) and waits until the relevant checks pass:
| Check | Meaning | Applies to (examples) |
|---|---|---|
| Attached | element is in the DOM | all actions |
| Visible | non-empty bounding box, not visibility:hidden |
click, fill, hover, check |
| Stable | not animating; same bounding box for two consecutive animation frames | click, hover, check |
| Receives events | not obscured; the hit target at the click point is the element | click, hover, check |
| Enabled | not disabled (including via a disabled fieldset or aria-disabled) |
click, fill, check, selectOption |
| Editable | enabled and not readonly |
fill, clear |
If a check fails it re-tries until the action timeout (by default no separate limit, so bounded by the test timeout of 30 seconds) and then fails with a log explaining which check never passed. { force: true } skips the non-essential checks; using it routinely hides real problems such as an overlay covering a button.
Navigation has its own waiting: page.goto waits for the load event by default (configurable with waitUntil). For navigations triggered by a click, assert on the destination with await expect(page).toHaveURL(...) or use page.waitForURL(...).
Web-first assertions
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');
The assertion re-checks until it passes or the expect timeout (5 seconds by default) runs out. Snapshot reads such as await locator.isVisible() or await locator.textContent() do not wait.
When it is not enough
1. State that has no DOM signal yet. An order is placed, but fulfilment runs as a background job. The UI shows “Order received” instantly; the thing you want to verify happens seconds later. Poll the source of truth:
await expect.poll(async () => {
const res = await request.get(`/api/orders/${orderId}`);
return (await res.json()).status;
}, { intervals: [500, 1_000, 2_000], timeout: 30_000 }).toBe('FULFILLED');
2. Several conditions that must hold together, or a check that needs a retry of an action:
await expect(async () => {
await page.getByRole('button', { name: 'Refresh' }).click();
await expect(page.getByTestId('report-status')).toHaveText('Ready', { timeout: 1_000 });
}).toPass({ timeout: 30_000 });
3. The UI is visible before it is interactive. For example, server-rendered HTML before hydration: the button is visible, enabled and stable, the click “succeeds”, but no handler is attached yet. Auto-waiting cannot know. Wait for a signal that the app is ready (an element that only renders after hydration, or a response the click depends on), and raise it with developers as a testability issue.
4. Waiting for a specific network response before asserting on data derived from it:
const saved = page.waitForResponse((r) => r.url().endsWith('/api/profile') && r.request().method() === 'PUT');
await page.getByRole('button', { name: 'Save' }).click();
expect((await saved).ok()).toBeTruthy();
Start waiting before the action that triggers the request.
5. Your assertion is the wrong one. Checking for a spinner to vanish passes instantly if the spinner has not appeared yet. Assert on the end state (the data is shown) instead.
What not to do
page.waitForTimeout(3000) is a fixed sleep: too short in CI, wasteful locally. The documentation discourages it outside debugging.
What interviewers look for
- The actionability checks, roughly correct, and that they apply per action type.
- Retrying web-first assertions versus non-retrying snapshot reads.
- Specific limits: backend state, hydration, multi-step conditions.
- Correct tools:
expect.poll,toPass,waitForResponse, never fixed sleeps.
A strong answer covers
- Lists actionability checks performed before actions
- Mentions web-first assertions retrying until timeout
- Identifies limits: app/back-end state with no UI signal
- Uses explicit event waits like waitForResponse instead of fixed sleeps
- Mentions timeouts: action, expect, test, navigation