11.5 — Actions
Locators find; actions DO. And the deep fact about Playwright’s actions is what happens before each one: a four-point checklist — attached, visible, stable, enabled — retried until the element is genuinely ready for a human-grade interaction. 7.8 taught you why that gap exists; today you watch the tool close it.
await page.getByLabel("Email").fill("ada@shop.com");
await page.getByRole("button", { name: "Log in" })
.click();
await page.getByLabel("Qty").press("ArrowUp");
await page.getByLabel("Size").selectOption("L");
await page.getByLabel("Accept terms").check();
await page.getByText("Menu").hover();
// before EVERY action, the checklist:
// attached? (in the DOM — 7.1)
// visible? (rendered — 7.8)
// stable? (not mid-animation)
// enabled? (not disabled)
// …retried until all tick, or timeoutEach action line is one command across 11.1’s bridge — and on the browser side it becomes REAL input events, not JavaScript fakery. A Playwright click is a trusted click: the same event sequence a physical mouse produces.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
The checklist is per-action-appropriate: click additionally checks the element isn’t covered by something else (it must actually receive events at the click point — a cookie banner on top blocks it, exactly as it blocks a human); fill checks the target is editable. The four you learned are the core; the variations all serve the same principle.
Escape hatches exist and are honestly labeled: click({ force: true }) skips the checks, and dispatchEvent fires synthetic events. Treat both as confessions — “my test clicks something a user couldn’t” — occasionally right (a deliberately hidden but functional control), usually a locator or app bug wearing a workaround.
Timing precision: the checklist runs against the element the locator resolves at action time, and re-resolves during retries — so even if the page re-renders and the node is replaced (frameworks do this constantly), the action follows the DESCRIPTION, not a dead reference. 11.4’s locator-not-element design pays exactly here.
Job note: “element is not stable” failures usually mean CSS animations — real suites often disable animations globally for tests (a config/use option) both for speed and stability. And “not visible” with a correct locator often means you found a SECOND, hidden copy of the element (mobile + desktop nav rendering twice) — strict mode’s cousin; narrow the locator’s scope (11.4’s chaining).
⌨️ build the actionability gate
Model the checklist that runs before every real click: given an element’s state, either clear it for action or name EXACTLY which check blocked it — the same name the real TimeoutError would carry.
requirements:
- Write
canAct(el): checkattached,visible,stable,enabledIN THAT ORDER; return the FIRST failing check’s name, or"actionable"when all four hold (early returns keep it clean). - Write
click(el): consult the gate — printclick performedwhen actionable, otherwiseblocked: NAME. - Run it on a ready button (
{ attached: true, visible: true, stable: true, enabled: true }), then on a hidden one (same butvisible: false), then on a disabled one (visible butenabled: false).
when you press RUN, the console must show exactly:
✏️ Quick check 1
Name the four actionability checks, in order.
✏️ Quick check 2
A button exists in the DOM but has display:none. Does .click() eventually click it, or time out?
✏️ Quick check 3
You want the field to contain exactly "mug" regardless of what was there before. Which verb — fill or press?
🗣️ Now teach it back
Explain to a friend what happens between “await button.click()” and the click landing: the bridge, the trusted events, the four-point checklist (with each check’s Phase-7 root), and what the error says when a check never passes.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.