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:
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.
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.
cy.get('#prices li') // command [<li>Oranges</li>]
cy.last() got a list with one
LI element, and yielded it
cy.get('#prices li') // command => [<li>Oranges</li>]
.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.
Cypress has a lot of commands and version 12 now separates commands that change the application (like
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.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.
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:
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:
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
// 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:
cy.map comes from the
cypress-map plugin. There are also
cy.tap, and a few other commands.
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.
To help with this problem,
cypress-map has a query
cy.tap command. The same test can be correctly written as
Or even simpler
🎓 I have covered the
cypress-map commands in my Cypress Plugins course.