skip to content

Expectation Styles

The two ways a Cypress test states what it expects - a chained should that keeps retrying the query in front of it, and an expect block that runs once - and when each one is right.

on this pageshow

explore

questions

9

In Cypress, how do you write an `expect()` assertion inside a `.then()` callback?

level: juniorimportance: must knowfreq 70%

answer

  1. The callback gets the previous command's subject
  2. No assertion library to import
  3. cy.get yields a jQuery collection, not one element
  4. Chai's expect and assert are spec globals
  5. expect($rooms).to.have.length(4) inside then

basics

~10 s

Cypress makes Chai's expect and assert global in every spec, so inside .then(($rooms) => ...) you call expect($rooms).to.have.length(4) straight on the yielded subject. A throw from that expect fails the test at that command.

solid answer

~40 s

`.then(cb)` hands the previous command's subject to plain JavaScript. After `cy.get('[data-cy=room-card]')` that subject is a **jQuery collection** of the matched room cards, so `expect($rooms).to.have.length(4)` or `expect($rooms.eq(0).text()).to.contain('Deluxe King')` both work. Nothing needs importing: Cypress bundles Chai and exposes `expect` (BDD) and `assert` (TDD) as spec globals, plus the Chai-jQuery and Sinon-Chai extensions. A failing assertion throws an `AssertionError`; Cypress catches it, fails the test at that command and renders the assertion in the Command Log. Chai's optional second argument labels a check — `expect($rooms, 'four rooms listed').to.have.length(4)` — which is worth using once a block holds several assertions. Use the explicit form when you must compute first: parse `$210.00` into a number, compare two elements, check sort order. The catch is that `.then()` runs its callback once and does not retry.

code

javascript · 16 lines
javascript
it('lists four rooms in ascending price order', () => {
  cy.visit('/rooms')

  cy.get('[data-cy=room-card]').then(($rooms) => {
    expect($rooms, 'four rooms listed').to.have.length(4)

    // .find() and .toArray() here are jQuery's; Cypress bundles jQuery
    const rates = $rooms
      .find('[data-cy=nightly-rate]')
      .toArray()
      .map((el) => Number(el.textContent.replace(/[^0-9.]/g, '')))

    expect(rates).to.deep.equal([...rates].sort((a, b) => a - b))
    expect(rates[0]).to.be.greaterThan(0)
  })
})

go deeper

for a junior

Be ready to write the block from memory: cy.get(...).then(($rooms) => expect($rooms).to.have.length(4)). Say plainly that Chai ships inside Cypress, so there is nothing to import.

for a middle

Explain what the callback actually receives — a jQuery collection, not one element — and how you reach the value you assert on: toArray, eq, text, or a raw node by index.

for a senior

Show judgment about when an explicit block beats a chained assertion: computed values, relationships between two elements, and groups of checks against one query result.

for a principal

Own the convention. Decide which checks a suite writes explicitly, how long such a block may grow before its first throw hides the rest, and how the team reviews them.

