skip to content

In Cypress, how do `.find()`, `.filter()` and `.children()` differ on a set of elements?

level: middleimportance: must knowfreq 58%

answer

  1. Down into, or across within?
  2. One of these never matches itself
  3. Direct children versus any depth
  4. What narrows without moving levels?
  5. Same selector twice yields nothing

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.

solid answer

~50 s

`.find(selector)` descends: it searches the descendants of every element in the current set and never matches the elements themselves. `.children(selector)` descends one level only. `.filter(selector)` does not move at all — it keeps the elements already in the set that match, and `.not(selector)` keeps those that do not. So on a room list, `cy.get('.room-card').find('.book-btn')` yields one button per card, while `cy.get('.room-card').filter('[data-available="true"]')` yields fewer cards. Writing `cy.get('.room-card').find('.room-card')` yields nothing, because `.find()` excludes its own subjects — that case wants `.filter()`, and the timeout names the selector rather than the mistake. `.eq(n)`, which accepts a negative index, plus `.first()` and `.last()`, narrow positionally when a command needs exactly one element. All of them follow jQuery's semantics and all of them are queries, so they retry and the whole chain is re-evaluated against the live DOM on each attempt.

code

javascript · 14 lines
javascript
// descend: one button per card
cy.get('.room-card').find('.book-btn')

// narrow: fewer cards, same level
cy.get('.room-card').filter('[data-available="true"]')
cy.get('.room-card').not('[data-available="true"]')

// one level down only
cy.get('.room-list').children('.room-card')

// positional, and negative indexes count from the end
cy.get('.room-card').eq(2)
cy.get('.room-card').eq(-1)
cy.get('.room-card').first().find('.price')

go deeper

for a junior

Know which command goes down into elements and which keeps some of the ones you already have, and be able to reach one card's price.

for a middle

Explain that .find() excludes its own subjects, that .children() stops at one level, and how .eq(), .first() and .last() satisfy single-element commands.

for a senior

Read a timeout on a traversal chain correctly: the message names the selector, so decide whether the command or the selector was wrong.

for a principal

Set the house style for narrowing — positional index versus content or attribute — and be ready to justify the maintenance cost of each.

## Two different moves: descend, or narrow Once a Cypress query has yielded a set of elements, there are only two things the next query can do to it: **go down into the elements**, or **keep some of the elements you already have**. Confusing the two is the most common traversal bug on a list page. - **Descend** — `.find(selector)` searches the descendants of every element in the set, and never matches the elements themselves. `.children(selector)` does the same but stops at direct children. - **Narrow** — `.filter(selector)` keeps the elements of the current set that match, and `.not(selector)` keeps the ones that do not. The set stays at the same level of the tree. On a room list, `cy.get('.room-card')` yields one element per card. `.find('.book-btn')` then yields one button per card, one level down. `.filter('[data-available="true"]')` yields fewer cards, at the same level. Neither can do the other's job. ## The command map | query | direction | matches | typical use | |---|---|---|---| | `.find(sel)` | down, any depth | descendants only | reach a price inside each card | | `.children(sel)` | down, one level | direct children only | rows immediately under a `<tbody>` | | `.filter(sel)` | sideways | elements already in the set | keep only bookable cards | | `.not(sel)` | sideways | elements not matching | drop the sold-out cards | | `.eq(n)` | sideways | one element by index | the third card | | `.first()` / `.last()` | sideways | one element by position | the first card | `.find()`, `.filter()`, `.not()`, `.children()` and `.first()` follow jQuery's semantics exactly, so a jQuery habit transfers — including the one below, which is where it bites. ## The classic mistake ```javascript // yields nothing: .find() never matches the elements it started from cy.get('.room-card').find('.room-card') // yields the cards that are bookable cy.get('.room-card').filter('[data-available="true"]') ``` Because `.find()` excludes the subject set itself, any selector that describes the *current* elements rather than something inside them must go through `.filter()` or `.not()`. The failure is loud — the query times out with `Expected to find element: '.room-card', but never found it` — but the message points at the selector, not at the wrong command, so it reads as a missing element. ## Positional narrowing `.eq(n)` is zero-based, and it also accepts a negative index counted from the end, so `.eq(-1)` and `.last()` pick the same element. Positional narrowing is the cheapest way to satisfy commands that insist on a single element: 1. Action commands such as `.click()` and `.type()` need exactly one element. 2. `.within()` needs exactly one element and says so in its error. 3. `.shadow()` is happiest on a single host. The caveat is that an index encodes render order, so it is only as stable as the order of the list under test. Narrowing by content — `cy.contains('.room-card', 'Deluxe King')` — or by an attribute the component owns says what you mean and survives a re-sort. ## Narrowing is a query, so it re-runs As of Cypress 16 all of these are **queries**: they retry, they honour `defaultCommandTimeout`, and they are safe to chain further commands onto. Two consequences follow: - When an assertion at the end of a chain retries, the *whole* chain of queries re-runs from its start, so `cy.get('.room-card').filter('[data-available]').first()` re-evaluates the filter against the current DOM on every attempt rather than replaying a stale set. - A filter that matches nothing does not fail on the spot; it keeps retrying until the timeout, and the failure surfaces on the assertion or action at the end of the chain. ## The rest of the family: up and across Descending and narrowing are two of three directions. The same set of queries also walks **upward** and **sideways**, and the distinctions there are the ones people most often blur: - `.parent()` goes exactly one level up; `.parents()` goes all the way up and yields every ancestor, optionally filtered by a selector. - `.closest(sel)` walks up and stops at the **first** match — and, unlike `.parents()`, it will match the element itself if the element matches. That is what makes `cy.contains('Deluxe King').closest('.room-card')` the idiomatic way to get from a piece of text back to the card that contains it. - `.siblings(sel)` yields the other children of the same parent, which is how you reach the price cell in the same row as the label you just matched. Put together, the common shape on a list page is *find something identifiable, walk up to its container, then descend again*: `cy.contains('Deluxe King').closest('.room-card').find('.book-btn')`. ## Choosing between them - If the selector describes something **inside** what you have, use `.find()` — or `.children()` when you specifically mean one level and want to exclude deeper matches. - If it describes **which of the things you have** you want, use `.filter()` or `.not()`. - If it describes **how many** or **which position**, use `.first()`, `.last()` or `.eq(n)`. - If you are about to write `.find()` with the same selector you just used in `cy.get()`, you almost certainly wanted `.filter()`.

  • In Cypress, why does `cy.get('.room-card').find('.room-card')` yield nothing?
    `.find()` follows jQuery's semantics: it searches the descendants of each element in the set and never matches the elements themselves. Since the cards are the subjects, nothing inside them matches, so the query retries until it times out. When the selector describes the elements you already hold, `.filter()` is the command you want.
  • In Cypress, what does `.eq(-1)` do, and how does it relate to `.last()`?
    `.eq()` accepts a negative index counted from the end of the set, so `.eq(-1)` yields the final element and matches what `.last()` yields. `.eq(0)` and `.first()` line up the same way. Use whichever reads better, and remember that any index encodes render order, so it is only as stable as the list's ordering.

saying these in an interview costs you the question

  • Uses .find() with a selector that describes the current elements
  • Thinks .children() searches all descendants like .find() does
  • Believes .filter() searches inside each element of the set
  • Assumes .eq() is one-based, or rejects a negative index
  • Expects an empty .filter() result to fail immediately rather than retry