In Cypress, what does `.click()` check about an element before it fires the click?
answer
- The action waits for the element
- A fixed list, re-run until it passes
- Scroll, hidden, disabled, detached, readonly
- Bounded by defaultCommandTimeout, not a sleep
- The error names the check that failed
basics
~20 sCypress re-runs a fixed readiness list before every action: it scrolls the element into view, then requires it to be attached, visible, enabled, not readonly, not animating and not covered at the point the event will land.
solid answer
~40 s`.click()` does not fire until the element passes Cypress's actionability checks, and Cypress re-runs that whole list — re-querying the subject each time — until it passes or `defaultCommandTimeout` (`4000` ms) runs out. Cypress scrolls the element into view using `scrollBehavior`, then checks it is not hidden, not `disabled`, not detached from the document, not readonly (only `.type()` and `.clear()` ask for that), not animating, and not covered at the point the event will land — nudging the page when a `position: fixed` or `sticky` element is in the way. Only then does it compute the coordinates, the element's centre unless you pass `position`, and fire the events. A failure names the check that never passed, so the error itself tells you what to fix.
code
javascript · 7 linescy.visit('/catalogue?q=dune')
cy.contains('[data-cy=book-row]', 'Dune')
.find('[data-cy=borrow]')
.click({ timeout: 10000, scrollBehavior: 'center' })
cy.get('[data-cy=loan-count]').should('have.text', '1')go deeper
Be ready to name the list without prompting: scroll into view, then not hidden, disabled, detached, readonly, animating or covered, then fire at the coordinates.
Explain that the whole list is re-run on every retry against a freshly queried subject, and that the budget is defaultCommandTimeout rather than a fixed pause.
Show that you read the specific failure text rather than adding force, and that you know which check produced which message.
An interviewer at this level wants the policy: which of these checks a suite is allowed to switch off, where that decision is recorded, and how it is reviewed.
## Why an action command waits at all Cypress runs your spec inside the browser, in the same event loop as the application, and an **action command** simulates the events a real user's interaction would produce. Firing those events at an element a user could not reach would prove nothing, so before `.click()`, `.dblclick()`, `.rightclick()`, `.type()`, `.clear()`, `.check()`, `.uncheck()`, `.select()`, `.trigger()` or `.selectFile()` does anything, Cypress asks whether the element is in a state that would accept the interaction. That question is answered by a **fixed list of checks and actions** that Cypress calls **actionability** — always the same list, in the same order, for every action command. ## The list Cypress runs before the event | Check or action | What it means | Typical cause of a failure | |---|---|---| | Scroll the element into view | Uses `scrollBehavior`, `'top'` by default | not a gate — Cypress always scrolls | | Not hidden | The element is visible by the configured `visibilityStrategy` | a results panel collapsed with `display: none` | | Not disabled | The `disabled` property is not `true` on a form control | the Borrow button still `disabled` while availability loads | | Not detached | The element is still inside the application's `document` | the book row re-rendered under the command | | Not readonly | Only `.type()` and `.clear()` ask for this | the search box locked while a request is in flight | | Not animating | Movement between position samples stays under `animationDistanceThreshold` | a drawer still sliding open | | Not covered | Nothing else sits at the point the event will land on | a cookie banner over the button's centre | | Scroll again if still covered | Nudges the page past a `fixed` or `sticky` coverer | a sticky catalogue header pinned to the top | | Fire at the coordinates | The centre of the element unless you pass `position` | — | Two entries on that list are worth calling out. **Cypress scrolls every time**, even when the element was already on screen, so that a command behaves identically on every run. And **coverage is judged at a single point**, not across the whole box, so a button that is half under an overlay still fails when its centre is the covered part. ## It re-runs the list; it does not sleep The list is not evaluated once. Cypress evaluates it, and if any check fails it evaluates the whole thing again from the top: 1. The subject is re-queried from the chain that produced it, so a row the app has just re-rendered can be picked up on a later attempt. 2. Every check runs again in order — a check that passed a moment ago is not trusted. 3. The moment all of them pass, the events fire **synchronously**, so nothing can change between the last check and the event. The budget for that loop is `defaultCommandTimeout`, `4000` ms by default, or the `{ timeout }` you pass to the individual command. When it runs out, the error you see is the last check that failed, which is why the message is usually specific rather than a bare timeout. ## Reading the failure Each check has its own wording, and the wording is the diagnosis: - `failed because this element is not visible` — with a reason line explaining which rule hid it. - `failed because this element is disabled` — the `disabled` property, not a class or `aria` attribute. - `failed because this element is readonly` — only ever from `.type()` or `.clear()`. - `could not be issued because this element is currently animating` — with the three options that relax it. - `failed because this element ... is being covered by another element` — and it prints the coverer. - `failed because this element ... has CSS pointer-events: none` — a different rule from coverage. ## What the list deliberately leaves out - **Queries never scroll.** `cy.get()`, `.find()`, `.first()` and friends locate elements without moving the page; only action commands scroll. - **`opacity: 0` is actionable.** An element at zero opacity fails a `should('be.visible')` assertion, but an action command will still interact with it. - **`disabled` only counts on form controls.** A `disabled` attribute on a `<div>` does not stop a real user, so it does not stop Cypress either. - **Readonly is not checked for clicks.** A readonly input is perfectly clickable; only typing commands request that check. ## What changed in Cypress 16 As of Cypress 16 the visibility check delegates to the browser's own `Element.checkVisibility()` API under `visibilityStrategy: 'modern'`, which is the default, and `scrollBehavior` additionally accepts a per-axis `{ block, inline }` object as well as the whole-alignment values `'start'`, `'end'`, `'center'` and `'nearest'`. The list itself is unchanged: the same checks run, in the same place, before every action.
- Which Cypress commands run this readiness list?The action commands: `.click()`, `.dblclick()`, `.rightclick()`, `.type()`, `.clear()`, `.check()`, `.uncheck()`, `.select()`, `.trigger()` and `.selectFile()`. Queries such as `cy.get()` and `.find()` do not — they locate elements without touching the page, which is why they never scroll anything into view.
- Does the readiness list check that an element is `opacity: 0`?No, not for actions. An element at zero opacity is treated as actionable and receives the events, even though a `should('be.visible')` assertion on it fails. The two rules deliberately differ: an assertion is asking what the user can see, an action is asking what the user could reach.
saying these in an interview costs you the question
- Cypress just sleeps before each click
- The checks run once, not on every retry
- cy.get() also scrolls the element into view
- A disabled attribute on a div blocks the click
- An opacity: 0 element can never be clicked