In Cypress, why can .should('have.been.calledWith', args) pass on an earlier wrong call?
answer
- it searches the whole call history
- some call, not the last call
- arguments match as a subset
- always, Once and Exactly tighten it
- have.callCount pins the number
basics
~20 sSinon-Chai's calledWith is satisfied if any recorded call matched those arguments, and it matches them partially rather than exactly. A spy called correctly once and wrongly afterwards still passes. Use the always, Once or Exactly variants when the whole history matters.
solid answer
~40 s`have.been.calledWith` comes from Sinon-Chai and asks whether **some** recorded call matched — it scans the spy's whole history and stops at the first match, and it treats the arguments as a subset rather than an exact list. So a booking page whose confirm handler fires once with the right room and again with a stale one still passes `cy.get('@confirmBooking').should('have.been.calledWith', { roomId: 12 })`. The sharper chainers say what a test usually means: `have.always.been.calledWith` requires every call to match, `have.been.calledWithExactly` rejects extra arguments, `have.been.calledOnceWith` pins the count and the arguments together, and `have.callCount` states the number outright. Because the assertion is chained onto a query, Cypress re-reads the aliased spy until it passes or `defaultCommandTimeout` expires — which is why a too-permissive chainer shows up as green rather than slow.
go deeper
Know that have.been.calledWith is asserted on a spy rather than an element, and that it comes from Sinon-Chai rather than from Cypress itself.
Explain that the chainer searches the whole call history and matches arguments as a subset, and name the Exactly and Once variants that tighten each axis.
An interviewer expects you to catch a double-firing handler that a calledWith assertion cannot see, and to pick the chainer whose semantics match the defect you are guarding against.
Own the convention for how call-level assertions are written, so a spy check states its count and its argument strictness rather than quietly accepting a family of behaviours.
## The chainer is a search, not a claim about the last call `have.been.calledWith` is a **Sinon-Chai** chainer, and Sinon's semantics are existential: it asks whether *some* call in the spy's recorded history received those arguments. It does not ask about the most recent call, and it does not ask whether every call looked like that. ```javascript // given a spy already aliased as @confirmBooking cy.get('[data-cy=confirm-booking]').click() cy.get('@confirmBooking').should('have.been.calledWith', { roomId: 12, nights: 3 }) ``` If a stale click handler fired once with `{ roomId: 12, nights: 3 }` and the real submission then fired with `{ roomId: 12, nights: 0 }`, this passes. One matching entry anywhere in the history is enough. The assertion is telling the truth — the spy *was* called with those arguments — but the test reads as though it verified what the booking finally sent, and it did not. There is a second looseness on top of that: `calledWith` is a **partial** argument check. Extra arguments Sinon observed on the matching call are not a contradiction, so a handler that also received a third argument still satisfies a two-argument expectation. ## The Sinon-Chai family, and what each one pins down | Chainer | Passes when | | --- | --- | | `have.been.called` | at least one call happened, arguments ignored | | `have.been.calledWith`, args | some call received at least those arguments | | `have.always.been.calledWith`, args | every call received at least those arguments | | `have.been.calledWithExactly`, args | some call received exactly those arguments and no more | | `have.been.calledOnceWith`, args | there was exactly one call, and it received at least those | | `have.been.calledOnceWithExactly`, args | exactly one call, receiving exactly those arguments | | `have.callCount`, n | the spy was called n times, arguments ignored | Reading the table as two axes makes it easy to pick: **which calls** must match (`some`, `always`, `once`), and **how tightly** each argument list must match (`With` for a subset, `WithExactly` for the whole list). A booking confirmation usually wants both axes pinned, which is what `have.been.calledOnceWithExactly` says in one chainer. ## Why the loose default is rarely what a booking test means A confirm-booking handler that fires twice is a real defect: it double-books the room. But `have.been.calledWith` cannot see it, because the first call already satisfied the assertion. The same reasoning applies to a date-picker change handler that fires on every keystroke, or a guest form that submits once on `Enter` and again on the button click. - If **how many times** matters, use `have.callCount` or one of the `Once` chainers. - If **no extra arguments** may be passed, use `calledWithExactly`. - If **every** call must look right, use the `always` form. - If you only want a loose structural match on an object argument, `calledWithMatch` uses Sinon's matcher semantics rather than strict deep equality — say so explicitly rather than relying on it by accident. ## The assertion still retries, which is why this reads as green rather than slow A Sinon-Chai chainer is attached to its query exactly like a DOM chainer, so the whole retry-and-timeout story applies unchanged: - Cypress re-reads the aliased spy and re-evaluates the chainer until it passes or the command's window closes, governed by `defaultCommandTimeout` — 4000 ms as of Cypress 16. - That is what lets the assertion be written before the application has made the call at all; the test does not have to know when the confirm request fires. - It also means a **too-permissive** chainer is satisfied on the first evaluation, so the failure mode is a fast green rather than a timeout you would notice. - Raising the timeout therefore cannot repair a wrong chainer, for the same reason it cannot repair a negative assertion that passed immediately: the larger budget is never consulted. So the cost of choosing `calledWith` over `calledOnceWithExactly` is invisible in the run time and invisible in the report. The only place it shows up is the defect it fails to catch. ## Where candidates go wrong - Reading `calledWith` as "was called with exactly this, on the last call". It is neither exact nor last. - Assuming a passing spy assertion bounds the number of calls. It does not; only a count-bearing chainer does. - Reaching for `have.always.been.calledWith` when the real requirement is a single call, which leaves a double-fire green because both calls matched. - Treating a spy assertion as evidence the application *did the work*. It is evidence a function was invoked, which is why a booking test should also assert the resulting confirmation on the page. - Forgetting whose vocabulary this is. `have.been.calledWith` is Sinon-Chai's, not Chai-jQuery's, and it only makes sense when the subject is a spy or stub rather than an element.
- Which Sinon-Chai chainer states that a Cypress spy was called exactly once with exactly these arguments?`have.been.calledOnceWithExactly`. It pins both axes at once: `Once` requires the call count to be one, and `Exactly` requires the argument list to match in full rather than as a subset. `have.been.calledOnceWith` relaxes only the argument axis, and `have.always.been.calledWithExactly` relaxes only the count axis, allowing any number of identical calls.
- Does chaining a Sinon-Chai assertion onto cy.get('@spy') still retry in Cypress?Yes. The assertion is attached to the query the same way a DOM assertion is, so Cypress re-reads the aliased spy and re-evaluates the chainer until it passes or the command's window closes, governed by `defaultCommandTimeout`. That is what lets a test assert on a call the application has not made yet. It also means an over-permissive chainer passes immediately rather than surfacing as a timeout.
saying these in an interview costs you the question
- Reads calledWith as an exact argument match
- Thinks calledWith describes the spy's most recent call
- Assumes a passing spy assertion bounds the call count
- Cannot name which library have.been.calledWith comes from
- Uses always where the requirement is a single call