🎁 You can find these tests in the repo bahmutov/better-cypress-each-example.
Imagine you have an application where you have bunch of table rows. After you click a button in a row, it reveals a random number from 0 to 9. Your goal is to click the buttons until you get the number 7.
Can we write a Cypress test that would iterate over potentially all buttons, but stop when it finds 7?
Cypress cy.each command
Cypress has cy.each command. This command is nice for doing the same set of actions for each element. For example, you can quickly click every button on the page
1 | it('clicks every button', () => { |
You can see me writing the above spec in the video Iterate Over Table Rows And Click A Button In Each Row Using cy.each Command and below:
The test does not stop, even if the number 7 is clearly visible. The test does not even wait for the number to be revealed before moving to the next table row and clicking the next button. Not good. We can make the test wait, of course. We need to grab the next cell and confirm it shows a single digit number.
1 | it('clicks every button waits for the number', () => { |
But what about stopping if we see the number 7? We still to implement this.
Stop the iteration early
If you use a simple synchronous callbacks, you can return false
to stop the iteration early. For example, the next step that iterates over strings will stop when it sees the 3rd string.
1 | it('cy.each stops iteration when returning false', () => { |
Can we return or yield false
to stop the iteration if we use Cypress commands in the each(callback)
function? Something like this does NOT work:
1 | // 🚨 JUST FOR DEMO, INCORRECT TEST |
Unfortunately, the above test does not work. It quickly iterates over all buttons, queuing up all Cypress commands inside the each(callback)
. The queue after finding the buttons looks like this:
1 | - visit |
Even if you yield false
from the first button, there are all the commands already queued up and they cannot be removed. We need a way to avoid queuing unnecessary commands. To do this, we can queue cy.then(callback)
inside the each(callback)
. Inside the then(callback)
we can decide if we need to queue up more Cypress commands, or simply do nothing. Instead of returning or yielding false
, we will use a local variable to signal an early exit.
1 | it('stops when it sees 7', () => { |
You can see the derivation of the above test in the video Stop cy.each Iteration When Using Cypress Commands Inside The Callback Function and below:
Tip: in the video, I use the cypress-command-chain to visualize the already queued commands. Read the blog post Visualize Cypress Command Queue for more details.
Reusable each function
The boilerplate logic for iteration and early exit can be abstracted into a library. In fact, it looks so much like my recursive iteration in the plugin bahmutov/cypress-recurse that I just added the each
as one of its features. Import the each
into your spec and provide the callback function and the optional predicate function that tells each
when to stop. Without the predicate function, it simply iterates over the each subject item. To stop early, for example when we see the number 7, we write the test like this one:
1 | // https://github.com/bahmutov/cypress-recurse |
We use the each
function to create the callback to the cy.then
callback which will yield the list of items to iterate over. This is how the test looks in action:
You can see me writing the above test in the video Use each Function From cypress-recurse Plugin To Iterate And Stop or below:
Replace the cy.each command
Finally, we can replace the existing cy.each
command with the each
from cypress-recurse
. Here is how the spec could do this.
1 | // https://github.com/bahmutov/cypress-recurse |
Watch the video Overwrite The cy.each Command to see me writing the above test.