cypress-recurse is a small utility for re-running Cypress commands until a predicate function passes. This blog post collects several videos I have recorded explaining how to code cypress-recurse
from zero all the way to publishing the NPM module and using it in other tests.
- The problem
- Recursively call cy.task from the test
- Reusable function
- Iteration limit
- Timeout
- Options object
- Adding types via JSDoc comments
- Published NPM package
- Use case 1: find the downloaded file
- Use case 2: visual testing retries
- recurse vs test retries
The problem
Recently a user asked how the test can do the following: reload the page until it shows the number 7. The page shows a random digit on every load, so we need to run cy.reload()
until the page shows it. The final solution would look like this:
The user tried to implement the test using a JavaScript while
loop which only crashes the browser window
1 | // ⛔️ DOES NOT WORK, crashes the browser |
In the next several videos I show the correct way of implementing the test and progressively refactor it to be a reusable function. You can read a short description of every step and watch the embedded videos from the YouTube playlist. The final solution is available on NPM as cypress-recurse module.
Recursively call cy.task from the test
First, let's figure out what goes wrong with the while
loop, and how a test can avoid the runaway loop by calling an intermediate function recursively. The video below does that and then derives the correct solution.
The final solution uses the checkNumber
function - it calls the Cypress command, checks the yielded value. If the value is not work we need, it calls itself recursively.
1 | it('yields 7 from the task', () => { |
Reusable function
It makes sense to factor out the solution and make it usable from other tests.
The final result is a reusable little function that can call the given two functions: one is a chain of Cypress commands, another one is a predicate function checking the result to know when to stop.
1 | it('yields 7 from the task (recursive)', () => { |
Iteration limit
If our predicate never return true, we can execute the Cypress commands until the end of times. We better limit the maximum number of attempts. The next video adds the maximum iteration limit to the function.
The solution uses only local variables without global state
1 | it('yields 7 from the task (limit)', () => { |
Timeout
Most Cypress commands use a time limit, not an attempt counter limit. Thus the next improvement we are making is adding a time limit to the recurse
function.
Again, we are using arguments and avoid global state
1 | it('yields 7 from the task (time limit)', () => { |
Options object
We want to use both the iteration and time limit and also add "log" flag to enable logging. Using positional arguments quickly becomes hard, thus we switch to using an options object.
Here is the options pattern, including applying the defaults
1 | it('yields 7 from the task (options object)', () => { |
The user can omit the options, or specify just some of them.
Adding types via JSDoc comments
We want to make the reusable function recurse
... easier to reuse. Having the type information for the function's signature is a big help to the users, but I do not want to write TypeScript. Thus I take a middle road by adding JSDoc comments describing the types of the arguments. Modern code editors like VSCode can read and use JSDoc type comments to show IntelliSense.
Here are these JSDoc comments
1 | /** |
Published NPM package
Our reusable function is ready - we can now move out from a Cypress test and into its own repository bahmutov/cypress-recurse and publish it on NPM.
We definitely need a CI setup and automated dependency updates to make sure our NPM package is working with the newer versions of Cypress.
Use case 1: find the downloaded file
Now that we have cypress-recurse
, let's use it. Calling cy.task
repeatedly to find a file is a good use case. Here is an example from our Cypress example recipes where we try to find a downloaded file
1 | import { recurse } from 'cypress-recurse' |
Use case 2: visual testing retries
In the blog post Canvas Visual Testing with Retries I show how to use the cypress-recurse
to download and compare HTML5 canvas to a previously saved image. The code is using recurse
1 | it('smiles broadly', () => { |
You can see how I am writing the above test in this video and find the full project at bahmutov/monalego
recurse vs test retries
Cypress already includes test retries. If a test fails, you can re-run the entire test up to N times, called attempts. The test retries could be configured globally, per suite, or per individual test:
1 | // this test can fail, then retry up to 2 times more |
There are differences between the test retries and cypress-recurse
:
- test retries only apply to the body of the test, they do not retry the test hooks like
before
andbeforeEach
- test retries re-run the entire test, while
recurse
only retries the given commands.
In general I would consider the recurse
a "normal" part of running tests, similar to the command retry-ability, but extended to an arbitrary number of commands. Test retries are a mechanism to fight the truly unpredictable test flake, like slow responses from the server.