DEV Community

Cover image for The 'second' most abused (and misused) Cypress command ever: cy.contains()
Sebastian Clavijo Suero
Sebastian Clavijo Suero

Posted on

The 'second' most abused (and misused) Cypress command ever: cy.contains()

(Cover image from pexels.com by Levi Damasceno)


ACT 1: EXPOSITION

 

About a year ago, I wrote an article about how cy.wait(TIME) is the most abused Cypress command ever, and it quickly sparked an interesting conversation among my fellow QAs.

When I asked which other Cypress command was excessively used but equally misunderstood or misused, the response was quite unanimous: cy.contains().

While I still firmly believe cy.wait(TIME) is the reigning champion of infamous commands in Cypress, I must admit that cy.contains() earns a solid silver medal.

Do not get me wrong, cy.contains() is one of my favorite and most powerful commands in the Cypress arsenal. But much like John Wick, it’s a double edged weapon, capable of incredible precision, yet risky if not handled correctly. This misuse frequently results in failing or flaky tests, making its misuse both widespread and often tricky to catch.

So yes, cy.contains() (amazing but often poorly executed), deserves, in my opinion, its spot as runner-up in the "most abused/misused Cypress commands" debate!


ACT 2: CONFRONTATION

 

What is .contains()?

According to the official Cypress documentation, it is used to get the DOM element containing the text. DOM elements can contain more than the desired text and still match.

In other words, it yields one DOM element that contains the passed substring.

It has a Dual Nature

It has a dual nature because it can start a new Cypress chain (searching the entire document for the content):

// Start a chain (search entire document)
cy.contains('my text')`
Enter fullscreen mode Exit fullscreen mode

or use an existing chain (querying inside the DOM tree of the specified yielded element):

// Use an existing chain (search restricted within element with id 'list')
cy.get('#list').contains('my text')
Enter fullscreen mode Exit fullscreen mode

It is a Cypress query

You must know that .contains() is a Cypress query that retries until all the chained assertions pass and the element exists in the DOM or until the timeout expires according to the defaultCommandTimeout in cypress.config.js. But this can be overridden with the timeout option.

// Timeout override in millisecs
cy.contains('my text', { timeout: 10000 })`
Enter fullscreen mode Exit fullscreen mode

Because .contains() is a Cypress query, it not only retrieves the element matching the text but also performs an implicit assertion that the element must exist.

Some Overlooked Options

.contains() supports the log and timeout options, like many other Cypress commands and queries, but there are a couple of very important options that are often overlooked:

  • matchCase: To check case sensitivity. By default, it is true, meaning the match is case-sensitive.
// It will also find "My TEXT"
cy.contains('my text', { matchCase: false })` 
Enter fullscreen mode Exit fullscreen mode
  • includeShadowDom: To include in the match elements within the shadow DOM. With this option you can directly target elements within the Shadow DOM without using the .shadow() command explicitly. By default, it uses the includeShadowDom option in the cypress.config.js, or false if not specified.
// Includes elements inside Shadow DOM.
cy.contains('my text', { includeShadowDom: true })` 
Enter fullscreen mode Exit fullscreen mode

Note: A shadow DOM element is a hidden subtree attached to a regular DOM element, isolating its HTML and CSS from the main document to prevent conflicts and enable reusable, self-contained web components.

I would like you to try to remember these two options matchCase and includeShadowDom, as they’re going to come up again later.

Selector Support

An important thing to know about .contains() is that you can provide a selector to narrow and accelerate the search. When a selector is provided, it will return the DOM element that matches the selector, and either itself or one of its children matches the specified text.

For example, for this HTML:

<form>
  <label>Enter Password</label>
  <input id="password" placeholder="Enter your password"
         type="password">
</form>
Enter fullscreen mode Exit fullscreen mode

If we execute a .contains() on the document using the text "Password":

// Obtains the <label> element that contains "Password" as text
cy.contains("Password")
Enter fullscreen mode Exit fullscreen mode

Alternatively, if we execute a .contains() using the HTML tag form as the selector and the text "Password":

// Obtains the <form> element that contains "Password" in its DOM tree
cy.contains("form", "Password")
Enter fullscreen mode Exit fullscreen mode

⚠️ Notice that, in the last case, although the element with the text "Password" is the <label>, .contains() will return the <form> element because it is the provided selector, and it contains a child with the text "Password" in its DOM tree.

Contains with input[type="submit"] Element