## The subject the callback receives Every Cypress command yields a **subject** to the next step in the chain. `.then(cb)` is the escape hatch that hands that subject to plain JavaScript: whatever the previous command produced arrives as the callback's first argument. After `cy.get('[data-cy=room-card]')` on a hotel room list, that argument is a **jQuery collection** wrapping every matched element — not a single element, and not a raw DOM node. After `cy.request()` it is the response object, after `cy.fixture('rooms')` the parsed fixture, after `.invoke('text')` a string. Inside the callback you are in ordinary synchronous JavaScript, so anything you could do to that value in a console you can do here. Cypress names the two assertion styles after the subject. A chained `.should('have.length', 4)` is an **implicit** assertion: Cypress supplies the subject for you. Writing `expect($rooms).to.have.length(4)` inside a callback is an **explicit** assertion: you name the subject yourself. This is the second form. ## Chai is already in the spec — both `expect` and `assert` You do not import an assertion library into a Cypress spec. Cypress bundles **Chai** and installs both of its interfaces as globals on the spec window: - `expect(value)` — Chai's BDD interface, the one nearly every Cypress example uses: `expect(rates).to.deep.equal([180, 210, 240, 320])`. - `assert` — Chai's TDD interface, so `assert.equal(actual, expected, message)` and its siblings work in the same callback if your team prefers that style. Two extensions ride along. **Chai-jQuery** teaches Chai about jQuery objects, which is why `expect($rooms).to.have.length(4)` and `expect($rooms.first()).to.contain('Deluxe King')` read naturally; **Sinon-Chai** adds the spy and stub chainers. A failing `expect` throws an `AssertionError`; Cypress catches the throw, fails the test at that command and renders the assertion in the Command Log. There is no accumulate-and-report mode — the first throw ends the test. Chai's `expect` also takes an optional message as its second argument, and Cypress surfaces it: `expect($rooms, 'four rooms listed').to.have.length(4)` labels that entry so a failure names the check instead of only printing two values. In a block with several assertions it is the cheapest possible diagnostics. ## Getting from a jQuery subject to the value you want | what you want | how you get it | |---|---| | how many rooms matched | `expect($rooms).to.have.length(4)` | | the text of one card | `expect($rooms.eq(0).text()).to.contain('Deluxe King')` | | a raw DOM node | `$rooms[0]`, then `.className`, `.dataset.rate`, `.textContent` | | every rate as a number | jQuery's `.toArray()`, then a plain `.map()` | | one attribute | `$rooms.eq(0).attr('data-rate')` — jQuery's `.attr()` | Two habits keep these blocks readable. Convert to plain JavaScript early: jQuery's `.toArray()` returns a real array, so `.map()`, `.filter()` and `.sort()` behave the way you expect. And assert on the computed value rather than on the collection, because Chai's failure output for a number or an array of strings is far easier to read than its output for a jQuery object. ## When an explicit block earns its place Reach for one when the check is not expressible as a single chainer: 1. **A computed value.** The nightly rate renders as `$210.00`, the assertion is about the number 210, and the parse has to live somewhere. 2. **A relationship between values.** The booking total equals the nightly rate times the number of nights the date picker spans — no built-in chainer relates two subjects. 3. **A group of checks on one snapshot.** Length, the first card's name, and the full list of room types, all asserted against the same query result. 4. **A shape the chainers do not cover.** Sort order, a numeric tolerance, a regular expression over a derived string. For anything a chainer already says — visibility, class, attribute, exact text — the chained form is shorter and reads better. Explicit blocks are the tool for the cases the vocabulary misses, not a replacement for it. ## The one cost to keep in mind `.then()` invokes its callback **once**. The moment the subject is available Cypress calls it, the assertions inside get a single attempt, and nothing above `.then()` in the chain re-runs. That is the right trade when the page has settled and the wrong trade when the value you are computing is still arriving — which is why the identical code moved into a `.should()` callback behaves differently.

  • In a Cypress `.then()` callback, how do you get a plain array out of the jQuery subject?
    Call jQuery's `.toArray()` on it: `$rooms.toArray()` returns a real array of DOM nodes, so `.map()`, `.filter()` and `.sort()` behave normally. Indexing works too — `$rooms[0]` is a raw element while `$rooms.eq(0)` keeps you in jQuery. Assert on the derived array rather than the collection; Chai's output for an array of strings is far easier to read than its output for a jQuery object.
  • What does the second argument to Chai's `expect()` do in a Cypress spec?
    It is a message label. `expect($rooms, 'four rooms listed').to.have.length(4)` attaches that text to the assertion, and Cypress shows it on the Command Log entry and in the failure output. In a callback holding several `expect()` calls it is the cheapest way to make a failure say which check broke instead of only which two values differed.

saying these in an interview costs you the question

  • Thinks Chai must be imported into the spec file
  • Calls the .then() argument a single DOM element
  • Believes a failed expect only logs a warning
  • Expects an expect inside .then() to keep retrying
  • Assumes only chained should strings can assert in Cypress
open as a page

In Cypress, what does .should('have.text', 'Deluxe King') do when chained onto cy.get()?

level: juniorimportance: must knowfreq 84%

basics

~10 s

It attaches a Chai-jQuery assertion to the preceding cy.get(), and Cypress keeps re-evaluating the query and the check until the element's text matches or the four-second default command timeout expires, then fails the test.

open as a page

In Cypress, what changes when an `expect()` moves from `.then()` into `.should()`?

level: middleimportance: must knowfreq 64%

basics

~20 s

Nothing about the code changes; the timing does. A .then() callback runs once against a frozen chain, while a .should() callback is retried, queries and all, until nothing inside it throws or the command timeout expires.

open as a page

In Cypress, where do you put { timeout: 10000 } so a chained .should() retries longer?

level: middleimportance: must knowfreq 63%

basics

~10 s

Put it on the query, not the assertion: cy.get(selector, { timeout: 10000 }). That single window covers the lookup and every assertion chained after it, because .should() accepts no options object of its own.

open as a page

In Cypress, why does .should('be.visible').and('not.be.visible') on one cy.get() never pass?

level: middleimportance: should knowfreq 41%

basics

~20 s

Every assertion chained onto one cy.get() runs against the same subject reference in the same retry window, so a single element cannot satisfy both conditions at once. Split the checks into two cy.get() statements so Cypress re-queries between them.

open as a page

A Cypress `.should()` callback collects room rates into an array and ends up with duplicates. Why?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because the callback is the retry unit. Cypress re-runs the whole callback body on every attempt until nothing inside throws, so a push into an array declared outside it happens once per attempt rather than once per test.

open as a page

Why can a Cypress .should('not.exist') check pass even when the feature is broken?

level: seniorimportance: should knowfreq 46%

basics

~20 s

A negative chainer is satisfied the moment the thing is absent, including before the app has rendered anything at all. Cypress stops retrying on the first pass, so the check can succeed on an untouched page rather than on the state you meant to verify.

open as a page

How far should a Cypress suite let `.then()` blocks replace retrying assertions?

level: principalimportance: should knowfreq 34%

basics

~20 s

Only where the block must run exactly once. If it merely computes and asserts, put it in a should callback and keep the retry; reserve then for capturing values, queueing commands, and task or file work.

open as a page

In Cypress, why can .should('have.been.calledWith', args) pass on an earlier wrong call?

level: seniorimportance: nice to knowfreq 29%

basics

~20 s

Sinon-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.

open as a page