In Cypress, why does a `.click()` report the event firing on the button's inner span?
answer
- The command aims at a point
- Whatever is topmost gets hit
- A child over its parent is fine
- The Command Log names the element
- Focus goes to the focusable ancestor
basics
~20 sCypress fires at a coordinate, not at a node. The click lands on whatever element is topmost at that point, so a child covering the button's centre gets the event, and the Command Log names it. Focus still goes to the button.
solid answer
~40 sA Cypress action does not call a method on your element; it dispatches real events at a screen coordinate — the centre of the subject by default. If a descendant sits over that point, and inside a button an icon or a `<span>` label usually does, then that descendant is what a real user's pointer would hit, so that is where Cypress fires. Cypress treats a child covering its parent as fine, notes the descendant it used in the Command Log, and shows a red hitbox at the coordinate. Bubbling means a handler bound on the button still runs, but `event.target` is the span. Focus is separate: it goes to the first focusable ancestor, so the button gets it — unless `mousedown` had its default prevented, in which case nothing receives focus.
go deeper
Know that a Cypress click is aimed at a point on the element, usually its centre, and that the Command Log tells you which element the event actually reached.
Explain why a descendant covering its own parent is allowed while a separate overlay is not, and what that means for a handler reading event.target.
Diagnose the pair of symptoms this produces: a click swallowed by a stacked child, and a click that never moves focus because the app cancelled mousedown.
Decide how much a suite should aim at coordinates at all, since position arguments couple tests to layout that a redesign will move underneath them.
Cypress action commands are deliberately not `element.click()`. Cypress works out a coordinate, then dispatches the events a browser would dispatch at that coordinate — `pointerover`, `mouseover`, `pointermove`, `mousemove`, `pointerdown`, `mousedown`, `pointerup`, `mouseup`, `click`. Which node receives them is therefore a question about geometry, not about the selector you wrote. ## Where the coordinate comes from By default the coordinate is the **centre** of the subject's bounding box. You can move it two ways, and both change which descendant can end up under the point: - A named position: `cy.get('[data-cy=borrow]').click('topLeft')`. The valid names are `topLeft`, `top`, `topRight`, `left`, `center`, `right`, `bottomLeft`, `bottom` and `bottomRight`. - Explicit pixels from the element's top-left corner: `cy.get('[data-cy=borrow]').click(15, 40)`. Once the coordinate is fixed, Cypress asks the document which element is topmost there. That is exactly the question a browser asks when a real pointer is over the page. ## Why a child is allowed to be in the way Cypress refuses to act on an element that some *other* element covers — an open modal over the catalogue, a sticky header over a row. A **descendant** covering its own ancestor is a different case, and Cypress treats it as fine, because that is how markup normally works: ```html <button data-cy="borrow"> <i class="icon-book"></i> <span>Borrow</span> </button> ``` The `<span>` occupies the button's centre. A real reader clicking "Borrow" hits the span too; the button reacts because the event bubbles. So Cypress fires at the span and records that it did — the Command Log entry for the click names the descendant the event was issued on, and pinning the command shows the red hitbox at the coordinate. ## What that changes in your application code For most applications, nothing: the handler is bound on the button, `click` bubbles, and the borrow request goes out. It matters in three situations. 1. **A handler that reads `event.target`.** `if (e.target.tagName === 'BUTTON')` is false when the span was hit, so the handler silently does nothing and your test fails for a reason that has nothing to do with Cypress. `e.currentTarget`, or `e.target.closest('button')`, is the fix in the application. 2. **A listener bound on the child.** If the icon carries its own listener, it runs, and it runs before the button's — which can matter if it stops propagation. 3. **A test asserting on the event's target.** A spy recording the target sees the span. Assert on behaviour rather than on the event object where you can. ## Focus is a separate rule Firing at the span does not mean the span takes focus. Cypress gives focus to the **first focusable element** in the chain, which for the markup above is the button — the same thing a browser does when you click a label inside a button. When no focusable ancestor is found, the window receives focus instead, again matching real browser behaviour. And if your application calls `preventDefault()` on `mousedown`, nothing receives focus at all, because the spec says a cancelled `mousedown` does not move focus. That is the usual cause of "the click worked but the field never became active". ## Reading the evidence When a click seems to have gone somewhere unexpected, the Command Log is the place to look rather than the spec: - The click entry names the element the event was issued on, so a descendant shows up there explicitly. - Clicking the entry prints the coordinates and the full list of mouse events to the browser console, including any that were skipped because a previous one was cancelled. - The red hitbox on the snapshot shows the point Cypress used, which is how you catch a case where an oversized child moved the centre somewhere you did not expect. ## Where this bites in practice In a library catalogue, the row-level Borrow control is usually a button wrapping an icon and a label, and the availability badge is often an absolutely positioned child of the row. Two failures come out of that: - A test clicks the row expecting the detail drawer to open, and instead the badge — a child stacked over the row's centre — receives the click and swallows it with its own handler. - A test clicks Borrow, the request goes out, but an assertion that the button now has focus fails, because the app cancels `mousedown` to run its own ripple animation. Neither is a Cypress defect, and neither is fixed by changing the selector. The first is fixed by aiming at a point the badge does not cover, or by targeting the control rather than the row; the second is a genuine application behaviour that the test should assert as it is.
- In Cypress, if the event fires on the inner span, why does a handler on the button still run?Because Cypress dispatches real, bubbling DOM events rather than calling a method on your element. The `click` fires at the span and then bubbles up through the button, so a listener bound there runs with `event.currentTarget` set to the button. Only a handler that reads `event.target` — or a child listener that stops propagation — sees a difference.
- In Cypress, why can a click succeed while the element never becomes focused?Focus goes to the first focusable ancestor of the point that was hit, or to the window when there is none. If the application calls `preventDefault()` on `mousedown`, the spec says focus does not move, so Cypress fires the rest of the sequence and the element stays unfocused. A following assertion on `have.focus` then fails even though the click itself worked.
Cypress aims at a pixel, not at a node — like a real reader's finger on the screen, whatever is on top at that point is what gets hit, even if you were thinking of the button underneath.
saying these in an interview costs you the question
- Thinks Cypress calls click() on the matched node
- Says a covering child fails the readiness check
- Expects the span to receive focus
- Treats the descendant note as a Cypress bug