skip to content

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

level: middleimportance: should knowfreq 58%

answer

  1. The deepest match is not always yielded
  2. A few tags outrank deeper nodes
  3. Think what you usually want to click
  4. A first argument changes the rules
  5. Case sensitivity has an option

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.

solid answer

~40 s

`cy.contains(text)` searches the current subject — the whole `document` when it begins a chain — and yields **one** element: the first deepest match, except that Cypress prefers a wrapping `input[type='submit']`, `button`, `a` or `label` over the deeper node. On a room card marked up as `<button><span>Book now</span></button>` you therefore get the `<button>`, which is usually what you meant to click. The two-argument form is `cy.contains(selector, text)` — selector first — and it filters candidates to that selector while switching the preference off, so `cy.contains('.room-card', 'Deluxe Double')` yields the card rather than the `<h3>` inside it. Matching is a case-sensitive substring match over normalized whitespace; `{ matchCase: false }` relaxes it and a `RegExp` gives you patterns. Only the first match is ever yielded.

go deeper

for a junior

Be able to say what Cypress hands back for a plain text match: one element chosen by its text, not a list. Recalling the argument order - selector first, text second - is the other half.

for a middle

Explain the preference order and why Cypress promotes a wrapping button or anchor over the deeper span. At this level an interviewer wants the mechanism, not just the observation.

for a senior

Show where the rule bites in a real suite: a card whose label sits in a span, or a second chained contains that searches inside the previous subject and fails on a dialog that is clearly visible.

for a principal

Own the failure mode. A query that yields a shallower or deeper node than its author assumed produces a green test that acted on the wrong element, and you should have a view on how a suite makes that visible.

## The rule: first deepest, then promoted `cy.contains('Book now')` searches the current subject for the text — the whole `document` when it starts a chain — and yields **exactly one** element. The base rule is *first deepest match*: of the elements containing that text, Cypress walks to the deepest one, since `<html>`, `<body>` and every wrapper technically contain it too. Then it applies one override. If that deepest element sits inside one of four preferred tags, Cypress yields the **wrapper** instead: - `input[type='submit']` - `button` - `a` - `label` So for a room card marked up as `<button class="book"><span>Book now</span></button>`, Cypress yields the `<button>`, not the `<span>`. That is almost always what you wanted, because the next thing you write is a click, and clicking the span inside a button is a worse test than clicking the button. ```html <li class="room-card"> <h3>Deluxe Double</h3> <button class="book"><span>Book now</span></button> </li> ``` ```js cy.contains('Book now') // yields <button class="book"> cy.contains('span', 'Book now') // yields <span>, preference switched off cy.contains('.room-card', 'Deluxe Double') // yields the <li> ``` ## What the selector argument changes The two-argument form is `cy.contains(selector, content)` — **selector first, text second**, which is the order people most often get backwards. It does two things at once: 1. It filters the candidates to elements matching the selector. 2. It **turns the tag preference order off**, because you have now said explicitly which element you want. That is how you keep a shallower element as the subject. `cy.contains('.room-card', 'Deluxe Double')` yields the whole card, so the next command in the chain operates on the card rather than on the `<h3>` buried inside it. | form | yields | preference order | |---|---|---| | `cy.contains('Book now')` | first deepest match, promoted to a preferred wrapper | applied | | `cy.contains('.book', 'Book now')` | first match that is also `.book` | ignored | | `cy.get('.room-card').contains('Book now')` | first deepest match **inside that card** | applied | ## Matching: case, whitespace and regular expressions - Matching is a **case-sensitive substring** match by default, over normalized whitespace. `{ matchCase: false }` relaxes it: `cy.contains('deluxe double', { matchCase: false })` finds the card. - A `RegExp` gives you anchoring and patterns: `cy.contains('.room-card', /^Deluxe/)` matches only cards whose text begins with *Deluxe*. - The two combine, with one rule. If you pass `{ matchCase: false }` and the regex has no `i` flag, Cypress adds it for you. If you pass `{ matchCase: true }` alongside a regex that already carries `i`, Cypress rejects the call — those options contradict each other, so it asks you to choose one. - A number is allowed and is treated as text: `cy.contains(2)` finds the guest-count badge. - `<pre>` is the exception to whitespace normalization: leading, trailing and repeated spaces inside a `<pre>` are significant, and your search string has to match them. There is no negated form. Asking for the elements that do *not* contain a piece of text is a different job, done by filtering a set you already hold rather than by `cy.contains()`. ## Only ever the first match This is the half people forget. `cy.get()` yields every match; `cy.contains()` yields **one**. On a room list where three cards mention *Sea view* in their description, `cy.contains('Sea view')` yields the first one in document order and says nothing about the other two. It cannot fail with "matched three elements", because matching three elements is not an error condition for it — which means a text query that is less specific than its author believed produces a green test that acted on the wrong card rather than a loud failure. One special case is worth knowing: an `<input type="submit">` with no `value` attribute renders a browser-supplied, locale-dependent label. Its `value` is the empty string, and Cypress has no way to read the label the user sees, so `cy.contains('Submit')` will not find it. Set an explicit `value` on the element, or assert `cy.get('input').should('have.value', '')` and target it another way. ## The chaining trap `.contains()` behaves differently depending on whether it starts a chain or continues one, and this is where suites go wrong: ```js // wrong: the second contains searches INSIDE the Book now button cy.contains('Book now').click().contains('Confirm booking').click() // right: cy starts a fresh search from the document again cy.contains('Book now').click() cy.contains('Confirm booking').click() ``` Chained off a subject, `.contains()` searches within that subject. After a click on the *Book now* button, the subject is still that button, and *Confirm booking* is nowhere inside it — so the command fails on a confirmation dialog that is plainly on screen. Starting a new `cy` chain resets the search to the document. ## What to say in an interview Three sentences cover it: `cy.contains()` yields one element, the first deepest match promoted to a wrapping submit input, button, anchor or label; the two-argument form is selector-then-text and turns that promotion off; and chaining it off an existing subject scopes the search to that subject rather than the page. The follow-up is nearly always about case sensitivity — the default is sensitive, and `{ matchCase: false }` or a regex is how you loosen it.

  • In Cypress, what does `cy.contains('.room-card', /^Deluxe/)` match, and how does `matchCase` interact with a regex?
    It yields the first `.room-card` whose normalized text begins with *Deluxe*. With a regular expression, `{ matchCase: false }` adds the `i` flag for you when it is missing. Passing `{ matchCase: true }` alongside a regex that already carries `i` is rejected — Cypress reports that the two options conflict and asks you to pick one.
  • Why does `cy.contains('Book now').click().contains('Confirm booking').click()` look in the wrong place in Cypress?
    The second `.contains()` is chained, so it searches inside the subject it was handed — the *Book now* button — rather than the document. The confirmation text is not inside that button, so the command fails on a dialog that is visibly on screen. Start a fresh chain with `cy.contains('Confirm booking').click()`; `cy` always searches from the root again.

saying these in an interview costs you the question

  • Says cy.contains always yields the deepest matching element
  • Thinks cy.contains yields every element containing the text
  • Passes text first and selector second
  • Assumes text matching is case-insensitive by default