(Cover image from pexels.com by Levi Damasceno)
- ACT 1: EXPOSITION
-
ACT 2: CONFRONTATION
-
What is
.contains()
? -
12+1 Misuses of
.contains()
– some might even call it abuse- 1. Unnecessary Use of
.should('exist')
Assertion - 2. Misinterpreting Partial Matching
- 3. Ignoring Whitespace Issues
- 4. Ignoring Case-sensitivity Issues
- 5. Assuming
.contains()
Returns All Matching Elements - 6. Using it for Too Broad Element Searches (VERY IMPORTANT!)
- 7. Relying on
.contains()
for Only Visible Elements - 8. Relying on
cy.contains
Instead of Declarative Selectors - 9. Using
.contains()
Against Non-Readable or Elements - 10. Misuse of
.contains()
in Assertions for Non-Text Elements - 11. Misunderstanding
.contains()
with Dynamic Content - 12. Misuse of
.contains()
Accessing a Shadow DOM Element - 12+1. Chaining Incorrectly Multiple
.contains()
- 1. Unnecessary Use of
- Diferences:
.contains()
vs.should('contains.text')
vs.should('have.text')
- Preference Order When Looking for Elements
- New:
.findOne()
in Gleb Bahmutov's cypress-map Plugin
-
What is
- ACT3: RESOLUTION
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')`
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')
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 })`
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 istrue
, meaning the match is case-sensitive.
// It will also find "My TEXT"
cy.contains('my text', { matchCase: false })`
-
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 theincludeShadowDom
option in thecypress.config.js
, orfalse
if not specified.
// Includes elements inside Shadow DOM.
cy.contains('my text', { includeShadowDom: true })`
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
andincludeShadowDom
, 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>
If we execute a .contains()
on the document using the text "Password":
// Obtains the <label> element that contains "Password" as text
cy.contains("Password")
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")
⚠️ 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>
This command will execute successfully:
// It will find the <input> element of type submit with text "Send"
cy.contains("Send")
This is because
input[type='submit']
is an element that behaves like a<button>
, with its text in thevalue
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');
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')
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')
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$/)
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}$`))
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 ')
Better Practice
- Trim or format the text properly.
// Avoid trailing/leading spaces
const text = ' Add to Cart '.trim()
cy.contains(text)
- Or better yet, be precise in matching.
// Precise matching
cy.contains('Add to Cart')
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')
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 })
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);
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>
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')
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")')
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')
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>
- 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
In this case:
cy.get('form')
selects all<form>
elements in the DOM.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
In this other case:
- 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')
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')
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')
⚠️ 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')
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()
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()
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">
And the user enters the text "John Wick" into the input field:
A .contains()
command searching for "John Wick" in the <input>
element will not find it:
cy.contains('input', 'John Wick') // This will fail
Better Practice
Use cy.get()
for non-readable elements like <input>
.
cy.get('input').should('have.value', 'John Wick') // This will pass
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>
The following will not work:
// This will fail because "Submit Button" is an alt attribute, not a visible text
cy.contains('button', 'Submit Button')
Better Practice
Use cy.get()
for non-standard-text elements, like <alt>
attribute.
cy.get('button').should('have.attr', 'alt', 'Submit Button');
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');
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');
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>
...
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();
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();
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...
})
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();
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>
...
The chained contains() will fail the test:
// The second contains will fail
cy.get('#container').contains('User').contains('Password')
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 bycy.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')
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();
-
.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
, andinclude.text
.
// Asserts the element with class .greeting contains "Welcome" within its text
cy.get('.greeting').should('contains.text', 'Welcome');
-
.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');
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
...
If we search for the element that contains the text "User":
// Yields the <label>, not the <span>
cy.contains('User').find('input')
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>
...
The .findOne()
command for the text "Hammer":
// It will yield <label>Hammer</label>
cy.get('#tools').findOne('Hammer')
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$/)
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`)
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$`))
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!
Top comments (4)
Great article — clear examples and no fluff. Really appreciate the real-world cases!
I'm very glad you find it useful @lvrub ! 🙂
Awesome article, Sebastian! I really enjoyed reading it!
Thank you very much @mmonfared !