skip to content

Selectors and Assertions

How a Cypress test names an element and then judges it: the query commands, the traversal helpers that narrow a set, and the assertion styles that retry until the page agrees.

on this pageshow

explore

questions

29

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, how do you find an element by its `data-cy` test attribute?

level: juniorimportance: must knowfreq 82%

basics

~20 s

Pass a CSS attribute selector to cy.get(), as in cy.get('[data-cy="room-card"]'). Cypress ships no dedicated test-id command: the attribute is ordinary markup and the selector is ordinary CSS, matched the way jQuery's $() matches, with data-cy carrying no special status.

open as a page

In Cypress, why does a re-render detach the element your chain already holds?

level: juniorimportance: must knowfreq 80%

basics

~20 s

A Cypress chain carries a concrete DOM node, not the selector that found it. A re-render throws that node away and mounts a fresh one, so the node your chain still holds is no longer in the document.

open as a page

In Cypress, what does `cy.get('.room-card')` yield when the page renders 12 room cards?

level: juniorimportance: must knowfreq 78%

basics

~20 s

One jQuery-wrapped collection holding all 12 room-card elements, not a single DOM node and not an array. Its length is 12, and the whole set becomes the subject the next command in the chain receives.

open as a page

In Cypress, what does `.within()` change about the `cy.get()` calls inside its callback?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Inside a .within() callback Cypress re-roots its document-level element queries at the subject, so cy.get() and cy.contains() search only that element's descendants and cy.root() yields it. The subject must be exactly one element.

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, how do `.find()`, `.filter()` and `.children()` differ on a set of elements?

level: middleimportance: must knowfreq 58%

basics

~10 s

Cypress's .find() searches descendants of every element at any depth and .children() only direct children, while .filter() keeps the elements already in the set that match and .not() keeps those that do not.

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

In Cypress, what does setting `selectorPriority` via `Cypress.ElementSelector` control?

level: middleimportance: should knowfreq 44%

basics

~20 s

It sets the order of attributes Cypress prefers when it writes a selector for you, in Cypress Studio, the Selector Playground and cy.prompt(). It never changes what cy.get() matches, and it replaces Cypress's default order rather than extending it.

open as a page

In a Cypress spec, what does `@testing-library/cypress` add to `cy`?

level: middleimportance: should knowfreq 58%

basics

~20 s

It registers Testing Library's asynchronous find queries as Cypress commands - cy.findByRole, cy.findByLabelText, cy.findByTestId and their All variants - so a spec can locate elements by role and accessible name. It is a third-party add-on, imported once in the support file.

open as a page

In Cypress, how does `cy.contains()` choose which element to yield, and what does a selector argument change?

level: middleimportance: should knowfreq 58%

basics

~20 s

It yields one element: the first deepest match, promoted to an enclosing input[type=submit], button, a or label when there is one. Passing a selector as the first argument filters the candidates and switches that promotion off.

open as a page

Why does a Cypress command chained after a `.within()` block run against the parent?

level: middleimportance: should knowfreq 44%

basics

~10 s

.within() always yields the subject it was given, never anything the callback found. Cypress discards the callback's return value, including a cy.wrap() inside it, so the next command acts on the container element.

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

In Cypress, why is a jQuery element captured in .then() unsafe after an action?

level: seniorimportance: should knowfreq 47%

basics

~20 s

The callback runs once and is never retried, so it captured a snapshot of one node. Once an action re-renders that node away, the snapshot still answers reads with stale values, letting an assertion pass against a row that left the page.

open as a page

In Cypress, what does a detached failure that says 'while this command was executing' point at?

level: seniorimportance: should knowfreq 50%

basics

~20 s

It points at the application's timing, not at your chain. Cypress re-ran the query while waiting for the element to become actionable and got nothing attached back, so the element genuinely vanished between being found and being acted on.

open as a page

In Cypress, a room row is aliased with `.as('firstRoom')` and then re-renders. What does `cy.get('@firstRoom')` yield?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The freshly rendered row. By default Cypress stores the chain of queries that produced the subject rather than the element itself, so reading the alias re-runs those queries against the current DOM instead of returning a stale reference.

open as a page

In a Cypress test, when is `Cypress.$` safe to use, and why is it empty right after `cy.visit()`?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Cypress.$ is the bundled jQuery function and it queries the page synchronously, the instant its line runs. cy.visit() above it has only been queued, so it reads the previous page. Use it inside a callback, or from DevTools while debugging.

open as a page

In a Cypress `.within()` block, why can `cy.get()` time out on a visible element?

level: seniorimportance: should knowfreq 36%

basics

~20 s

A .within() scope restricts queries to the scoped element's DOM descendants, not to what looks nearby on screen. A date picker or modal rendered into a container at the end of body sits outside it, so the query never matches.

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

How far should a Cypress project's `selectorPriority` list constrain generated selectors?

level: principalimportance: should knowfreq 26%

basics

~20 s

Far enough that generated code reaches for your test attribute first, not so far that Cypress has nothing left to fall back on. A one-entry list forces Cypress to improvise; leaving class and nth-child in lets fragile selectors into the repo.

open as a page

How far should a Cypress suite go to absorb detached-node failures the app causes?

level: principalimportance: should knowfreq 32%

basics

~20 s

Absorb the ordinary case by re-querying, which is normal Cypress hygiene. Escalate when a region is replaced so constantly that no query can win, because that churn costs real users their focus and scroll position too.

open as a page

In Cypress, what does Cypress.dom.isDetached($el) return, and what can it not do?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

It returns a boolean, the exact negation of Cypress.dom.isAttached: true unless every node you pass is connected to a document with a live window. It is a snapshot read that never waits, never re-queries and never repairs anything.

open as a page

In Cypress, what do `cy.title()`, `cy.document()` and `cy.window()` yield to the next command?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

cy.title() yields the document.title string, cy.document() yields the application's window.document, and cy.window() yields its window object. All three read the app under test, and none of them yields a DOM element you can chain element commands onto.

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

In Cypress 16, what happens to a support file calling `Cypress.SelectorPlayground`?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Every method on it throws. Cypress.SelectorPlayground.defaults() reports that it was renamed to Cypress.ElementSelector.defaults(), and getSelector() reports that it was removed. The rename landed in Cypress 15.0.0, and because the call sits in the support file, every spec fails.

open as a page

In Cypress, how do you click a button that lives inside a component's open shadow root?

level: seniorimportance: nice to knowfreq 32%

basics

~10 s

Chain .shadow() off the host element and keep traversing: cy.get('room-card-widget').shadow().find('.book-btn').click(). Alternatively pass { includeShadowDom: true } to that one query, or set the includeShadowDom config value, which defaults to false.

open as a page