Advanced CSS locators for Cypress (or other automation tools)

Ideally, each element to be located in our tests should contain a specific attribute to uniquely identify it, such as data-testid or data-cy. However, sometimes we may not be able to add these attributes, such as when using third-party components.

It is, therefore, a good idea to become familiar with locating elements in other ways. The most common in Cypress is through CSS locators.

In this article I won’t go into detail on how to locate elements through their tagname or class, as there are hundreds of articles on how to do that, like this one or this one. Instead, I’ll discuss a couple of tricks for selecting elements in less typical situations.

Partial attribute values.

Let’s say we have the following HTML code:

<header>
  <nav>
    <ul>
      <li data-testid="header-menu-home-option">
          <span>Home</span>
      </li>
      <li data-testid="header-menu-cart-option">
          <span>Cart</span>
      </li>
      <li data-testid="header-menu-aboutus-option">
          <div>
              <span>About Us</span>
          </div>
      </li>
    </ul>
  </nav>
<header>
<main>
  <h1>This is my page</h1>
  <div>
    <span>Hello!</span>
  </div>
</main>

Each item contains a data-testid, so it is possible to select each of them uniquely. However, we may need to select all list items using a single selector to perform some kind of check (for example, to check that there are three items in the menu). To do this, we can use the values of the data-testid attributes using partial approximations. That is:

Items whose value contains a particular text:

cy.get('[data-testid*="-menu-"]');

Elements whose value begins with a particular text:

cy.get('[data-testid^="header-menu-"]');

Elements whose value ends with a given text:

cy.get('[data-testid$="-option"]');

And the most restrictive option: elements whose value begins with a given text and ends with another given text:

cy.get('[data-testid^="header-menu-"][data-testid$="-option"]');

This last selector will return all those items starting with “header-menu-” and ending with “-option”.

const expectedItems = 3;
cy.get('[data-testid^="header-menu-"][data-testid$="-option"]').should('have.length', expectedItems);

Direct descendants (children)

CSS offers a series of selectors that allow us to reference elements that are children of other elements or that share the same parent.

Thus, to obtain all the <span> elements that are direct children of <li>, we would use the following selector:

cy.get('li > span');

This selector will return only the span nodes that are direct children of a li node, that is to say:

<span>Home</span>
<span>Cart</span>

The <span> containing the text “About Us” would not be returned by this locator, since its parent is a <div>, not a <li>. Note the difference with the following selector:

 cy.get('li span');

In this case we would get all the spans that have an ancestor (parent, grandparent, great-grandparent, …) that is a li. Therefore, we would get the following:

<span>Home</span>
<span>Cart</span>
<span>About Us</span>

 We can also select elements that are grandchildren of other elements by concatenating the ‘>‘ operator with the wildcard ‘*‘. For example, the following selector will return the <ul> tags that are grandchildren of a <header> tag:

 cy.get('header >*> ul');

Consecutive elements (siblings).

We can also select elements that share the same parent node, that is, that are siblings. To do this we will use the operators ‘+‘ and ‘~‘.

 The ‘+‘ operator will return the next sibling of the previous element (and only the next sibling). Thus, the following selector:

 cy.get('[data-testid="header-menu-home-option"] + li');

will return the first sibling of the li whose data-testid is “header-menu-home-option”, that is: 

<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>

If we want to get all the siblings after a given element, we should use the selector ‘~‘:

 cy.get('[data-testid="header-menu-home-option"] + li');

The selector will return us all the siblings of the first <li>, ie:

<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>
<li data-testid="header-menu-aboutus-option">
  <div>
    <span>About Us</span>
  </div>
</li>

It is important to keep in mind that we will only get the siblings found after the element before ‘~‘, so if there were any other <li> element before it, it would not be retrieved using this selector.

Elements containing other elements. :has()

One of the big problems we face when locating elements with CSS is the impossibility of navigating upwards. That is: to obtain the parent of a given element. Although XPath offers us the possibility of doing this directly, CSS also allows us to do it by means of a little trick: indicating in the selector that we want to select an element that contains another element. For example, if we want to get those <li> that contain a <span>, we will use the :has() operator to indicate it:

 cy.get('li:has(span)');

This will return the following:

<li data-testid="header-menu-home-option">
  <span>Home</span>
</li>
<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>
<li data-testid="header-menu-aboutus-option">
  <div>
    <span>About Us</span>
  </div>
</li>

Note: We can take advantage of child and sibling filtering and perform more complex queries, for example, those <li> that have a <span> as a direct descendant (child):

 cy.get('li:has(> span)');

In this case, the result will omit the last <li>, which has a <div> as a direct child instead of a <span>

<li data-testid="header-menu-home-option">
  <span>Home</span>
</li>
<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>

Elements that do not fulfill a condition. :not()

The :not() operator is also a good ally when selecting elements by discarding. Thus, the following selector will allow us to locate all the <li> elements but “About Us”:

 cy.get('[data-testid^="header-menu-"][data-testid$="-option"]:not([data-testid*="-aboutus-"])');

Elements containing a text. :contains()

Through the pseudo-class :contains() it is possible to locate elements containing a text, either directly or in a descendant. For example, the following selector:

cy.get('*:contains("Ho")');

will return the elements header, nav, ul, li and span corresponding to the following subtree:

<header>
  <nav>
    <ul>
      <li data-testid="header-menu-home-option">
        <span>Home</span>

We could also further refine the search by specifying other options:

cy.get('span:contains("Ho")');

Which would return us only the span element.

Note: :contains() is case-sensitive!!

Elements that fulfill one condition among several

As a last hint, we can create a selector in CSS that acts just like a logical OR by concatenating other selectors with commas (,). For example:

 cy.get('span:contains("Home"),span:contains("Cart")');

This selector will return all those span elements that contain either the substring “Homeor the substring “Cart“. This technique is very useful for scenarios in which two different elements can be shown before the same action.

More information: