Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cypress.io How to handle async code

Tags:

I'm in the middle of process of moving our old capybara tests to cypress.io as our application is going SPA way.

In our case we have over 2000 tests covering a lot of features. So common pattern to test feature is to have an user with created and published offer.

On the beginning I wrote case where cypress were going trough page and clicking everything. It worked but I saw that offer create + publish took almost 1,5 minute to finish. And sometimes we need multiple offers. So we have a test which takes 5 minutes and we have 1999 left to rewrite.

We came up with REST API to create offer and user, basically shortcut for test env preparation.

I came to the point where everything is working using async/await. So here's the thing. If I want to use normal async JS code with cypress I get Error: Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.

Here's how it looks like:

    const faker = require('faker')     import User from '../../support/User';      describe('Toggle button for description offer', () => {       const user = new User({         first_name: faker.name.firstName(),         last_name: faker.name.firstName(),         email: `QA_${faker.internet.email()}`,         password: 'xxx'       })       let offer = null        before(async () => {         await user.createOnServer()         offer = await user.createOffer()         await offer.publish()       })        beforeEach(() => {         user.login()         cy.visit(`/offers/${offer.details.id}`)         cy.get('.offer-description__content button').as('showMoreButton')       })        it('XXX', function () {         ...some test       })     }) 

This snippet works as expected. Firstly it fires before and creates whole env then when it's done it goes further to beforeEach and starts testing.

Now I would like to merge before and beforeEach like

  before(async () => {     await user.createOnServer()     offer = await user.createOffer()     await offer.publish()     user.login()     cy.visit(`/offers/${offer.details.id}`)     cy.get('.offer-description__content button').as('showMoreButton')   }) 

Which will fail because of async keyword. Now the question is: how to rewrite it to use async/await and cypress commands together? I tried to rewrite it with normal Promise but It won't work too ...

Any help appreciated.

like image 667
Daniel Słaby Avatar asked Apr 23 '18 11:04

Daniel Słaby


People also ask

How do you handle asynchronous in Cypress?

Cypress has come up with a wrapper which makes sure that test execution of its commands happen in sequence in which they are developed. Thus these commands are executed all at once but they are queued. However if a Javascript command works along with a Cypress command, it remains asynchronous.

Can we use async await in Cypress?

Nesting a single level is pretty straight-forward, but multiple levels of nesting can become much harder to understand. Async/await makes it much easier to unwrap values, but Commands are not Promises. Using await on a Cypress chain will not work as expected. Using async/await removed a nesting level.

Is Cypress synchronous or asynchronous?

Remember: Cypress commands are asynchronous and get queued for execution at a later time. During execution, subjects are yielded from one command to the next, and a lot of helpful Cypress code runs between each command to ensure everything is in order.


2 Answers

Your problem stems from the fact that cypress commands are not promises, although behaving like promises.

I can think of two options:

  • Try to refactor your test code to not use async/await, as these commands don't behave as expected when running your code on cypress (check this bug). Cypress already has a whole way of dealing with async code as it creates a command queue that always run sequentially and in the expected order. That means you could observe the effects of your async code to validate that it happened before moving forward on your test. For instance, if User.createUserOnServer must wait a successful API call, add code to your test that will wait for the request to complete, using cy.server(), cy.route() and cy.wait(), like below:

    cy.server(); cy.route('POST', '/users/').as('createUser'); // do something to trigger your request here, like user.createOnServer() cy.wait('@createUser', { timeout: 10000}); 
  • Use another third-party library that changes how cypress works with async/await, like cypress-promise. This lib may help you to treat cypress commands as promises that you can await in your before code (read more about it in this article).

like image 57
Guilherme Lemmi Avatar answered Sep 24 '22 13:09

Guilherme Lemmi


While @isotopeee's solution basically works, I did run into issues, especially when using wait(@alias) and an await command right after that. The problem seems to be, that Cypress functions return an internal Chainable type that looks like a Promise but isn't one.

You can however use this to your advantage and instead of writing

describe('Test Case', () => {   (async () => {      cy.visit('/')      await something();   })() }) 

you can write

describe('Test Case', () => {   cy.visit('/').then(async () => await something()) }) 

This should work with every Cypress command

like image 26
Dominik Ehrenberg Avatar answered Sep 23 '22 13:09

Dominik Ehrenberg