Let the Engineers Speak: Selectors in Cypress

Getting Started — Published April 7, 2023

Earlier this month, Applitools hosted a webinar, Let the Engineers Speak: Selectors, where testing experts discussed one of the most common pain points that pretty much anyone who’s ever done web UI testing has felt. The first article in our two-part series defined our terms and challenges of locating web elements using selectors, as well as recapped Christian’s WebDriverIO selectors tutorial and WebdriverIO Q&A. Be sure to read that article first to help set context.

Introducing our experts

I’m Pandy Knight, the Automation Panda. I moderated our two testing experts in our discussion of selectors:

Locating web elements with Cypress

Filip walked us through selectors in Cypress, starting with the basics and using Trello as an example app.

Note: All the following text in this section is based on Filip’s part of the webinar. Filip’s repository is available on GitHub.

When talking about selectors in web applications, we are trying to target an HTML element. Cypress has two basic commands that can help with selecting these elements: cy.get and cy.contains. In Filip’s example demo, he has VS Code on the left side of his screen and Cypress running in graphic user interface mode (open mode) on the right side.

The example test uses different kinds of selector strategies, with each approach using either the get command or the contains command.

Locating elements by class, ID, or attribute

The first first approach calls cy.get(‘h2’) using the H2 tag. In the Cypress window, if you hover over the get h2 command, it will highlight the selector that has been selected.

If you’re struggling to find the right selector in Cypress, there’s this really nice tool that basically works like the inspect element tool, but you don’t have to open the dev tools if you don’t want to.

In our other three approaches with the get command, we are using the class, the ID, and the attribute, respectively:

  cy.get('h2')
  cy.get('.board') // class
  cy.get('#board-1') // id
  cy.get('[data-cy=board-item]') // attribute

The syntax for the get commands is basically just CSS selectors. If you are selecting an element with a class of board, you need to prefix the class with a dot. You can even write more complex selectors, like adding an h2 tag inside the element that has the class board like cy.get(‘.board > h2’). The options are endless. If you have ever worked with CSS, you know that you can pretty much target any element you like.

Locating elements by text

Another strategy that we have in Cypress is selecting elements by text. This approach may not always work, but in cases like a login, sign up, sent, or okay button, these usually need to have specific text. To select an element using text, we use the contains command. The contains command will only select one element, and it’s actually going to look for elements within some context.

The example uses two different texts to search: cy.contains(‘My Shopping’) and cy.contains(‘My’). On the Trello board page, ‘My Shopping’ appears once and ‘My’ appears twice. The contains call using ‘My’ will return with the first element that has the text ‘My’ on the page, which is ‘My Board’ in the example. So that’s something to watch out for. If you want to be more specific with the kind of element you want to select, you can actually combine these two approaches, selecting a CSS element and specifying the text which you want to find. For example, cy.contains(‘.board’, ‘My’) would return the correct element.

  cy.contains('My Shopping') // text
  cy.contains('My') // find the first one
  cy.contains('.board', 'My') // specify element to find

Locating elements using XPath

There are other selector strategies like using XPath. Cypress has an official plugin for XPath. If you install that, you will get the ability to select elements using XPath. You can use XPaths, but they may be harder to read. For example, cy.xpath(‘(//div[contains(@class, “board”)])[1]’) does the same thing as cy.get(‘board’).eq(0).

// Filter an element by index
cy.xpath('(//div(contains(@class, "board") ]) [1]')

// Select an element containing a specific child element
cy.xpath('//div [contains(@class, "list")] [.//div[contains(@class, "card")]]')

// Select an element by text
cy.xpath('//*[text()[contains(., "My Boards")]]')

// Select an element after a specific element
cy.xpath('//div[contains(@class, "card")][preceding::div[contains(., "milk")]]')
//Filter an element by index
cy.get('.board').eq(0)

// Select an element containing a specific child element
cy.get(".card").parents('.list')

// Select an element by text
cy.contains('My Boards')

// Select an element after a specific element
cy.contains('.card', 'milk').next('.card')

Filip’s recommendation is that you don’t really need XPaths. XPaths can be really powerful in traversing the DOM structure and selecting different elements, but there are other options in Cypress.

Traversing elements in Cypress

For our example, we have a Trello app and two lists with item cards. The first list has two cards, and the second has one card. We want to select each of the cards from a list. We can find the last card by doing a pair of commands cy.get(‘[data-cy=card]’).last(). First, we’re using the get command to target the cards, which will return all three cards. When you hover over your get command, you’ll see that all three cards are selected.

When you use the last command, it’s going to filter to the last card, on which you can then do some action like click or make an assertion.

You can also traverse up or down using Cypress. The next example cy.contains(‘[data-cy=card]’, ‘Soap’).parents(‘[data-cy-list]’) tries to target a parent element using text to select the card and then looks for a parent element using a CSS selector. This example is going to select our whole list.

Alternatively, if you want to traverse between the next element or a previous element, that can be done easily with cy.contains(‘[data-cy=card]’, Milk’).next() or cy.contains(‘[data-cy=card]’, Milk’).prev().

it.only('Find an element on page', () => {

  cy.visit('/board/1')

  // find last card
  cy.get('[data-cy=card]')
    .last()

  // find parent element
  cy.contains('[data-cy=card]', 'Soap')
    .parents('[data-cy=list]')

  // find next element
  cy.contains('[data-cy=card]', 'Milk')
    .next()

  // find next element
  cy.contains('[data-cy=card]', 'Bread')
    .prev()

});

You may sometimes deal with tricky situations like elements loading in at different times. The DOM is going to be flaky, because DOM is pretty much always flaky, right? Things get loaded, things get re-rendered, and so on. There was a Cypress 12 update that makes sure that when we are selecting an element and we have an assertion about that element, if the assertion does not pass, we are going to re-query our DOM. So in the background, these should command this assertion and our querying is interconnected.

it('Dealing with flaky situations', () => {

  cardsLoadRandomly(10000)

  cy.visit('/board/1')

  cy.get('[data-cy=card]')
    .last()
    .should('contain.text', 'Soap')

});

Locating elements in a shadow DOM

Dealing with shadows DOM can be tricky. By default, Cypress is not going to look for shadow DOM elements, only DOM elements. If we want to include the shadow DOM elements, we have a few options in Cypress.

it('closes side panel that is in shadow DOM', () => {

  cy.visit('https://lit.dev/playground/#sample=docs%2Fwhat-is-lit&view-mode=preview')

  cy.get('litdev-drawer', { includeShadowDom: true })
    .find('#openCloseButton', { includeShadowDom: true })
    .click()

});

If we don’t have too many of these shadow DOM elements on the page, we can use either of two commands that do essentially the same thing:

  • cy.get(‘litdev-drawer’, { includeShadowDom: true }). find(‘#openCloseButton’, { includeShadowDom: true })
  • cy.get(‘litdev-drawer’).shadow().find(‘#openCloseButton’)

Alternatively, if we have a lot of shadow elements in our application, we can use the approach of changing the option in the config file includeShadowDom from false to true. By default it is set to false, but if you set it to true and save your configuration, Cypress will look for shadow DOM elements automatically.

Locating elements within an iframe

Cypress does not have an iframe command. Whenever we traverse, we interact with this timeline that we have in Cypress to see the state of our application as it was during the execution of that command. In order to support the iframes, Cypress would have to do the snapshot of the iframe as well. Essentially, if you want to access an iframe using Cypress, you write a Cypress promise that will resolve the contents of the iframe.

You can add a custom command to your code base to retry the iframe. So if the iframe takes a little bit of time to appear, it’ll resolve when it appears or when the timeout eventually times out. Another way of dealing with that is installing a plugin.

it('dealing with iframes', () => {

  cy.visit('https://kitchen.applitools.com/ingredients/iframe')

  cy.iframe('#the-kitchen-table')
    .find('section')
    .should('contain.text', 'The Kitchen')

});

Cypress selector recommendations

Cypress works best when you have your test code along with the source code in the same repository. These are the recommendations that Cypress gives in the documentation:

SelectorRecommendation
Generic HTML tags, elements, or classes like:
cy.get(‘button’)
cy.get(‘.btn.btn-large’)
Never recommended.
Lack content or are often paired with styling and therefore highly subject to change.
IDs, HTML name attributes, or title attributes like:
cy.get(‘$main’)
cy.get(‘[name=”submission”]’)
Sparingly recommended.
Still coupled to style or JS event listeners, or coupled to the name attribute, which has HTML semantics.
Text attributes like:
cy.contains(‘Submit’)
Recommendation depends.
This is only suggested for elements where text is not expected to change.
Custom IDs or data test attributes like:
cy.get(‘[data-cy=”submit”]’)
Always recommended.
Isolated from all changes.

The best recommendation is to create custom IDs for elements. As you create your test, you will create those IDs as well. So if you need a test, create an attribute, and that will do a single job, be available for end-to-end test.

Filip recommends two blog posts about selector strategies:

Adding visual testing

We can go beyond accessibility and functional tests and we can add visual tests into our test suite. We can do this with Applitools. Create a free Applitools account to access your API key, and follow our tutorial for testing web apps in JavaScript using Cypress.

Note: Do not share your API key with others. For this demo, Filip rotated his API key, so it’s no longer valid.

The visual testing will have three basic parts:

  1. Open your “eyes”.
  2. Check the window.
  3. Close your “eyes”.

Applitools Eyes will then validate the snapshot it takes against the current baseline. But sometimes we don’t want to test certain areas of the page like dynamic data. Applitools is really intelligent about that and has options in the Test Manager to add ignore regions, but we can help it with our own ignore regions by using selectors.

it('check home screen', () => {

  cy.eyesOpen({
    appName: 'Trello',
  })

  cy.visit('/')

  cy.get('[data-cy=board-item]')
    .should('be.visible')

  cy.eyesCheckWindow({
    ignore: {
      selector: 'hide-in-applitools'
    }
  })

  cy.eyesClose()

});

In the example, we are showing the ignore region and telling which selector it should ignore. You can create a custom hide-in-applitools class to add to all your elements you want to hide, and Applitools will automatically ignore them.

Cypress selectors Q&A

After Filip shared his Cypress demonstration, we turned it over to our Q&A, where Filip responded to questions from the audience.

Using Cypress commands

Question: I was recently working on XPaths and Cypress. I understand cypress-xpath is deprecated and I was suggested to use @cypress/xpath. Is that the case?
Filip’s response: I don’t know. I know this was the situation for the Cypress grab package, which can grab your test so you can just run a subset of your Cypress test. It was a standalone plugin and then they sort of moved it inside a Cypress repository. So now it’s @cypress/grab. I believe the situation with Xpath might be similar.

Using the React testing library

Question: What are your thoughts about using the React testing library plugin for locating elements in Cypress using their accessibility roles and names – findByRole, findByLabel?
Filip’s response: [To clarify the specific library mentioned] there’s this testing library, which is a huge project and has different subprojects. One is the React testing library and their Cypress testing library, and they basically have those commands inside this (findByRole, findByPlaceholder, etc.). So I think the Cypress testing library is just an implementation of the thing you are mentioning. So what’s my opinion on that? I’m a fan. Like I said, I’m not using it right now, but it does two things at once. You can check your functionality as well as accessibility. So if your test fails, it might be annoying, but it also might mean you need to work on the accessibility of the app. So I recommend it.

Using div.board or .board

Question: Do you have a stance/opinion on div.board versus .board for CSS selectors?
Filip’s response: Not really. As I mentioned, I prefer adding my own custom selectors, so I don’t think I would have this dilemma too often.

Tracking API call execution

Question: How can we find out if an API call has executed when we click on an element?
Filip’s response: In Cypress, it’s really easy. There’s this cy.intercept command in which you can define the URL or the method or any kind of details about the API call. And you need to make sure that you put the cy.intercept command before the click happens. So if the click triggers that API call you can then use cy.wait and basically refer through alias to that API call. I suggest you take a look into the intercept command in Cypress docs.

Working with iframes

Question: Is it possible to select elements from an iframe and work with an iframe like normal?

Filip’s response: Well, I don’t see why not. It is a little bit tricky. Iframe is just another location, so it’ll always have a source pointing to a URL. So, alternatively, if you are going to do a lot of testing within that iframe, maybe you just want to open that and test the page that is being iframed. It would be a good thing to consult with developers to see if there’s a communication between the iframe and the parent frame and if there’s anything specific that needs to be covered. But if you do like a lot of heavy testing, I would maybe suggest to open the URL that the iframe opens and test that.

Learn more

So that covers our expert’s takeaways on locating web elements using Cypress. If you want to watch the demos, you can access the on-demand webinar recording.

If you want to learn more about any of these tools, frameworks like Cypress, WebDriverIO, or specifically web element locator strategies, be sure to check out Test Automation University. All the courses and content are free.
Be sure to register for the upcoming Let the Engineers Speak webinar series installment on Test Maintainability coming in May. Engineers Maaret Pyhäjärvi from Selenium and Ed Manlove from Robot Framework will be discussing test maintenance and test maintainability with a live Q&A.

Are you ready?

Get started Schedule a demo