Retry-ability
For more information see Cypress retry-ability guide.
Element added to the DOM
Let's test a situation where the application inserts a new element to the DOM
<div id="app-example"></div>
<script>
setTimeout(() => {
document.getElementById('app-example').innerHTML =
'<div id="added">Hello</div>'
}, 2000)
</script>
Because Cypress querying commands have the built-in existence check, all we need to do is to ask:
// cy.get will retry until it finds at least one element
// matching the selector
cy.get('#added')
Element becomes visible
If an element is already hidden in the DOM and becomes visible, we can retry finding the element by adding a visibility assertion
<div id="app-example">
<div id="loader" style="display:none">Loaded</div>
</div>
<script>
setTimeout(() => {
document.getElementById('loader').style.display = 'block'
}, 2000)
</script>
Because Cypress querying commands have the built-in existence check, all we need to do is to ask:
// the cy.get command retries until it finds at least one visible element
// matching the selector
cy.get('#loader').should('be.visible')
Element becomes visible using jQuery :visible selector
You can optimize checking if an element is visible by using the jQuery :visible
selector
<div id="app-example">
<div id="loader" style="display:none">Loaded</div>
</div>
<script>
setTimeout(() => {
document.getElementById('loader').style.display = 'block'
}, 2000)
</script>
// cy.get has a built-in existence assertion
cy.get('#loader:visible').should('have.text', 'Loaded')
// or use a single cy.contains command
cy.contains('#loader:visible', 'Loaded')
Matching element's text
Imagine the element changes its text after two seconds. We can chain cy.get and cy.invoke commands to get the text and then use the match
assertion to compare the text against a regular expression.
<div id="example">loading...</div>
<script>
setTimeout(() => {
document.getElementById('example').innerText = 'Ready'
}, 2000)
</script>
Notice that .invoke('text')
can be safely retried until the assertion passes or times out.
cy.get('#example')
.invoke('text')
.should('match', /(Ready|Started)/)
Rather than splitting cy.get
+ cy.invoke
commands, let's have a single command to find the element and match its text using the cy.contains command.
// equivalent assertion using cy.contains
// https://on.cypress.io/contains
cy.contains('#example', /(Ready|Started)/)
Multiple assertions
<div id="example"></div>
<script>
setTimeout(() => {
document.getElementById('example').innerHTML = `
<button id="inner">Submit</button>
`
}, 2000)
setTimeout(() => {
document
.getElementById('inner')
.setAttribute('style', 'color: red')
}, 3000)
</script>
cy.get('#inner')
// automatically waits for the button with text "Submit" to appear
.should('have.text', 'Submit')
// retries getting the element with ID "inner"
// until finds one with the red CSS color
.and('have.css', 'color', 'rgb(255, 0, 0)')
.click()
Counts retries
One can even count how many times the command and assertion were retried by providing a dummy .should(cb)
function. A similar approach was described in the blog post When Can The Test Click?.
<div id="red-example">Will turn red</div>
<script>
setTimeout(() => {
document
.getElementById('red-example')
.setAttribute('style', 'color: red')
}, 800)
</script>
let count = 0
cy.get('#red-example')
.should(() => {
// this assertion callback only
// increments the number of times
// the command and assertions were retried
count += 1
})
.and('have.css', 'color', 'rgb(255, 0, 0)')
.then(() => {
cy.log(`retried **${count}** times`)
})
Merging queries
Instead of splitting querying commands like cy.get(...).first()
use a single cy.get
with a combined CSS querty using the :first
selector.
<ul id="items">
<li>Apples</li>
</ul>
<script>
setTimeout(() => {
// notice that we re-render the entire list
// and insert the item at the first position
document.getElementById('items').innerHTML = `
<li>Grapes</li>
<li>Apples</li>
`
}, 2000)
</script>
How do we confirm that the first element in the list is "Grapes"? By using a single cy.get
command.
cy.get('#items li:first').should('have.text', 'Grapes')
// equivalent
cy.contains('#items li:first', 'Grapes')
Query the element again
<div id="cart">
<div>Apples <input type="text" value="10" /></div>
<div>Pears <input type="text" value="6" /></div>
<div>Grapes <input type="text" value="5" /></div>
</div>
cy.get('#cart input') // query command
.eq(2) // query command
.clear() // action command
.type('20') // action command
The above test is ok, but if you find it to be flaky, add more assertions and query the element again to ensure you find it even if it re-rendered on the page.
// merge the cy.get + cy.eq into a single query
const selector = '#cart input:nth(2)'
cy.get(selector).clear()
// query the input again to make sure has been cleared
cy.get(selector).should('have.value', '')
// type the new value and check
cy.get(selector).type('20')
cy.get(selector).should('have.value', '20')
Watch the explanation for the above test refactoring in my video Query Elements With Retry-Ability To Avoid Flake.
Element appears then loads text
All assertions attached to the querying command should pass with the same subject.
📺 Watch this example explained in the video Element Becomes Visible And Then Loads Text.
<div id="app-example">
<div id="loader" style="display:none">Loaded</div>
</div>
<script>
setTimeout(() => {
document.getElementById('loader').style.display = 'block'
}, 2000)
setTimeout(() => {
document.getElementById('loader').innerText =
'Username is Joe'
}, 4050)
</script>
Notice that the element becomes visible after 2 seconds, well within the default command timeout of 4 seconds. But it gets the expected text slightly after 4 seconds. Both assertions must pass together, thus the following test fails.
cy.get('#loader')
.should('be.visible')
.and('have.text', 'Username is Joe')
One solution is to increase the timeout in cy.get
command
cy.get('#loader', { timeout: 5_000 })
.should('be.visible')
.and('have.text', 'Username is Joe')
Alternative: split the assertions by inserting another cy.get
element command to "restart" the timeout clock.
cy.get('#loader').should('be.visible')
cy.get('#loader').should('have.text', 'Username is Joe')
Item is added to the local storage
Let's confirm the application sets the productId
in the localStorage
object. Unfortunately, we do not know when the application is going to set it, only that it will be within a couple of seconds after clicking the button "Save".
<button id="save">💾 Save</button>
<script>
document
.getElementById('save')
.addEventListener('click', () => {
setTimeout(() => {
window.localStorage.setItem('productId', '1234abc')
}, 1500)
})
</script>
cy.contains('button', 'Save').click()
cy.window() // query
.its('localStorage') // query
.invoke('getItem', 'productId') // query
.should('exist') // assertion
.then(console.log) // command
.should('match', /^\d{4}/) // assertion
By inserting an assertion should('exist')
after queries, we retry checking the local storage until the item is found. Then we can use other commands, like cy.then(console.log)
that do not retry.
Fun: call function using retry-ability
📺 Watch the explanation for these recipes in Fun With Cypress Query Commands And Asynchronous Functions.
Usually we have data subject passing through Cypress queries and functions until the assertions pass. For example, the subject could be an object and its property value:
// use max 10 but for demos use something larger like 50
// to show off retries
const getRandomN = () => Cypress._.random(10, false)
const o = {}
const i = setInterval(() => (o.n = getRandomN()), 0)
cy.log('**subject is an object**')
cy.wrap(o) // subject is an object {n: ...}
.its('n') // subject is a number
.should('equal', 7)
We can flip it around and wrap the function getRandomN
itself as Cypress command chain subject. It will sit there doing nothing until we call it. Tip: you can invoke any function using Function.prototype.call
or Function.prototype.apply
methods.
cy.log('**subject is a function**')
cy.wrap(getRandomN) // subject is function "getRandomN"
.invoke('call') // subject is a number
.should('equal', 7)
The above chain of Cypress queries retries calling getRandomN.call
as quickly as it can until the assertion passes.
😒 Unfortunately, cy.invoke
query command does not yield the resolved value from asynchronous functions, so you cannot write retry-able asynchronous chains, like you can do using cypress-recurse plugin.
// 🚨 DOES NOT WORK
cy.wrap(fetch)
.invoke('call', null, '/get-n')
.invoke('json')
.its('n')
.should('equal', 7)
More fun: invoke an asynchronous function with retry-ability
If you look at the above example, maybe you think it is impossible to make cy.invoke
wait for the resolved value. Do not worry, life finds a way. Here is super hacky way to retry fetching data from an external endpoint using fetch
, cy.invoke
, and built-in retries
First, I will add a query named nsync
to implement polling.
Cypress.Commands.addQuery('nsync', () => {
let value
return (subject) => {
if (typeof subject === 'object' && 'then' in subject) {
subject.then((x) => (value = x))
const result = value
value = undefined
return result
} else {
return subject
}
}
})
After each asynchronous function call, like fetch
or res.json
, add nsync()
query to "synchronize" it.
cy.wrap(fetch)
.invoke('call', null, 'http://localhost:4200/random-digit', {
headers: { 'x-delay': 100 },
})
.nsync()
.invoke('json')
.nsync()
.its('n')
.should('equal', 7)