There is an interesting case with .contains() where it can find an <input> element with the attribute type="submit" and the attribute value contains the provided text.

For example, with the following HTML:

<div id="main">
  <form>
    ...
    <input type="submit" value="Send" />
  </form>
</div>
Enter fullscreen mode Exit fullscreen mode

This command will execute successfully:

// It will find the <input> element of type submit with text "Send"
cy.contains("Send")
Enter fullscreen mode Exit fullscreen mode

This is because input[type='submit'] is an element that behaves like a <button>, with its text in the value attribute. Therefore, .contains() also includes this specific type of element in the search.

 

Ok, now that we understand the theory, using .contains() in our test shouldn’t be too hard, right? 😎

Well, most likely not.

But what if I told you that this mighty command is, unfortunately, often misused in Cypress tests, making them flakily unpredictable?

Don’t believe me? 🩳🔥
Alright, let me explain!

 

12+1 Misuses of .contains() – some might even call it abuse

 

1. Unnecessary Use of .should('exist') Assertion

Misuse
Unnecessarily chaining the assertion .should('exist') after .contains() to ensure the element is present in the DOM.

// Chaining `.should('exist')` after `.contains()` is unnecessary
cy.contains('my text').should('exist'); 
Enter fullscreen mode Exit fullscreen mode

Better Practice
Do not include .should('exist'), as .contains() is a Cypress query (like .get()) and automatically waits for the element to exist and be in an actionable state.

// Directly use `.contains()`
cy.contains('my text') 
Enter fullscreen mode Exit fullscreen mode

 

2. Misinterpreting Partial Matching

Misuse
Users may not realize cy.contains() performs partial text matching by default, leading to unintended behavior.

// Matches both "Add to Cart" and "Address".
cy.contains('Add')  
Enter fullscreen mode Exit fullscreen mode

Better Practice
If you want an exact match, you can use a regular expression with special characters: start-of-string ^ and end-of-string $.

// Ensures it matches the whole word 'Add'
cy.contains(/^Add$/)  
Enter fullscreen mode Exit fullscreen mode

If you want to find an element that contains an exact text match, and part of the text comes from a dynamic variable, you can use new RegExp() to handle the dynamic value:

// Exact match for "John Wick"
let who = "Wick"
cy.contains(new RegExp(`^John ${who}$`)) 
Enter fullscreen mode Exit fullscreen mode

 

3. Ignoring Whitespace Issues

Misuse
Passing text that doesn't account for formatting, such as extra spaces and newlines.

// Fails due to extra spaces after the word "Cart"
cy.contains('Add to Cart   ') 
Enter fullscreen mode Exit fullscreen mode

Better Practice

  • Trim or format the text properly.
// Avoid trailing/leading spaces
const text = ' Add to Cart  '.trim()
cy.contains(text) 
Enter fullscreen mode Exit fullscreen mode
  • Or better yet, be precise in matching.
// Precise matching
cy.contains('Add to Cart') 
Enter fullscreen mode Exit fullscreen mode

 

4. Ignoring Case-sensitivity Issues

Misuse
Passing text that doesn't account for casing issues. By default .contains() is case-sensitive.

// Only finds an element with the exact text "add to cart" in all lowercase
cy.contains('add to cart') 
Enter fullscreen mode Exit fullscreen mode

Better Practice

If you want to find an element with the text "add to cart" regardless of its letter casing, you can use matchCase option.

// Enables case-insensitive matching
cy.contains('add to cart', { matchCase: false }) 
Enter fullscreen mode Exit fullscreen mode

You could also write a regular expression to achieve the same goal of case-insensitive matching:

// Also enables case-insensitive matching
cy.contains(/add to cart/i);
Enter fullscreen mode Exit fullscreen mode

 

5. Assuming .contains() Returns All Matching Elements

Misuse
Beginners often assume that .contains() retrieves all the elements that match the specified text, whereas it actually only selects the first matching element in the DOM.

For example, if your DOM contains multiple matching elements with the word "Enter":

<label>Enter Username</label>
<label>Enter Password</label>
Enter fullscreen mode Exit fullscreen mode

Then this command will find only the first <label> element matching the text:

// Will find and return only the first <div> that contains "Enter",
// and that would be <label>Enter Username</label>
cy.contains('Enter') 
Enter fullscreen mode Exit fullscreen mode

Better Practice
To get all elements that contains the substring you can use cy.get() with :contains() jquery selector extension.

