It is hard to test a dynamic site that keeps changing. How do you retry checking the site if the application is updating the DOM? Let's say the application is inserting new items into the list.
How would you confirm the last item in the list is "Potatoes" (imagine you want to be a vice-president some day)? In Cypress versions before v12, you could use an assertion to wait for the app to finish updating before checking the list. For example, if you expected 3 items, you could write:
1 | cy.visit('cypress/prices-list.html') |
The test worked in all Cypress versions because after the assertion .should('have.length', 3)
passed, the list of DOM elements would not change. We could safely grab the last element using cy.last()
command and expect it to have the right text. If we forgotten the .should('have.length', 3)
assertion, the test would fail and in a very developer-unfriendly way. Here is the test and its behavior in Cypress v11.
1 | cy.get('#prices li') // command |
Why does the test fail? Because Cypress before v12 only re-ran the last command before the assertion. Thus at the start of the test, the page was showing 1 item, and the cy.get(...)
yielded it.
1 | cy.get('#prices li') // command [<li>Oranges</li>] |
The cy.last()
got a list with one LI
element, and yielded it
1 | cy.get('#prices li') // command => [<li>Oranges</li>] |
The assertion .should('contain', 'Potatoes')
fails, and Cypress goes back to re-run the .last()
command again and again. Cypress never went to re-run the cy.get(...)
command, thus cy.last()
never "saw" the updated list...
🎁 You can find the code used in this blog post in the repo bahmutov/cypress-map-example.
Queries
Cypress has a lot of commands and version 12 now separates commands that change the application (like cy.click
, cy.type
, cy,task
) from queries: the commands that do NOT change the application. For example, cy.get
command is a query, since it just inspects the DOM, but never changes it. Other common query commands are cy.find
, cy.contains
, cy.its
, and cy.invoke
. With such separation, Cypress v12 changed how it re-runs the command when an assertion fails: it now re-runs the entire chain of queries attached to the assertion.
1 | cy.get('#prices li') // query ↰ |
So if the list at first has 1 element and it does not have text "Potatoes", the test goes back to cy.get('#prices li')
command and tries to fetch the DOM elements again. After some time, it gets a list of 2 elements - still the last element does not have the right root vegetable. So again, it goes to the cy.get
, gets 3 elements, and then the last element is "Potatoes" and the test finishes.
No more weird errors, and the test syntax look readable and clear.
You can have multiple queries in the chain, for example let's check the price in a very contrived way:
1 | cy.get('#prices li') // => jQuery<DOM elements> |
I commented what every query command yields.
Tip: I would use a single cy.contains
command to write the test above:
1 | cy.contains('#prices li:last .money', '$0.20') |
Adding custom query commands
You can add your own query commands to be retried as part of the chain. Let's say we want to extract text from each DOM element. We could create a new query command text
:
1 | // make sure the assertion message shows the entire array |
I have created a new plugin cypress-map that has several common query commands to help you write better tests. For example, the above test can be written as:
1 | // https://github.com/bahmutov/cypress-map |
The command cy.map
comes from the cypress-map
plugin. There are also cy.mapInvoke
, cy.reduce
, cy.tap
, and a few other commands.
Warning
When writing longer chains of queries, make sure you do NOT include regular Cypress commands, since they will prevent the test from retrying the entire chain. It is easy to accidentally insert a cy.then(console.log)
command when you want to debug the test and break the retry-ability.
1 | cy.get('#prices li') |
To help with this problem, cypress-map
has a query cy.tap
command. The same test can be correctly written as
1 | // https://github.com/bahmutov/cypress-map |
Or even simpler .tap('texts')
🎓 I have covered the cypress-map
commands in my Cypress Plugins course.