Bug
Repo https://github.com/bahmutov/cypress-promise-all-test
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
But the arguments commands
and utilities
are both "Utilities" elements
Also, the assertion is an unhandled promise itself - it fails but the test is green
only console shows the unresolved promise message
The tests are in https://github.com/bahmutov/cypress-promise-all-test
Ok, we must flatten the values we are getting using Cypress, so need a little utility function. This works
const all = (...fns) => {
const results = []
fns.reduce((prev, fn) => {
fn().then(result => results.push(result))
return results
}, results)
return cy.wrap(results)
}
it.only('wraps multiple cypress commands', () => {
return all(
getNavCommands,
getNavUtilities
).spread((commands, utilities) => {
console.log('got commands', commands.text())
console.log('got utilities', utilities.text())
})
})
Need to describe this in a recipe for users
We could wrap this into Cypress.all
or something like cy.all
.
It could take a chainer or a function as arguments and then massage them all together.
I just ran into this issue - why is this closed? Why doesn't Promise.all()
work here?
(I actually didn't run into exactly this issue; I tried to use Promise.all() to run two cy.request(...)
s in parallel, and instead of getting back two responses, I got back [undefined, undefined]
.)
Oh, I've just answered my own question: https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Not-Promises
Anything wrong with doing something like this:
const promisify = cmd => new Promise(resolve => cmd.then(resolve))
Promise.all([cy.get(), cy.get()].map(cmd => promisify(cmd))).then(...)
@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 = []) => ...)
Ok, we must flatten the values we are getting using Cypress, so need a little utility function. This works
const all = (...fns) => { const results = [] fns.reduce((prev, fn) => { fn().then(result => results.push(result)) return results }, results) return cy.wrap(results) } it.only('wraps multiple cypress commands', () => { return all( getNavCommands, getNavUtilities ).spread((commands, utilities) => { console.log('got commands', commands.text()) console.log('got utilities', utilities.text()) }) })
Need to describe this in a recipe for users
Can we have this available in standard API as cy.all
?
Summing up several solutions:
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:
:x:
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
});
});
@bahmutov's solution is correct, but disadvantage is you need to pass callbacks instead of commands directly. :heavy_check_mark:
Another solution is this, where you can pass commands directly:
:heavy_check_mark:
(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 Fixed. You can now nest as you like:cy.all()
as an argument to cy.all()
(but that's an edge case which you won't do anyway).
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 ]
});
});
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:
:heavy_check_mark:
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
});
});
:warning: 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:
:x:
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
});
});
});
});
Just a heads-up if anyone's using my cy.all
helper. It turned out it was completely wrong :).
Here's an updated version:
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;
}
@dwelle nice solution, but can we set it as Cypress commands e.g. Cypress.Commands.add('mergeResult', ...
. As I tried to do it, I got infinite loop or something like that. Tests hang out and nothing happens.
@nataliaroshchyna yea, I'm not surprised. Making it into a command adds it to the command queue when called, which apparently messes it up. Why do you need it as a command? Declaring it on cy
object should act the same. Or, if you think it's "dirty", you could make it into an util function that you'd import where you need.
This is exactly what I'm looking for. I'm trying to use's the cy.all helper from @dwelle 's post, but am getting the following errors when inserting this helper above my describe.
Any help would be greatly appreciated :)
Here's a hacked TS version. I don't really have experience in TS, so if anyone wants to properly type it, have at it. I'll update it as I go along.
declare namespace Cypress {
interface cy {
all (...commands: Cypress.Chainable[]): Cypress.Chainable,
queue: any
}
interface Chainable {
chainerId: string,
"___CY_ALL_CHAIN_START___": any
}
}
// changed from Symbol to string because TS doesn't support symbol index types ATM.
const chainStart = "___CY_ALL_CHAIN_START___";
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;
}
So I put this code above my describe in my spec file. It still doesn't like it. Sounds like this code should go somewhere else so it can be incorporated in my project properly?
Seems like @dwelle's solution takes the commands out of the cypress command queue , I was just wondering if there is any solution, where we could pass cy.request's into cy.all and they would happen parallel and not in sequence.
@MCFreddie777 in that case I'd not use cy.all
and cy.request
at all, but do something like:
cy.wrap(Promise.all([
window.fetch(/*...*/),
window.fetch(/*...*/),
window.fetch(/*...*/),
]));
or if you need the calls to be made at a given point of cypress test execution, you can do:
cy.wrap(null).then(() => {
return Promise.all([
window.fetch(/*...*/),
window.fetch(/*...*/),
window.fetch(/*...*/),
]);
});
(untested)
This is a typescript version I created from the above suggestions.
export type CypressFn<T> = () => Cypress.Chainable<T>;
// tslint:disable: max-line-length
export function CypressAll<T1, T2, T3, T4, T5, T6>(fns: [CypressFn<T1>, CypressFn<T2>, CypressFn<T3>, CypressFn<T4>, CypressFn<T5>, CypressFn<T6>]): Cypress.Chainable<[T1, T2, T3, T4, T5, T6]>;
export function CypressAll<T1, T2, T3, T4, T5>(fns: [CypressFn<T1>, CypressFn<T2>, CypressFn<T3>, CypressFn<T4>, CypressFn<T5>]): Cypress.Chainable<[T1, T2, T3, T4, T5]>;
export function CypressAll<T1, T2, T3, T4>(fns: [CypressFn<T1>, CypressFn<T2>, CypressFn<T3>, CypressFn<T4>]): Cypress.Chainable<[T1, T2, T3, T4]>;
export function CypressAll<T1, T2, T3>(fns: [CypressFn<T1>, CypressFn<T2>, CypressFn<T3>]): Cypress.Chainable<[T1, T2, T3]>;
export function CypressAll<T1, T2>(fns: [CypressFn<T1>, CypressFn<T2>]): Cypress.Chainable<[T1, T2]>;
export function CypressAll<T1>(fns: [CypressFn<T1>]): Cypress.Chainable<[T1]>;
export function CypressAll<T>(fns: CypressFn<T>[]): Cypress.Chainable<T[]> {
const results: T[] = []
fns.forEach(fn => {
fn().then(result => results.push(result))
});
return cy.wrap(results)
}
// tslint:enable: max-line-length
The use it like this:
const getBody = () => cy.wait(payloadAlias, { log: false }).then(xhr => xhr.requestBody);
const getTable = () => cy.wrap(table.rawTable);
CypressAll([getBody, getTable])
.then(([payload, matches]) => {
for (const [path, expected] of matches) {
const value = getValue(payload as object, path);
cy.wrap(value).should('eq', expected);
}
});
Thanks @dwelle for providing a solution. But I get null for the last command. Somehow in my case the stopCommand doesn't have prev attributes. I have to modify two lines to get correct results. Haven't do further validation, please correct me if my fix would cause other issues
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) <-- here, modify slice() range
.slice(0, commands.length)
.flatMap(cmd => {
// return cmd.prev.get('subject'); <-- here, get current subject instead of from prev
return cmd['subject'];
})
.value();
});
Will there be an official solution to this?
@bahmutov shall I create another issue or can you reopen this one and set it to another type instead of bug. I think the community would still benefit for progress.
Most helpful comment
Summing up several solutions:
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::x:
@bahmutov's solution is correct, but disadvantage is you need to pass callbacks instead of commands directly. :heavy_check_mark:
Another solution is this, where you can pass commands directly:
:heavy_check_mark:
(edit 19-03-23 → rewritten to fix issues)
but note, you can't pass anotherFixed. You can now nest as you like:cy.all()
as an argument tocy.all()
(but that's an edge case which you won't do anyway).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:
:heavy_check_mark:
:warning: 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:
:x: