-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When using Promise.all
to get multiple elements in Cypress, same last element is returned
#915
Comments
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 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 |
(I actually didn't run into exactly this issue; I tried to use Promise.all() to run two |
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:
|
@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 = []) => ...) |
Can we have this available in standard API as |
Summing up several solutions:
|
Just a heads-up if anyone's using my 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. |
@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 |
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.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. |
@dwelle Today I stumbled upon a problem in your function. As the function really touches the cypress chain (stack), it struggles when the function inside it contains another cy commands. Example below - using Without |
@MCFreddie777 I don't recall whether this was meant to ever work or not, but it may also likely be because the implem depends on internals of a pretty old Cypress version. Would have to review & attempt to fix, but right now I don't have the time :(. |
@MCFreddie777 if it helps you, the @dwelle fix number 3 is working fine for me in Cypress version 4.12.1, but I'm just using it with |
Fighting with Promise.all() for almost five hours cause I tought that the issue( same value returned for all promises passed to Promise.all) was related with that method but it wasn't. Finally I figured out (thanks to Gleb) that it's better to handle multiple cy.request (non-related each other) with wrap() and spread() (built-in Cypress commands). Thanks a bunch for the solution @bahmutov |
I still don't understand why this is broken the way it is. I understand that when I call a command it isn't run immediately but at some point in the future, and that cypress commands are run in deterministic fashion, in-order. What I don't understand is why each Promise returned by each deterministic in-order command doesn't resolve to the result of the command that returned it, and instead resolves to something else. :/ |
Cypress commands are not promises but more similar to reactive streams thus promise.all does not work. May I ask to open new discussions with code samples of what you are trying to accomplish rather than commenting on this closed issue |
FYI, anyone still using the above mentioned solution, here's an updated version for Cypress >=8.3.x due to updates to internal
|
@samtsai @dwelle Thanks for posting the solutions. I have been using this solution but with recent Cypress upgrade >=8.3.0, I am not able to use this method anymore. I am using js and not typescript. I tried to convert the solution mentioned above to js but it did not work. |
@AmiChandra89 Can you post a code snippet of what you're trying and what errors if any are you seeing? |
@samtsai I am using this code to combine the responses of multiple apis:
|
try the code snippet I posted above: You can start it at |
For anyone still tracking this I created a gist with improved Types: https://gist.github.com/samtsai/687b117bf6de73a0aa2229b2b69aee1b You can now use it like, where
|
Stumbled on this problem today and came up with the following solution after reading a few comments in this issue. I'm still wondering why declare global {
namespace Cypress {
interface Chainable {
all(
thenableGetters: (() => Chainable<unknown>)[]
): Chainable<Promise<unknown>>
}
}
}
Cypress.Commands.add('all', thenableGetters =>
cy.wrap(
new Promise(resolve => {
const result = []
const firstLink = thenableGetters.shift()
thenableGetters
.reduce(
(chain, nextLink) =>
chain.then(value => {
result.push(value)
return nextLink()
}),
firstLink()
)
.then(value => {
result.push(value)
resolve(result)
})
})
)
)
export {} |
It's 2022 and we still don't have a |
The same thing happened to me when trying to load multiple fixtures in parallel:
In this case, both console logs print the values inside |
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
We can even spread results using built-in Bluebird promise
spread
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"
I can see that it really grabbed each link correctly during in the reporter
But the arguments
commands
andutilities
are both "Utilities" elementsAlso, 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
The text was updated successfully, but these errors were encountered: