JavaScript Testing

Diving deeper into commands and selectors in Cypress

Marcin Wanago
JavaScriptTesting

In the previous part of this series, we’ve learned the basics of the Cypress framework. Although the above knowledge is enough to write our first tests, there is much more to explore. In this article, we dive a bit deeper into the mechanisms of Cypress. By doing so, we can avoid some troubles along the way and make our tests run smoother.

Asynchronous nature of Cypress commands

An example of a Cypress command is the  cy.get function. So far, we’ve written quite a few selectors using it. We might think that under the hood, it works in the same way as regular JavaScript DOM selectors. That’s not exactly the case here.

1cy.get('#search_input');
2cy.get('#search_submit');
1document.querySelector('#search_input');
2document.querySelector('#search_submit');

We’ve already pointed out that Cypress retries the  cy.get selector for us. Either it succeeds, or a timeout is reached, and the test fails. In fact, the  cy.get is asynchronous!

We might think that the above operations can run in parallel if they are asynchronous. That’s not the case, though. Let’s inspect this test:

1cy.visit('http://wanago.io')
2 
3cy.get('#search_input')
4  .type('JavaScript')
5 
6cy.get('#search_submit')
7  .click();
8 
9cy.get('#search_term').should('contain.text', 'JavaScript');

Even though the above functions are asynchronous, they run sequentially, To better visualize this, we can think of executing Cypress commands as putting elements in a queue. The details of how they operate are managed under the hood. The above code results in the following actions:

  • Opening a website and waiting for the load event to fire
  • Looking for the input and filling it with value and retrying until it is found in DOM
  • Looking for the button and clicking it while retrying until it is found and clickable
  • Traversing the DOM tree looking for the term “JavaScript” in the search title performing the assertion until it succeeds

The waiting and retrying occur before the next step begins. As you can see, Cypress does quite a lot under the hood to keep our code simple and clean.

Because commands in Cypress are asynchronous and promised-based, their return value has the  then function, among others. We can use it to interact with the result of the promise.

1cy.get('#search_term')
2  .then(searchTerm => {
3    expect(searchTerm).to.contain.text('JavaScript');
4  })

The expect is a global function that acts in a similar way to the assertions that we defined as strings passed to the  should function. Using it, we could manipulate the element in a more complicated way.

The  should function also accepts a callback function in a very similar way to the  then function.

1cy.get('.radio-button').should(radioButton => {
2  expect(radioButton).to.be.checked();
3})

It differs from the  then function because Cypress reruns the callback of the  should function until no assertions throw inside it.

The  and() function is an alias of  should()

More on selectors

So far, the only selector function that we’ve used is  cy.get. The Cypress framework offers more:

Find

The  find function allows us to find a descendant of a particular DOM element. We need to chain it after another command that looks for DOM elements, for example,  cy.get.

1cy.get('#form').find('.radio-button');

Within

Works similar to  find, but accepts a callback function.

1cy.get('#form').within(form => {
2  cy.get('#name_input').type('wanago.marcin@gmail.com');
3  cy.get('#password_input').type('strongPassword123');
4  form.submit();
5})

An important note is that in the example above,  cy.get('#name_input') and  cy.get('#password_input') traverse the DOM tree only within the  #form element.

Children

With the  children function, we can get the children of a DOM element. If we don’t provide it with a selector, it gives us all children.

1cy.get('.menu').children('.active');
1cy.get('.menu').children().should('have.length', 4);

Parents

It allows us to search through the ancestors of an element in a DOM tree, optionally filtered by a selector.

1cy.get('.menu-element').parents('.menu');

Contains

The  contains function makes possible looking for an element containing a particular text value.

1cy.get('#registration_form').contains('Click here to register').click();

Eq

Using the  eq function, we can get an element in an array that has a particular index.

1cy.get('#ingredients_list').eq(1).should('contain', 'onion');

Good practices regarding selectors

A considerable problem when writing End-to-End tests is the issue of dealing with UI that is subject to change. When using Cypress, we write selectors all the time. To prevent them from breaking, we need to put some thought into it. If we rely too much on classes and tag names, things might get complicated when we update our interface. To prevent this from happening, we should write selectors that work even if there are some changes to the UI.

Fortunately, HTML5 was designed to be easily extended. The  data-* attribute gives us an option to store additional information on regular HTML elements. Any attribute that starts with  data- is a data attribute. We can make use of the above by adding the  data-testid attribute.

Other popular naming conventions are  data-cy and  data-test
1<button
2  class="btn"
3  data-testid="submit"
4>
5  Submit
6</button>
1cy.get('[data-testid=submit]').click()

By correctly inserting the above attributes, we can make sure that our selectors will not suffer when we make some changes to the UI.

Summary

In this article, we’ve dived more in-depth into the mechanisms of the Cypress framework. With a more extensive knowledge of how Cypress commands work, we can understand them better and make our code less prone to mistakes. Today we’ve also learned more about selectors and how we can use additional functions such as  find and  contains.