A way to re-run Cypress commands until a predicate function returns true
A way to re-run Cypress commands until a predicate function returns true
Jump to: Options, Examples, Debugging, Videos.
npm i -D cypress-recurse
# or use Yarn
yarn add -D cypress-recurse
import { recurse } from 'cypress-recurse'
it('gets 7', () => {
recurse(
() => cy.task('randomNumber'),
(n) => n === 7,
)
})
The predicate function should return a boolean OR use assertions to throw errors. If the predicate returns undefined, we assume it passes, see examples in expect-spec.js.
it('works for 4', () => {
recurse(
() => cy.wrap(4),
(x) => {
expect(x).to.equal(4)
},
).should('equal', 4)
})
Important: the commands inside the first function cannot fail - otherwise the entire test fails. Thus make them as "passive" as possible, and let the predicate function decide if the entire function needs to be retried or not.
Optionally, you can register cy.recurse
custom command by importing the cypress-recurse/commands
from your support file or individual specs.
// cypress/support/e2e.js
import 'cypress-recurse/commands'
// your E2E specs
it('works', () => {
cy.recurse(...)
})
Parameters to the cy.recurse
are the same as for named function: the command function, the predicate, followed by the options. For example, to check if the loader goes away after clicking on a button:
cy.recurse(
() => {
cy.get('button').click()
return cy.get('.loader').should(Cypress._.noop)
},
($el) => $el.length === 0,
{
log: false,
delay: 1000,
},
)
The recurse
function yields the subject of the command function.
import { recurse } from 'cypress-recurse'
it('gets 7', () => {
recurse(
() => cy.wrap(7),
(n) => n === 7,
).should('equal', 7)
})
it('gets 7 after 50 iterations or 30 seconds', () => {
recurse(
() => cy.task('randomNumber'),
(n) => n === 7,
{
log: true,
limit: 50, // max number of iterations
timeout: 30000, // time limit in ms
delay: 300, // delay before next iteration, ms
},
)
})
You can see the default options
import { RecurseDefaults } from 'cypress-recurse'
The log option can be a boolean flag or your own function. For example to pretty-print each number we could:
recurse(
() => {...},
(x) => x === 3,
{
log: (k) => cy.log(`k = **${k}**`),
}
)
You can simply print a given string at the successful end of the recursion
recurse(
() => {...},
(x) => x === 3,
{
log: 'got to 3!',
}
)
If the log
option is a function, it receives the current value, plus a data object with main iteration properties
log (x, data) {
// data is like:
// value: 3
// successful: false|true
// iteration: 3
// limit: 18
// elapsed: 1631
// elapsedDuration: "2 seconds"
}
See the log-spec.js
If you want to run a few more Cypress commands after the predicate function that are not part of the initial command, use the post
option. For example, you can start intercepting the network requests after a few iterations:
// from the application's window ping a non-existent URL
const url = 'https://jsonplaceholder.cypress.io/fake-endpoint'
const checkApi = () => cy.window().invoke('fetch', url)
recurse(checkApi, ({ ok }) => ok, {
post: ({ limit, value }) => {
// after a few attempts
// stub the network call and respond
if (limit === 1) {
// start intercepting now
console.log('start intercepting')
return cy.intercept('GET', url, 'Hello!').as('hello')
}
// you can use the value prop to look at the fetch results
},
})
The argument is a single object with iteration
, limit
, value
, reduced
, success
, elapsed
, and elapsedDuration
properties.
See the post-spec.js and find-on-page/spec.js.
By default, the last value is NOT passed to the post
callback. You can pass the last value by setting an option
recurse(fn1, predicate, {
post () {
...
},
postLastValue: true
})
A good combination is postLastValue: true
and post({ value, success })
where the value
is yielded by the first function, and the success
is the result of checking that value using the predicate function.
Note: if you specify both the delay and the post
options, the delay runs first.
Use the error
option if you want to add a custom error message when the recursion timed out or the iteration limit has reached the end.
recurse(
() => {...},
(x) => x === 3,
{
error: 'x never got to 3!',
}
)
Similar to reducing an array, the reduce
function has an option to accumulate / reduce the values in the given object. The following options work together
reduceFrom
is the starting value, like []
reduce(acc, item)
receives each value and the current accumulator valuereduceLastValue
is false by default, turn it on to call the the reduce
function with the last value (for which the predicate function has returned true)TODO: document the above options
If there is a reduced value, it will be passed as the second argument to the predicate function.
See the reduce-spec.js for examples.
If you are accumulating a reduced value, you can yield it instead of the last value. You can even yield both the last and the reduced values.
yield: "value"
yields the value that passed the predicate functionyield: "reduced"
yields the accumulated valueyield: "both"
yields an object with value
and reduced
propertiesSee the reduce-spec.js for examples.
Sometimes you want to retry N times or for M seconds, but not fail the test if the predicate is still false.
recurse(commandFn, predicate, {
doNotFail: true,
})
The yielded value in this case is not guaranteed. You can yield the last value, even if it does not pass the predicate by explicitly asking for it
recurse(
() => cy.wrap(4),
(x) => x === 10,
{
doNotFail: true,
yield: 'value',
},
).should('equal', 4)
This plugin also includes the each
function that iterates over the given subject items. It can optionally stop when the separate predicate function returns true.
import { each } from 'cypress-recurse'
it('iterates until it finds the value 7', () => {
cy.get('li').then(
each(
$li => ..., // do something with the item
$li => $li.text() === '7' // stop if we see "7"
)
)
})
The each
function yields the original or transformed items
const numbers = [1, 2, 3, 4]
cy.wrap(numbers)
.then(
each(
(x) => {
return 10 + x
},
// stop when the value is 13
(x) => x === 13,
),
)
.should('deep.equal', [11, 12])
See the each-spec.js file.
Experimental: this function can change its API at any moment.
If you need retries in your config / plugins code, you can use the included retry
function.
// your cypress.config.js
import {retry} from 'cypress-recurse/src/retry.js'
// or use require
const {retry} = require('cypress-recurse/src/retry')
async function getData() {
// your async function that returns data
// let's say it is a number
return n
}
e2e: {
setupNodeEvents(on, config) {
on('task', {
async fetchData () {
// we want to retry "getData" function
// until it returns a value above 200
const data = await retry(getData, n => n > 200, {
limit: 10, // limit calling getData to 10 times
delay: 100 // delay 100ms between attempts
})
return data
}
})
},
},
retry(fn, predicateFn, options?)
Options object can have the following properties
limit
the maximum number of attempts to call the given functiondelay
in milliseconds between calls to fn
log
log individual calls to fn
(by default the logging is off). Could be your own function (see below)extract
a custom function that takes the result of the fn
and returns the value to yieldExample: retry until the list is non-empty, then return a property from the first object
const n = retry(fn, (list) => list.length, {
extract: (list) => list[0].n,
})
Example: custom log function
// user log function receives these arguments
const log = ({ attempt, limit, value, successful }) => {
console.log(
'attempt %d of %d, value %o success: %o',
attempt,
limit,
value,
successful,
)
}
retry(fn, predicate, { log })
Use options log: true
and debugLog: true
to print additional information to the Command Log
recurse(getTo(2), (x) => x === 2, {
timeout: 1000,
limit: 3,
delay: 100,
log: true,
debugLog: true,
}).should('equal', 2)
📝 Read the following posts
Tip: use https://cypress.tips/search to search all my testing content
I have explained how this module was written in the following videos
🎓 This plugin is covered in multiple lessons in my Cypress plugins course
🎓 This plugin was used in my course Cypress Network Testing Exercises
Author: Gleb Bahmutov [email protected] © 2021
License: MIT - do anything with the code, but don't blame me if it does not work.
Support: if you find any problems with this module, email / tweet / open issue on Github
Copyright (c) 2020 Gleb Bahmutov [email protected]
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.