question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

When using `Promise.all` to get multiple elements in Cypress, same last element is returned

See original GitHub issue
  • Operating System: Mac
  • Cypress Version: 1.0.3
  • Browser Version: built-in

Is this a Feature or Bug?

Bug

Test code

Repo https://github.com/bahmutov/cypress-promise-all-test

Problem

We can get multiple promises resolved in parallel, this code works as expected

it('works with values', () => {
    Cypress.Promise.all([
      Promise.resolve('a'),
      Promise.resolve('b')
    ]).then(([a, b]) => {
      expect(a).to.equal('a')
      expect(b).to.equal('b')
    })
  })

We can even spread results using built-in Bluebird promise spread

  it('spreads resolved values', () => {
    Cypress.Promise.all([
      Promise.resolve('a'),
      Promise.resolve('b')
    ]).spread((a, b) => {
      expect(a).to.equal('a')
      expect(b).to.equal('b')
    })
  })

But if any of the promises are Cypress chains of commands, then all values passed into array are the same - the last value. For example this test grabs navigation links from https://example.cypress.io/

First link should be “Commands” and the second link should be “Utilities”

  it('grabs element values', () => {
    cy.visit('https://example.cypress.io/')

    const getNavCommands = () =>
      cy.get('ul.nav')
        .contains('Commands')

    const getNavUtilities = () =>
      cy.get('ul.nav')
        .contains('Utilities')

    // each works by itself
    getNavCommands()
      .should('be.visible')

    getNavUtilities()
      .should('be.visible')

    // lets get both elements
    Cypress.Promise.all([
      getNavCommands(),
      getNavUtilities()
    ]).then(([commands, utilities]) => {
      console.log('got commands', commands.text())
      console.log('got utilities', utilities.text())
      // debugger
      expect(utilities.text()).to.equal('Utilities')
      expect(commands.text()).to.equal('Commands')
    })
  })

I can see that it really grabbed each link correctly during in the reporter

screen shot 2017-11-14 at 3 00 29 pm screen shot 2017-11-14 at 3 00 37 pm

But the arguments commands and utilities are both “Utilities” elements

screen shot 2017-11-14 at 3 01 56 pm

Also, the assertion is an unhandled promise itself - it fails but the test is green

screen shot 2017-11-14 at 3 00 17 pm

only console shows the unresolved promise message

screen shot 2017-11-14 at 3 01 05 pm

The tests are in https://github.com/bahmutov/cypress-promise-all-test

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:1
  • Comments:35 (8 by maintainers)

github_iconTop GitHub Comments

26reactions
dwellecommented, Mar 23, 2019

Summing up several solutions:

  1. Note that @brian-mann’s solution isn’t correct due to how late-chaining of .then callbacks currently works in cypress, as can be tested below:

    describe("test", () => {
        it("test", async () => {
    
            const accum = (...cmds) => {
                const results = [];
    
                cmds.forEach((cmd) => {
                    cmd.then(val => { results.push(val); });
                });
    
                return cy.wrap(results);
            };
    
            cy.document().then( doc => {
                doc.write(`
                    <div class="test1">one</div>
                    <div class="test2">two</div>
                `);
            });
    
            accum(
                cy.get(".test1").invoke("text"),
                cy.get(".test2").invoke("text")
            ).spread((a, b) => console.log(a, b)); // two two
        });
    });
    
  2. @bahmutov’s solution is correct, but disadvantage is you need to pass callbacks instead of commands directly. ✔️

  3. Another solution is this, where you can pass commands directly:

    ✔️

    (edit 19-03-23 → rewritten to fix issues)

    const chainStart = Symbol();
    cy.all = function ( ...commands ) {
        const _           = Cypress._;
        const chain       = cy.wrap(null, { log: false });
        const stopCommand = _.find( cy.queue.commands, {
            attributes: { chainerId: chain.chainerId }
        });
        const startCommand = _.find( cy.queue.commands, {
            attributes: { chainerId: commands[0].chainerId }
        });
        const p = chain.then(() => {
            return _( commands )
                .map( cmd => {
                    return cmd[chainStart]
                        ? cmd[chainStart].attributes
                        : _.find( cy.queue.commands, {
                            attributes: { chainerId: cmd.chainerId }
                        }).attributes;
                })
                .concat(stopCommand.attributes)
                .slice(1)
                .flatMap( cmd => {
                    return cmd.prev.get('subject');
                })
                .value();
        });
        p[chainStart] = startCommand;
        return p;
    }    
    
    describe("test", () => {
        it("test", async () => {
            cy.document().then( doc => {
                doc.write(`
                    <div class="test1">one</div>
                    <div class="test2">two</div>
                `);
            });
    
            cy.all(
                cy.get(".test1").invoke("text"),
                cy.get(".test2").invoke("text")
            ).spread((a, b) => console.log(a, b)); // one, two
        });
    });
    

    but note, you can’t pass another cy.all() as an argument to cy.all() (but that’s an edge case which you won’t do anyway). Fixed. You can now nest as you like:

    describe("test", () => {
        it("test", () => {
            cy.window().then( win => {
                win.document.write(`
                    <div class="test1">one</div>
                    <div class="test2">two</div>
                    <div class="test3">three</div>
                    <div class="test4">four</div>
                    <div class="test5">five</div>
                    <div class="test6">six</div>
                `);
            });
    
            cy.all(
                cy.get(".test1").invoke("text"),
                cy.get(".test2").invoke("text"),
                cy.all(
                    cy.get(".test3").invoke("text"),
                    cy.all(
                        cy.get(".test4").invoke("text")
                    )
                )
            ).then(vals => {
    
                return cy.all(
                    cy.get(".test5").invoke("text"),
                    cy.get(".test6").invoke("text")
                ).then( vals2 => [ ...vals, ...vals2 ]);
            }).then( vals => console.log( vals )); // [ one, two, three, four, five, six ]
        });
    });
    
  4. Yet another solution, definitely not recommended, is to wrap the commands into bluebird promise which (unlike native promise) somehow correctly resolves with the value yielded by cy command:

    ✔️

    describe("test", () => {
        it("test", async () => {
            cy.document().then( doc => {
                doc.write(`
                    <div class="test1">one</div>
                    <div class="test2">two</div>
                `);
            });
    
            const Bluebird = Cypress.Promise;
            
            cy.wrap(Promise.all([
                Bluebird.resolve(cy.get(".test1").invoke("text")),
                Bluebird.resolve(cy.get(".test2").invoke("text"))
            ])).spread((a, b) => console.log(a, b)); // one two
        });
    });
    

    ⚠️ Also, cypress will complain about you mixing promises and cy commands.

    But beware: unlike the previous solutions, you can’t nest those, for some reason:

    describe("test", () => {
        it("test", async () => {
            cy.document().then( doc => {
                doc.write(`
                    <div class="test1">one</div>
                    <div class="test2">two</div>
                `);
            });
    
            const Bluebird = Cypress.Promise;
            
            cy.wrap(Promise.all([
                Bluebird.resolve(cy.get(".test1").invoke("text")),
                Bluebird.resolve(cy.get(".test2").invoke("text"))
            ])).spread((a, b) => {
    
                console.log(a, b); // one two
    
                cy.wrap(Promise.all([
                    Cypress.Promise.resolve(cy.get(".test1").invoke("text")),
                    Cypress.Promise.resolve(cy.get(".test2").invoke("text"))
                ])).spread((a, b) => {
    
                    console.log(a, b); // undefined undefined
                });
            });
        });
    });
    
16reactions
brian-manncommented, Apr 5, 2018

@qoc-dkg you cannot race commands at the same time, and you’re not actually achieving anything here to be honest. If you want to aggregate an accumulation of command results you’d do something like this. Otherwise there’s nothing special - commands will run in serial and you don’t have to do anything to ensure they are awaited correctly.

const accum = (...cmds) => {
  const results = []

  cmds.forEach((cmd) => {
    cmd().then(results.push.bind(results))
  })

  return cy.wrap(results)
}

accum(cy.cmd1(), cy.cmd2(), cy.cmd3())
.then((results = []) => ...)
Read more comments on GitHub >

github_iconTop Results From Across the Web

Gather results from multiple cypress promisses - Stack Overflow
Cypress chainer objects aren't promises (they're not compatible with Promises/A+ at all) -- the similarity pretty much ends with them having .
Read more >
Cypress.Promise
Cypress is promise aware so if you return a promise from inside of commands like .then() , Cypress will not continue until those...
Read more >
Promise.all() - JavaScript - MDN Web Docs
The Promise.all() method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the ......
Read more >
Cypress And Promises - YouTube
In this video, I explain how to create promises correctly during Cypress tests (because promises are eager), and how to make Cypress test ......
Read more >
Making Promises In Cypress, Literally. - Leaf Grow
You can use the .then method to chain multiple calls in a single expression. The element returned by cy.get is of Chainable type...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found