Cypress: Nesting of Chainables and Promises doesn't work as expected

Created on 11 Jul 2018  路  3Comments  路  Source: cypress-io/cypress

Current behavior:

In the promise doc it's described how to make Cypress wait for asynchronous processes from within a Test.

I'm trying to do the following: To set up my test, I have to create some entities in the DB using REST calls. When creating nested entities I have to make several REST calls that have to wait for each other and resolve the before() block of my test when the last call is complete. This chain of async calls results in a nesting of Promises and Chainables. The observed behavior is that only the first level of such scopes get called, the second Chainable is probably waiting for the first one to finish, I don't know.


image

Desired behavior:

I wish to have a pattern that enables me to have nesting of async REST calls using Chainables and Promises.

Steps to reproduce:

    describe(`Demo: Nesting of Chainables and promises doesn't work as expected. #ID=2018-07-11`, () => {
    let waited: boolean = false;

    const waitNested: (i: number) => Cypress.Chainable<any> = (i: number): any => {
        if (i === 0) {
            waited = true;

            return Cypress.Promise.resolve();
        }

        return cy.wrap(i).then(() => {
            return new Cypress.Promise((resolve: Function): void => {
                setTimeout(() => {
                    waitNested(i - 1).then(() => {
                        resolve();
                    });
                }, 1000);
            });
        });
    };

    it('Wait 0', () => { // Success
        waited = false;

        waitNested(0)
            .then(() => {
                expect(waited).to.be.true;
            });
    });

    it('Wait 1', () => { // Success
        waited = false;

        waitNested(1)
            .then(() => {
                expect(waited).to.be.true;
            });
    });

    it('Wait wraps', () => { // Success
        let x: boolean = false;
        cy.wrap(0).then(() => {
            cy.wrap(0).then(() => {
                cy.wrap(0).then(() => {
                    x = true;
                });
            });
        })
            .then(() => {
                expect(x).to.be.true;
            });
    });

    it('Wait 2', () => { // Fail
        waited = false;

        waitNested(2)
            .then(() => {
                expect(waited).to.be.true;
            });
    });
    });

Versions


Cypress: 3.0.2
OS: Win 10
Browser: Chrome 67

Most helpful comment

This is failing because you've effectively created a deadlock in Cypress where you've intermixed promises and cypress commands, and where the cy.then() is awaiting a promise which is using a cy.wrap() which then enqueues another promise which is never resolved.

What's happening is that the inner / nested cy.wrap() command will not resolve because it is not run because the outer (parent) cy.then is left awaiting. It's never going to resolve the way you're writing it.

This is why we describe commands as having promise like capabilities, but you're trying to splice two completely different idioms together and it won't work cohesively.

In Cypress you cannot ever race commands against each other, they will be enqueued synchronously, but only ever run asynchronously.

With that said - in every situation we've seen this, there's generally an antipattern found somewhere. In other words, you don't ever need to find yourself in this situation if you're utilizing Cypress in the way it's meant to be utilized.

You mention your use case as needing to wait for several REST calls to seed a database. This is easily achievable out of the box using Cypress itself and you don't have to worry about or manage any of the concurrency with anything.

For instance...

cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })

Those 5 requests will happen sequentially, not in parallel. You don't even need to do anything special to await them. They will happen one at a time.

Another example...

cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)

Same principle.

There's no need to ever coordinate multiple commands in Cypress since only one can ever be processing at a time. This avoids the need to synchronize promises which is why these two interfaces are fundamentally incompatible. They just share some qualities that are similar in a few ways.

If you need to get the results of a previous commands yielded subject that's when you use .then() but not that is the only time it is necessary.

cy.request('...') // first
.then((resp) => {
  // use something here
  cy.request(..., resp.body.foo) // second
})

cy.request(...) // third

If you need to bypass this mechanism altogether and avoid using Cypress commands we recommend writing a single cy.task to do everything in node which then avoids the need to utilize Cypress idioms entirely. There you have ultimate control of whatever it is you'd like to do. Want to race commands together? Go ahead.

cy.task('db:seed', ...)

// inside cypress/plugins/index.js
module.exports = (on, config) => {
  on('task', {
    'db:seed': (arg1, arg2, arg3) => {
       // run a bunch of commands here and coordinate them
       // ourselves using Promises to do whatever we want
       return Promise.all([
          doTheThing(),
          doAnotherThing(),
          fooBarBaz(),
       ])
       .then(somethingElse)
       .catch((err) => { whatever(err) })
     }
  }
}

All 3 comments

Note: Using Cypress.Promise instead of Promise doesn't make a difference.

This is failing because you've effectively created a deadlock in Cypress where you've intermixed promises and cypress commands, and where the cy.then() is awaiting a promise which is using a cy.wrap() which then enqueues another promise which is never resolved.

What's happening is that the inner / nested cy.wrap() command will not resolve because it is not run because the outer (parent) cy.then is left awaiting. It's never going to resolve the way you're writing it.

This is why we describe commands as having promise like capabilities, but you're trying to splice two completely different idioms together and it won't work cohesively.

In Cypress you cannot ever race commands against each other, they will be enqueued synchronously, but only ever run asynchronously.

With that said - in every situation we've seen this, there's generally an antipattern found somewhere. In other words, you don't ever need to find yourself in this situation if you're utilizing Cypress in the way it's meant to be utilized.

You mention your use case as needing to wait for several REST calls to seed a database. This is easily achievable out of the box using Cypress itself and you don't have to worry about or manage any of the concurrency with anything.

For instance...

cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })
cy.request('https://app.corp.com/seed', { ... })

Those 5 requests will happen sequentially, not in parallel. You don't even need to do anything special to await them. They will happen one at a time.

Another example...

cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)
cy.task('db:seed', arg1, arg2)

Same principle.

There's no need to ever coordinate multiple commands in Cypress since only one can ever be processing at a time. This avoids the need to synchronize promises which is why these two interfaces are fundamentally incompatible. They just share some qualities that are similar in a few ways.

If you need to get the results of a previous commands yielded subject that's when you use .then() but not that is the only time it is necessary.

cy.request('...') // first
.then((resp) => {
  // use something here
  cy.request(..., resp.body.foo) // second
})

cy.request(...) // third

If you need to bypass this mechanism altogether and avoid using Cypress commands we recommend writing a single cy.task to do everything in node which then avoids the need to utilize Cypress idioms entirely. There you have ultimate control of whatever it is you'd like to do. Want to race commands together? Go ahead.

cy.task('db:seed', ...)

// inside cypress/plugins/index.js
module.exports = (on, config) => {
  on('task', {
    'db:seed': (arg1, arg2, arg3) => {
       // run a bunch of commands here and coordinate them
       // ourselves using Promises to do whatever we want
       return Promise.all([
          doTheThing(),
          doAnotherThing(),
          fooBarBaz(),
       ])
       .then(somethingElse)
       .catch((err) => { whatever(err) })
     }
  }
}

This is exactly what I was looking for, thanks a lot!

Was this page helpful?
0 / 5 - 0 ratings