Cypress.io How to handle async code
Asked Answered
R

12

52

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.

Read answered 23/4, 2018 at 11:43 Comment(0)
B
30

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.intercept(), (cy.server(), cy.route() and cy.wait() are deprecated), like below:

     cy.server();
     cy.intercept({method: 'POST', url: '/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).

Businessman answered 23/4, 2018 at 12:18 Comment(4)
Perfect! Thanks a lot, will test it right tomorrow!Meghanmeghann
You're welcome! This topic here github.com/cypress-io/cypress/issues/595 might also help you on how to use cy.server and cy.route.Businessman
@GuilhermeLemmi I have the same problem and found out that cy.route() only works for requests fired from the application. Are there any cypress instruments to wait for requests fired in the test, too?Parmer
A related but more complex question. Hoping someone could help. #78270125Pestilence
F
23

I am using the following code snippet to make sure a async function is executed in cypress before the next cypress command will be executed:

cy.wrap(null).then(() => myAsyncFunction());

Example:

function sleep(milliseconds) {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

async function asyncFunction1() {
    console.log('started asyncFunction1');
    await sleep(3000);
    console.log('finalized asyncFunction1');
}

async function asyncFunction2() {
    console.log('started asyncFunction2');
    await sleep(3000);
    console.log('finalized asyncFunction2');
}

describe('Async functions', () => {
    it('should be executed in sequence', () => {
        cy.wrap(null).then(() => asyncFunction1());
        cy.wrap(null).then(() => asyncFunction2());
    });
});

leads to following output:

started asyncFunction1
finalized asyncFunction1
started asyncFunction2
finalized asyncFunction2
Flamboyant answered 29/6, 2021 at 12:43 Comment(4)
Did you mean .then(async () => await myAsyncFunction())?Bemba
@Sarah No I've just written a test and added the results to this post it really seems to execute the code in sequence if you use cy.wrap(null).then(() => asyncFunction1());Flamboyant
Seems like the correct answer, however would be great if you could support your claim (that this is guaranteed to wait before executing the next cy-statement) with a link to the documentation. Also, as far as I can see, this only works for async statements where you don't care about the result (which I think is plenty, but good to specify)Promiscuous
@Claude: Here is a link to docs: docs.cypress.io/api/utilities/promise#Waiting-for-PromisesErigeron
B
16

While @jdoroy'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

Brindisi answered 17/7, 2020 at 16:1 Comment(3)
Thanks! That's a shame, everything else about the library is so nice to use.Defecate
This will cause problems with your reports. In case of a failure Mocha will not show the error. Tests will pass all the time. There are several other articles on the web that do not advice to use this approach.Titmouse
I have a more complex question about chaining with then(). Hoping someone could help. #78270125Pestilence
R
12

Put the async code in cy.then():

  before(() => {
    cy.then(async () => {
      await user.createOnServer()
      offer = await user.createOffer()
      await offer.publish()
      user.login()
      cy.visit(`/offers/${offer.details.id}`)
    })

    // This line can be outside `cy.then` because it does not
    // use any variables created or set inside `cy.then`.
    cy.get('.offer-description__content button').as('showMoreButton')
  })
Rosalia answered 25/3, 2022 at 5:0 Comment(1)
I have a more complex question about chaining with then(). Hoping someone could help. #78270125Pestilence
T
2

I'm having a similar issue regarding async/await inside it / test blocks. I solved my problem by wrapping the body inside an async IIFE:

describe('Test Case', () => {
  (async () => {
     // expressions here
  })()
})
Tootle answered 12/11, 2019 at 5:36 Comment(2)
async might be problematic! Cypress might stop at JS promises, and the tests look like they succeed, but really are ignored. I suggest to check if the tests are really processed, or just skipped. It also might behave differently e.g. in 'interactive' mode and 'headlessly'.Shortridge
I've done things like that for a while (a bit different since I was passing async blocks to before/after hooks) and it worked a lot of the time, but I regularly had incomprehensible bugs that took a crazy amount of time to debug. You're likely to see odd and inconsistent behaviours, as well as bad error handling (e.g. losing error messages etc). If you're interested, I posted my final solution as an answerDefecate
F
1

I had the exact same issue as the OP and I figured I'd share a simplified version of Timation's answer that I am using. I tested this in Cypress version 8.0.0.

In my case, I was calling an async function in the before() hook, but cypress keep throwing the same warning OP was getting.

Cypress was complaining about this code:

// Bad Code
const setupTests = async () => {
  await myAsyncLibraryCall();
}

before(async () => {
  await setupTests();
  cy.login();
});

To fix it, I just cy.wrap()'d the async function and cypress now runs the async function synchronously with other Cypress commands and doesn't complain.

// Good Code
before(() => {
  cy.wrap(setupTests());
  cy.login();
});
Frankenstein answered 8/10, 2021 at 18:41 Comment(1)
Hi, thanks for the solution. Wanted to add that the docs say that if you need synchronicity guarantees, you should use cy.wrap(p).then(() => {}) as by default they are all async and the ordering is not guaranteed: docs.cypress.io/api/commands/wrap#PromisesBenjie
D
0

I'll share my approach as I had an enormous amount of headaches writing my tests that involved a large amount of AWS SDK calls (all promises). The solution I came up with provides good logging, error-handling, and seems to solve all the issues I had.

Here's a summary of what it offers:

  • a method that wraps a lazy promise and calls the promise inside a Cypress chainable
  • the alias provided to the method will appear in the Cypress command panel in the UI. It will also be logged to the console when the execution starts, completes, or fails. Errors will neatly be displayed in the Cypress command panel instead of being lost (can happen if you run async functions in a before/after hook) or only appearing in the console.
  • using cypress-terminal-report, the logs should hopefully be copied from the browser to stdout, which means you'll have all the information you need to debug your tests in a CI/CD setting where the browser logs are lost after the run
  • as an unrelated bonus, I shared my cylog method which does two things:
    • logs in the message Cypress command panel
    • logs the message to stdout using a Cypress task, which executes with Node and not in the browser. I could log in the browser and rely on cypress-terminal-report to log it, but it doesn't always log when errors occur in a before hook, so I prefer using Node when it's possible.

Hope this didn't overwhelm you with information and was useful!

/**
 * Work around for making some asynchronous operations look synchronous, or using their output in a proper Cypress
 * {@link Chainable}. Use sparingly, only suitable for things that have to be asynchronous, like AWS SDK call.
 */
export function cyasync<T>(alias: string, promise: () => Promise<T>, timeout?: Duration): Chainable<T> {
    const options = timeout ? { timeout: timeout.toMillis() } : {}
    return cy
        .wrap(null)
        .as(alias)
        .then(options, async () => {
            try {
                asyncLog(`Running async task "${alias}"`)
                
                const start = Instant.now()
                const result = await promise()
                const duration = Duration.between(start, Instant.now())
                
                asyncLog(`Successfully executed task "${alias}" in ${duration}`)
                return result
            } catch (e) {
                const message = `Failed "${alias}" due to ${Logger.formatError(e)}`
                asyncLog(message, Level.ERROR)
                throw new Error(message)
            }
        })
}

/**
 * Logs both to the console (in Node mode, so appears in the CLI/Hydra logs) and as a Cypress message
 * (appears in Cypress UI) for easy debugging. WARNING: do not call this method from an async piece of code.
 * Use {@link asyncLog} instead.
 */
export function cylog(message: string, level: Level = Level.INFO) {
    const formatted = formatMessage(message, level)
    cy.log(formatted)
    cy.task('log', { level, message: formatted }, { log: false })
}

/**
 * When calling from an async method (which you should reconsider anyway, and avoid most of the time),
 * use this method to perform a simple console log, since Cypress operations behave badly in promises.
 */
export function asyncLog(message: string, level: Level = Level.INFO) {
    getLogger(level)(formatMessage(message, level))
}

For the logging, some additional changes are needed in plugins/index.js:

modules.export = (on, config) => {
    setUpLogging(on)
    // rest of your setup...
}

function setUpLogging(on) {
    // this task executes Node code as opposed to running in the browser. This thus allows writing out to the console/Hydra
    // logs as opposed to inside of the browser.
    on('task', {
        log(event) {
            getLogger(event.level)(event.message);
            return null;
        },
    });

    // best-effort attempt at logging Cypress commands and browser logs
    // https://www.npmjs.com/package/cypress-terminal-report
    require('cypress-terminal-report/src/installLogsPrinter')(on, {
        printLogsToConsole: 'always'
    })
}

function getLogger(level) {
    switch (level) {
        case 'info':
            return console.log
        case 'error':
            return console.error
        case 'warn':
            return console.warn
        default:
            throw Error('Unrecognized log level: ' + level)
    }
}

And support/index.ts:

import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector'

installLogsCollector({})
Defecate answered 26/2, 2021 at 21:39 Comment(0)
T
0

Here is another workaround that can be terser:

// an modified version of `it` that doesn't produce promise warning
function itAsync(name, callback) {
  it(name, wrap(callback))
}

function wrap(asyncCallback) {
  const result = () => {
    // don't return the promise produced by async callback
    asyncCallback()
  }
  return result
}

itAsync('foo', async () => {
  await foo()
  assert.equal('hello', 'hello')
})
Toenail answered 16/9, 2021 at 8:55 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Minard
H
0

Piggy-backing on @Thai's answer, I used cy.then(async () => await func)

The short version:

cy.then(async () => await makeFirebaseUser())
  .then(async (firebaseUser) => await makeRailsUser(firebaseUser))

In context:

Cypress.Commands.add(
  'createFirebaseGoogleOauthUser',
  ({ displayName, email }: CreateFirebaseGoogleOauthUserArgs) => {
    if (!displayName) displayName = faker.person.fullName();
    if (!email) email = displayName.split(' ').join('.').toLowerCase() + '@firebaseuser.com';

    const sub = Math.floor(Math.random() * 1000); // random number to make unique

    const makeFirebaseUser = async () => {
      await firebase.auth().signOut()
      const credential = await firebase.auth().signInWithCredential(
        firebase.auth.GoogleAuthProvider.credential(
          `{"sub": "${sub}", "email": "${email}", "email_verified": true}`,
        ),
      )
      await credential?.user?.updateProfile({ displayName })
      return credential?.user
    }

    const makeRailsUser = async (firebaseUser: CreatedFirebaseUser) => {
      cy.appFactories<UserModel[]>([['create', 'user', { email, display_name: displayName, full_name: displayName, firebase_id: firebaseUser?.uid }]]).then(
        async records => {
          const railsUser = records[0]
          return { railsUser, firebaseUser }
        }
      );
    };

    // note: return value of this custom command is the value of the last cypress command executed
    // in this case, makeRailsUser()
    cy.then(async () => await makeFirebaseUser())
      .then(async (firebaseUser) => await makeRailsUser(firebaseUser))

  }
);

Hope it helps someone! I burned a few hours on this.

Hackney answered 29/3, 2024 at 19:25 Comment(0)
M
-1

In my case, I discovered that cy.log() and Cypress.log() were causing this issue - try removing those.

Ref: https://github.com/cypress-io/cypress/issues/3472#issuecomment-552411367

Meingolda answered 1/12, 2022 at 9:45 Comment(0)
G
-1

If you want to console log after the registration request, you can try below.

const register = (...) = > {
 cy.request({
  method: 'POST'
  url: ...
  headers: ...
  body: ...
 }).then((resp) => {
  ...
 })
}

Using cy.then(...) is helpful to adjust asynchronous to synchronous.

it('...', () = > {
 register(...)
 cy.then(() => {
  console.log('this log will execute after register')
 })
})
Germiston answered 6/1, 2023 at 3:29 Comment(0)
D
-2

You can use Promise to with the await keyword. And look for more info where on w3schools: https://www.w3schools.com/js/js_promise.asp

  • This help me out greatly
// {bidderCreationRequest} was declared earlier

function createBidderObject() {
  const bidderJson = {};
  await new Promise((generateBidderObject) => {
    cy.request(bidderCreationRequest).then((bidderCreationResp) => {
      bidderJson.id = bidderDMCreationResp.body.id;

      generateBidderObject(bidderJson);
    });
  });

  return bidderJson.id
}

createBidderObject(); // returns the id of the recently created bidder instead of undefined/null

You can also use https://github.com/NicholasBoll/cypress-promise#readme, because the cy commands again are not Promises. So you kinda get luck if you using async/await with using the native Promise function or the plugin mentioned

Daphnedaphnis answered 31/3, 2021 at 21:58 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.