Cypress V12 Is A Big Deal

How Cypress version 12 retries the chains of multiple query commands.

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.

New items are added to 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
2
3
4
5
cy.visit('cypress/prices-list.html')
cy.get('#prices li') // command
.should('have.length', 3) // assertion
.last() // command
.should('contain', 'Potatoes') // assertion

Confirming the list has finished loading before getting the last element

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
2
3
cy.get('#prices li') // command
.last() // command
.should('contain', 'Potatoes') // assertion

The test fails to find the new last item in Cypress v11

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
2
3
cy.get('#prices li') // command [<li>Oranges</li>]
.last() // command
.should('contain', 'Potatoes') // assertion

The cy.last() got a list with one LI element, and yielded it

1
2
3
cy.get('#prices li') // command => [<li>Oranges</li>]
.last() // command => <li>Oranges</li>
.should('contain', 'Potatoes') // assertion

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
2
3
cy.get('#prices li') // query                       ↰
.last() // query | retries
.should('contain', 'Potatoes') // assertion fails 」

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.

The test passes as expected in Cypress v12

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
2
3
4
5
cy.get('#prices li')  // => jQuery<DOM elements>
.last() // => jQuery<DOM element>
.find('.money') // => jQuery<DOM element>
.invoke('text') // => string
.should('equal', '$0.20')

I commented what every query command yields.

Checking the price in the last item

Tip: I would use a single cy.contains command to write the test above:

1
cy.contains('#prices li:last .money', '$0.20')

Using jQuery :last selector and cy.contains command

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
2
3
4
5
6
7
8
9
10
11
12
13
// make sure the assertion message shows the entire array
chai.config.truncateThreshold = 200

Cypress.Commands.addQuery('text', () => {
return ($elements) => Cypress._.map($elements, 'innerText')
})

it('checks the texts', () => {
cy.visit('cypress/prices-list.html')
cy.get('#prices li')
.text()
.should('deep.equal', ['Oranges $0.99', 'Mango $1.01', 'Potatoes $0.20'])
})

The test uses custom query command

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
2
3
4
5
6
7
8
9
// https://github.com/bahmutov/cypress-map
import 'cypress-map'

it('checks the texts', () => {
cy.visit('cypress/prices-list.html')
cy.get('#prices li')
.map('innerText')
.should('deep.equal', ['Oranges $0.99', 'Mango $1.01', 'Potatoes $0.20'])
})

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
2
3
4
cy.get('#prices li')
.text()
.then(console.log) // this will break the test
.should('deep.equal', ['Oranges $0.99', 'Mango $1.01', 'Potatoes $0.20'])

The test does not retry because of cy.then command

To help with this problem, cypress-map has a query cy.tap command. The same test can be correctly written as

1
2
3
4
5
6
7
8
9
10
// https://github.com/bahmutov/cypress-map
import 'cypress-map'

it('checks the texts', () => {
cy.visit('cypress/prices-list.html')
cy.get('#prices li')
.text()
.tap(console.log, 'texts')
.should('deep.equal', ['Oranges $0.99', 'Mango $1.01', 'Potatoes $0.20'])
})

Or even simpler .tap('texts')

Debug the information flowing through the chain using cy.tap

🎓 I have covered the cypress-map commands in my Cypress Plugins course.