Note: source code for this blog post is in bahmutov/cypress-should-callback, see the spec files.
Cypress has a built-in retry-ability in almost every command - a concept that still keeps blowing my mind, and makes for a great demo during my presentations. For example, here is an application that adds elements to the page one by one
1 | const app = document.getElementById('app') |
My Cypress test for this is extremely simple.
1 | it('loads 3 elements', () => { |
Inside each Cypress command that does not change the state of the app (like get
, find
) there is a retry mechanism. The command will be executed, the result passed to the assertion that follows - and if the assertion passes, then the command completes successfully. If the assertion throws an error, the command is executed again, result passes to the assertion and so on and so on - until the assertion either passes, or the default timeout of 4 seconds ends. Here is an assertion that fails on purpose, looking for 4 items, while the application only shows 3.
1 | cy.get('#app div').should('have.length', 4) |
Imagine our test only checks for 2 items - it won't wait for 3 items to appear. The test passes as soon as the second item has been added.
1 | cy.get('#app div').should('have.length', 2) |
There is a huge variety of assertions you can use. Cypress comes with Chai, Chai-Sinon and Chai-jQuery assertions, and you can easily bring additional assertion libraries. The best part - the Cypress assertions do come with IntelliSense, which makes writing them less of memorization and more of expressing what you want to "see" in the test.
Should callback
If built-in assertions are not enough, you can pass your own callback function with assertions. For example, what if we want to confirm the text in each of the three items that appear? We could write 3 commands with 3 assertions.
1 | it('3 commands', () => { |
The test works, but the selectors are complex, and I would like to have a single assertion, rather than multiple ones. If I want a complex assertion that Cypress will use to rerun the previous command until it passes or times out - I need to pass a callback function to should(cb)
.
1 | it('loads 3 elements', () => { |
I can use any BDD and TDD style assertions inside the callback function, or even throw my own errors.
Notice how Cypress understands the explicit assertions we use inside the should
callback and shows them as pending. The assertions appear one by one - as first assertion passes, then the first and second assertions start running. When element <div>second child</div>
appears, all 3 assertions start running, and Command Log shows them as pending. Finally, when the third item appears, all assertions are shown as passing.
Should callback is an escape hatch - a way to write very complex logic to check the state of the application's user interface or internal state.
Returned value
Note that any returned value from should(cb)
is ignored - the subject passed to the next function is the original subject Cypress passed to the callback function.
1 | cy.get('#app div') |
If you want to change the subject - do it in the commands running after the assertion. At this point you know that assertion is passing and the application has the right UI and state (unless the app changes right after passing the assertion).
1 | it('loads 3 elements', () => { |
Custom commands with retry
If you want to write a custom Cypress command that would retry an assertion that follows it, it's not difficult. The code snippet below comes from cypress-xpath module we have written as good example.
1 | const xpath = (selector, options = {}) => { |
Easier custom commands with cypress-pipe
You can even remove all boilerplate of writing custom commands by using 3rd party module cypress-pipe. For example if the function that returns elements is our custom plain function, it will be retried with our should(cb)
function.
1 | /// <reference types="cypress" /> |
I love using cy.pipe
command because it allows me to compose "regular" functions in place in order to create a callback function. For example in the above example we get elements and then inside the should(cb)
iterate over them to get innerText
property. But we can use "standard" data transformation functions from a good functional library like Ramda to extract property innerText
from a given list of items.
1 | import { compose, map, prop } from 'ramda' |
In the above case, we don't even need should(cb)
with custom function, and we can use deep equality to confirm the text inside the elements.
1 | cy.document() |
We can always start with custom should(cb)
callback function, then if we notice general data transformations, refactor it to make it simpler and "standard-like". Readability and simplicity is the goal.
No cy commands allowed
Because the .should(cb)
callback can potentially run many times, you should never use any cy.<command>
methods (you can still use the static Cypress.<command>
methods). Trying to call cy.<command>
from the callback will try to schedule lots of commands, changing the subject for the next iteration, and leading to the unpredictable results. Here is a typical example that incorrectly tries to check if the value sent by the server is shown on the page:
1 | // INCORRECT, the test times out |
Instead, separate the assertions from commands. In the test above, let's confirm the fruit
returned by the server is a string, and then check the page.
1 | // ✅ The right way to check the page |
For more, watch the video Do Not Use cy Commands Inside A Should Callback Function.
More information
- See assertion examples
- Read Cypress assertions page
- Read
.should
documentation
Update 1
I wrote my own little library of functional utilities that works very well with Cypress should
callbacks, read Functional Helpers For Cypress Tests.