In an earlier blog post I showed how to convert the Selenium tests in the  Onion-DevOps-Architecture project to Cypress. In this post I’ll dive into one of the little gotchas I ran into from another project using Cypress.
Everything is Async and Queued
First a little background. Although you write a Cypress test in synchronous JavaScript, the flow of the test is completely asynchronous, as their guide describes. The jist is that when you call a cy.*
function, you are really calling queue_command(cy.*)
For example if you write the code below, it may not do what you expect when playing the computer.
it('fills in fields', function () {
cy.wait(5000)
cy.get('[data-cy="input1"]').clear().type('30000')
console.log('Dev1')
cy.log('CyLog1')
cy.wait(5000)
cy.get('[data-cy="input2"]').clear().type('testing')
console.log('Dev2')
cy.log('CyLog2')
cy.wait(5000)
cy.get('.btn-primary').click()
cy.get('#success-alert').should('exist')
})
When this runs in the Cypress UI with developer tools on, the browser’s console will show “Dev1” and “Dev2” right away since the cy.*
functions are just queuing up commands and the console.log
commands run while the others are queued.
After the initial cy.wait()
, the Cypress Command Log will show the “CyLog” entries serially with the output from the other commands.
If you need to run some JavaScript after a cy
function, you cannot await
, as you may be tempted to do, but you can use then
which brings me to the topic of this blog, polling for a page for a result.
Polling a Page
They do mention how to do this in their doc, but it still took me a while to get some working code. I hope this will save someone out there some time.
In my specific scenario, I had to test creating an loan, and processing it. For the site being tested, the loan id was generated asynchronously and wouldn’t always be immediately available on the loan search page.
Here are the basic steps of the test:
- Create the Loan
- Navigate to the Loan Search page
- Search the page for a new Loan
- If it was found, process it
- If it was not found loop back to 2
A simple for loop is what you want to do.
it('tries to find an Loan by polling' , () => {
cy.login(username, password)
var status = 'new'
for ( var i = 0; i < 5; i++) {
cy.visit('/LoanSearch/LoanSearch')
const items = Cypress.$(`[data-cy="td-state"]:contains("${status}")`)
if (items && items.length) {
console.log(`Dev Console Found it! on try ${i}`, items[0])
cy.log(`CyLog Found it! on try ${i}`, items[0])
break
} else {
console.log(`Dev Console Waiting on try ${i} ...`)
cy.log(`CyLog Waiting on try ${i} ...`)
cy.wait(1000)
}
}
})
But this doesn’t work (for good reason). In each pass of the loop, this is what Cypress does:
- Queue up a login
- Queue up a
cy.visit
- Check the current page for
Cypress.$(...)
, which will evaluate to false since it’s not on the search page at this point. (The login andcy.visit
above are just queued up. ) - Since it will never find anything and the loop always runs five times, five sets of the commands in the false block are queued up.
When running this, the browser’s console will show five of the Dev Console Waiting...
messages immediately (since not queued).
Then the Cypress Command Log will show the CyLog Waiting...
messages and the WAIT
messages. Since only the cy.log
and cy.wait
were queued up, that’s all that will run.
To do this correctly and run code after cy.visit
, use then
and recursively call a function to do the polling. Here’s the function.
export function findLoanAndProcess(status, depth = 0) {
// prevent infinite recursion
expect(depth).to.be.lessThan(10)
cy.visit('/LoanSearch/LoanSearch')
.then(() => {
console.log(Cypress.$(`[data-cy="td-state"]:contains("${status}")`))
const items = Cypress.$(`[data-cy="td-state"]:contains("${status}")`)
if (items && items.length) {
cy.log(`Found it and now call process on it`)
cy.get('[data-cy="td-state"]')
.contains(status)
.prev()
.then((element) => {
processIt(element.text())
})
} else {
cy.log('Did not find it, waiting to try again')
cy.wait(1000)
findLoanAndProcess(status, depth+1)
}
})
}
Depth is passed in to avoid infinite recursion looking for the new item. The expect
on line 4 will cause the function to error out if too deep.
This case uses cy.visit()
just like in the for
loop case above, but this time it uses then()
that will run code after the page is visited. Now, the if
statement is executed on the correct page and will actually be able to find the loan.
The browser’s console and the Cypress Command Log now are more in sync since the console log messages are also in the then
code.
Since the processIt
function on line 16 needs the loan id in the prev()
element you’d may want to try something like this, but remember cy
commands just queue things up and don’t return the result of running the command.
var id = cy.get(...).value
To get the id, we again have to use then
on line 15 after Cypress gets the element with cy.get(...).contains(...).prev()
and pass it to then's
lambda.
Here’s the test code that calls the recursive function. Statements after that function can continue with the test.
it('loan->process', function () {
fillAndSubmitLoan()
findLoanAndProcess(LoanStatus.preApproved)
// continue on as needed....
})
Final Thoughts
Cypress is a really powerful testing platform, that’s pretty easy to use. Most tests are just a bunch of cy.*
calls that queue up the commands and everything is easy. But even though when you’re writing code that looks like synchronous JavaScript calls, and smells like synchronous JavaScript calls, you must keep in mind that cy.*
is queuing a command to run after the JavaScript runs. Any code that needs to react to a cy.*
command must use then
. HTH
Links
Cypress.io They have done a great job with the doc.
Cypress Guide about Chains of Command — a must read
Cypress FAQ regarding polling
Previous blog post about Cypress