// Retrieves all elements in the DOM that contain the text "Enter"
cy.get(':contains("Enter")') 
Enter fullscreen mode Exit fullscreen mode

 

6. Using it for Too Broad Element Searches (VERY IMPORTANT!)

Misuse
Relying on cy.contains() without narrowing the context can cause the command to match unintended elements.

// Consider any element including text "Password" on the DOM
cy.contains('Password') 
Enter fullscreen mode Exit fullscreen mode

If there are multiple elements with the text "Password" on the page, this can lead to selecting the wrong element.

Better Practice

Let's consider the following HTML:

<form>
  <label>Enter Password</label>
  <input id="password" placeholder="Enter your password" type="password">
</form>
Enter fullscreen mode Exit fullscreen mode
  • Approach A: Use a parent element or container to scope the search.
// Scoped to <form> or children elements within the <form> that match the text
cy.get('form').contains('Password') // Yields the <label> element
Enter fullscreen mode Exit fullscreen mode

In this case:

  1. cy.get('form') selects all <form> elements in the DOM.

  2. Within these <form> elements, cy.contains('Password') finds a child element matching the text "Password" (in this case, the <label> element).

This approach is useful for constraining the scope of searchable elements before filtering by text.

  • Approach B: Leverage .contains() with a selector.
// Scope only to <form> elements that match the provided text itself or
// within one of their children
cy.contains('form', 'Password');  // Yields the <form> element
Enter fullscreen mode Exit fullscreen mode

In this other case:

  1. Finds the first <form> element anywhere in the whole DOM that contains the text "Password" within its DOM tree.

This is very useful for a quick search if you are looking for any <form> in the DOM that contains an element matching the text.

 

7. Relying on .contains() for Only Visible Elements

Misuse
Assuming that .contains() will only locate visible elements while ignoring hidden ones.

// The element containing 'Hidden Text' will be found even if the text is hidden
cy.contains('Hidden Text') 
Enter fullscreen mode Exit fullscreen mode

Beginners may mistakenly believe that .contains() will locate only elements that are visible. However, it will return the first element in the DOM that contains the specified text, regardless of its visibility.

Better Practice

To ensure you're working only with visible elements, you can use the :visible pseudo-selector.

  • Directly as part of a CSS selector:

Example 1.

// It will return a visible <div> element that contains the text "Username"
// or a child element within a visible <div> that contains the text "Username"
cy.get('div:visible').contains('Username')
Enter fullscreen mode Exit fullscreen mode

Example 2.

// It will return a <div> element that contains the text "Username"
// or a <div> element whose child contains the text "Username"
cy.contains('div:visible', 'Username')
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: Note the nuance! Both might look similar, but they are slightly different and could yield completely different elements or even pass or fail the implicit assertion depending on the DOM tree under the <div> element.

  • Used in a .filter() command:
// It will return a visible <div> element that contains the text "Username"
// or a child element within a visible <div> that contains the text "Username"
cy.get('div').filter(':visible').contains('Password')
Enter fullscreen mode Exit fullscreen mode

 

8. Relying on cy.contains Instead of Declarative Selectors

Misuse
Overusing .contains() instead of targeting elements by data attributes or selectors leads to brittle tests.

// It breaks if the text changes to something like "Remove"
cy.contains('Delete').click()  
Enter fullscreen mode Exit fullscreen mode

Better Practice
Utilize data attributes or stable semantic selectors.

// Robust and still readable (clearly we are targeting the 'delete' button)
cy.get('[data-cy="delete-btn"]').click()  
Enter fullscreen mode Exit fullscreen mode

 

9. Using .contains() Against Non-Readable or Elements

Misuse
Attempting to use .contains() with non-readable elements, such as <input> or <textarea>.

If your DOM includes an <input> element like this:

<input id="username" class="form-control" 
       placeholder="Enter your username" type="text">
Enter fullscreen mode Exit fullscreen mode

And the user enters the text "John Wick" into the input field:

Image description

A .contains() command searching for "John Wick" in the <input> element will not find it:

cy.contains('input', 'John Wick') // This will fail
Enter fullscreen mode Exit fullscreen mode

Better Practice
Use cy.get() for non-readable elements like <input>.

cy.get('input').should('have.value', 'John Wick') // This will pass
Enter fullscreen mode Exit fullscreen mode

 

10. Misuse of .contains() in Assertions for Non-Text Elements

Misuse
Using .contains() to locate structural attributes (like value, alt or placeholder) instead of visible text.

If your DOM includes an <button> element like this:

<button id="submit-btn" alt="Submit Button">Submit</button>
Enter fullscreen mode Exit fullscreen mode

The following will not work:

// This will fail because "Submit Button" is an alt attribute, not a visible text
cy.contains('button', 'Submit Button') 
Enter fullscreen mode Exit fullscreen mode

Better Practice
Use cy.get() for non-standard-text elements, like <alt> attribute.

cy.get('button').should('have.attr', 'alt', 'Submit Button');
Enter fullscreen mode Exit fullscreen mode

 

11. Misunderstanding .contains() with Dynamic Content

Misuse
Failing to account for dynamically loaded content, causing intermittent errors.

// This might timeout if the assertion is made prematurely
cy.contains('Loading...').should('not.exist'); 
Enter fullscreen mode Exit fullscreen mode

Better Practice
Chain .contains() with cy.wait() or cy.intercept() to handle async loading.

cy.intercept('GET', '/api/items').as('getItems');
cy.wait('@getItems');
cy.contains('Loaded Content').should('exist');
Enter fullscreen mode Exit fullscreen mode

 

12. Misuse of .contains() Accessing a Shadow DOM Element

Misuse
If you attempt to access a Shadow DOM element while testing a standard DOM page without explicitly indicating to Cypress to account for shadow elements, it will not locate them.

A Shadow DOM element is part of a hidden DOM tree attached to a standard DOM element (known as the shadow host). This feature allows the encapsulation of styles and scripts, preventing them from influencing or being influenced by the rest of the document.

For example, consider this HTML with a Shadow DOM component that includes a button:

...
  <my-shadow-component>
    #shadow-root
    <button class="my-button">Click Me</button>
  </my-shadow-component>
...
Enter fullscreen mode Exit fullscreen mode

If we try to locate the with text "Click Me" using .contains():

// This will not work because the button is inside a Shadow DOM
cy.contains('Click Me').click();
Enter fullscreen mode Exit fullscreen mode

Better Practice
Use the includeShadowDom option set to true within .contains() to automatically traverse Shadow DOM boundaries.

// This will locate and interact with the <button> element inside the Shadow DOM
cy.contains('Click Me', { includeShadowDom: true }).click();
Enter fullscreen mode Exit fullscreen mode

If you want to include Shadow DOM elements in all searches across your project, you can configure it globally in the cypress.config.js file by setting the includeShadowDom option to true. This way, you don’t need to include it in every .contains() call.

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // Other configuration options...
  includeShadowDom: true,
  // Additional options...
})
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use the .shadow() command to traverse shadow DOM boundaries:

// This will locate and interact with the <button> element inside the Shadow DOM
cy.get('my-shadow-component')
  .shadow()
  .contains('Click Me')
  .click();
Enter fullscreen mode Exit fullscreen mode

12+1. Chaining Incorrectly Multiple .contains()

Misuse
Sometimes, users do not realize that when performing a .contains() query, the yielded element may have changed. If you chain a second .contains(), the subsequent query will be applied to the element returned by the first .contains() and not to the original search context.

If we have this html:

...
<div id="#container">
   <label>User</label>
   <input id="username" placeholder="Enter your username" type="text"/>
   <label>Password</label>
   <input id="username" placeholder="Enter your password" type="password"/>
</div>
...
Enter fullscreen mode Exit fullscreen mode

The chained contains() will fail the test:

// The second contains will fail
cy.get('#container').contains('User').contains('Password')
Enter fullscreen mode Exit fullscreen mode

Why is that?

  • The first .contains('User') will locate the <label> element with the text "User".
  • The second .contains('Password') will be applied to the element returned by the first .contains(), which is a <label> element, and not the original element yielded by cy.get('#container').
  • As a result, the second .contains() query will fail.

Better Practice
Avoid chaining .contains() unless you are absolutely sure that the subsequent .contains() query will operate within the DOM tree of the element yielded by the first .contains().

// This test will pass
cy.get('#container').as('container')
cy.get('@container').contains('User')
cy.get('@container').contains('Password')
Enter fullscreen mode Exit fullscreen mode

 

Diferences: .contains() vs .should('contains.text') vs .should('have.text')

 

Something I have noticed during my time working with Cypress is that quite a number of people, especially newer users, confuse the differences between .contains(), .should('contains'), and .should('have.text').

While they may seem quite similar at first glance, each has its own specific purpose and behavior that can significantly impact your test cases.

  • .contains('a_text'): Cypress query used to locate elements containing specific text within them. It searches the entire or part of the DOM (and optionally shadow DOM) to find elements with either partial or exact text. It’s primarily a selector.
// Finds any element that contains the exact text "Submit"
cy.contains('Submit').click();
Enter fullscreen mode Exit fullscreen mode
  • .should('contains.text', 'a_text'): Assertion that checks if an element’s text includes a specific substring. It is not used to locate elements, but rather to validate that a particular element has text that contains the expected value. Other alias: contain.text, includes.text, and include.text.
// Asserts the element with class .greeting contains "Welcome" within its text
cy.get('.greeting').should('contains.text', 'Welcome');
Enter fullscreen mode Exit fullscreen mode
  • .should('have.text', 'a_text'): Stricter assertion used to validate that an element’s text exactly matches the expected value.
// Asserts the text of the element with class .greeting is exactly "Welcome User"
cy.get('.greeting').should('have.text', 'Welcome User');
Enter fullscreen mode Exit fullscreen mode

 

Image description

 

Preference Order When Looking for Elements

 

Cypress gives top priority to a selector when it is explicitly provided in the .contains() call.

If no selector is specified, Cypress defaults to selecting elements higher in the DOM tree when they are:

  • <input[type='submit']>
  • <button>
  • <a>
  • <label>

This means that if the element containing the text is a inside a element, .contains() will yield the <label> element instead of the <span>.

For example, in the following HTML:

...
<form>
  <label>
    <span>User</span>
    <input name="user" />
  </label>
</form
...
Enter fullscreen mode Exit fullscreen mode

If we search for the element that contains the text "User":

// Yields the <label>, not the <span>
cy.contains('User').find('input')
Enter fullscreen mode Exit fullscreen mode

 

New: .findOne() in Gleb Bahmutov's cypress-map Plugin

 

One week prior to when this article was written, Gleb Bahmutov introduced a new Cypress query .findOne() to his cypress-map plugin.

This new query finds one DOM element that exactly matches the provided text.

For the following HTML:

...
<div id="tools">
  <label>Mallet</label>
  <label>Power Hammer</label>
  <label>Hammer</label>
  <label>Sledgehammer</label>
</div>
...
Enter fullscreen mode Exit fullscreen mode

The .findOne() command for the text "Hammer":

// It will yield <label>Hammer</label>
cy.get('#tools').findOne('Hammer')
Enter fullscreen mode Exit fullscreen mode

This would be equivalent to using a regular expression for an exact match in a .contains():

// It will yield <label>Hammer</label>
cy.get('#tools').contains(/^Hammer$/)
Enter fullscreen mode Exit fullscreen mode

The advantage of this command .findOne() is that it makes it much easier to find an element when the text is dynamic:

const type = 'Power'

// It will yield <label>Power Hammer</label>
cy.get('#tools').findOne(`${type} Hammer`)
Enter fullscreen mode Exit fullscreen mode

And no need to use the cumbersome RegExp:

const type = 'Power'
// It will yield <label>Power Hammer</label>
cy.get('#tools').contains(new RegExp(`^${type} Hammer$`)) 
Enter fullscreen mode Exit fullscreen mode

You can find a video about how to use .findOne() at this link.

 


ACT3: RESOLUTION

 

Well... Do you believe me now when I said at the beginning of this article that people abuse (or misuse) .contains() in many different ways?

Let me tell you, there are plenty of other ways to mess with this powerful Cypress query, but I stopped at 12+1 since this article is already quite lengthy. Besides, I really like the mystical number 13, and that's because I'm not superstitious (that might bring bad luck)! 😄


I'd love to hear from you! Please don't forget to follow me, leave a comment, or a reaction if you found this article useful or insightful. ❤️ 🦄 🤯 🙌 🔥

You can also connect with me on my new YouTube channel: https://www.youtube.com/@SebastianClavijoSuero

If you'd like to support my work, consider buying me a coffee or contributing to a training session, so I can keep learning and sharing cool stuff with all of you.
Thank you for your support!
Buy Me A Coffee

Top comments (4)

Collapse
 
lvrub profile image
Leonid • Edited

Great article — clear examples and no fluff. Really appreciate the real-world cases!

Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero

I'm very glad you find it useful @lvrub ! 🙂

Collapse
 
mmonfared profile image
Mohammad Monfared

Awesome article, Sebastian! I really enjoyed reading it!

Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero

Thank you very much @mmonfared !