The Five Checks
Every timeout your suite has ever produced was one named check that never passed. Playwright knows which one. This lesson is about reading that answer instead of retrying past it.
~15 min · prerequisite for trace-driven diagnosis · siblings C2, C3
1. Correction first
I asked you to name the checks and said there were six. There are five. The sixth thing people count is attached — the locator resolving to an element in the DOM at all — which is a precondition to the checks rather than one of them. Worth knowing because the two failure modes read completely differently in a log.
Attached failing means the element was never there — your locator is wrong, or the app never rendered it. A check failing means the element is there and Playwright refuses to touch it yet. The first is a test bug; the second is usually a race, and occasionally a real product bug.
2. The five
| Visible | Non-empty bounding box, and not visibility:hidden. Note what is absent from that definition: opacity:0 counts as visible, and so does an element scrolled far off-screen or sitting under another one. |
| Stable | Same bounding box for at least two consecutive animation frames. This is the check your reducedMotion: 'reduce' config exists to satisfy — an element mid-transition never holds still for two frames. |
| Receives events | The element is the hit target of the pointer event at the action point. Playwright picks a point, asks the browser what is actually at that point, and compares. This is the answer to my question about the Moveable overlay. |
| Enabled | Not disabled. Applies to form controls and anything with the attribute. |
| Editable | Enabled and not readonly. |
The answer to Q1, stated plainly: a Moveable handle sitting between the cursor and your target layer fails receives events. The layer is visible, stable and enabled — the browser simply reports a different element as the hit target. Playwright then waits, hoping the overlay goes away, and eventually reports a timeout that names the intercepting element. That name is the most useful string in the whole failure, and it is the one people skip past on their way to adding a retry.
3. Not every action runs every check
This table is the part worth actually memorising, because it tells you which failures are possible for a given call — and therefore which hypotheses to discard immediately.
| Action | Visible | Stable | Receives | Enabled | Editable |
|---|---|---|---|---|---|
| click, dblclick, tap, check, uncheck, setChecked | ✓ | ✓ | ✓ | ✓ | — |
| hover, dragTo | ✓ | ✓ | ✓ | — | — |
| screenshot | ✓ | ✓ | — | — | — |
| fill, clear | ✓ | — | — | ✓ | ✓ |
| selectOption | ✓ | — | — | ✓ | — |
| selectText | ✓ | — | — | — | — |
| scrollIntoViewIfNeeded | — | ✓ | — | — | — |
| press, pressSequentially, focus, blur, dispatchEvent, setInputFiles | — | — | — | — | — |
Read the amber row again. press and dispatchEvent run no actionability checks at all. They fire into whatever state the app is in. Your pressHotkey helper is built on a dispatched synthetic event — deliberately, to dodge Chromium's OS-shortcut interception — which means it inherits zero of Playwright's auto-waiting. That is precisely why activateTool has to wait for data-active-tool by hand. The convention in your e2e/CLAUDE.md is correct, and this row is the reason it has to exist.
4. Recognition: which check?
Five failure descriptions. Click a symptom, then click the check it names. Immediate feedback; a wrong pair clears.
5. Bug hunt
Click the line that will produce an actionability failure — or the "no bug" button. One of these four is clean.
6. The diagnostic you are not using
Playwright has an option that runs the checks and then declines to perform the action:
await layer.click({ trial: true })
It is the cleanest way to answer one question: was the element ever actionable, or did my action itself change the world? A trial that passes where a real click fails tells you the click's own side effects are the problem — a re-render, a focus change, a Moveable overlay that appears because of the selection the click caused. That distinction is invisible from a normal timeout.
The other option, force: true, disables the non-essential checks and dispatches anyway. Worth knowing precisely what it costs: it does not make the app ready, it makes Playwright stop asking. Every force in a suite is an un-run check, and the hit-target check is the one it usually silences — so a forced click on a canvas can land on the overlay and the test still goes green.
Worth checking in your repo this week. Grep e2e/ for force:. Every hit is a place where a canvas gesture may be landing on a Moveable able instead of the layer, with no failure to show for it. That is not a flake — it is a test that passes for the wrong reason, which is strictly worse.
7. Free recall
Without scrolling: name the five checks, and name the one that press runs.
Visible · Stable · Receives events · Enabled · Editable
press runs none of them. Neither does dispatchEvent, focus, blur or setInputFiles. If you got that one, you have the part that actually changes how you write editor tests.
8. What this buys you this week
- Next timeout in CI: open the trace, find the last action, and name the check before forming any other hypothesis. The failure text already tells you.
- Grep
e2e/forforce:and decide, per hit, whether it is silencing the hit-target check on a canvas. - Add one sentence to
e2e/CLAUDE.md: dispatched hotkeys run no actionability checks, therefore every hotkey helper owns an explicit postcondition wait. The rule is already followed — it is not yet justified in writing.
STATUS · MISSION · RESOURCES · source: Playwright — Auto-waiting