-
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
Await-ing Cypress Chains #1417
Comments
Cypress commands are not 1:1 Promises. https://gitter.im/cypress-io/cypress?at=5a9ec8bf458cbde55701c7a8 They have promise like features in the sense that they have a This has serious implications where the What is possible - is to use You could not use Because Cypress enqueues commands on a master singleton, it already manages the async coordination for you. It's impossible to ever lose a chain of commands. What this means is - you would sometimes add the This also means you wouldn't use the This IMO would end up being confusing. You would use FWIW you can already avoid callback hell in Cypress by writing much more terse JS. There is almost never a situation where you ever would need to create nested callbacks, or ever need to make heavy use of describe('Filter textbox', () => {
beforeEach(() => {
cy
.get(test('suplovaniTable')).as('suplovaniTable')
.invoke('html')
.then(sha256)
.as('hash')
cy.get(test('filterTextbox')).as('filterTextbox')
})
it('changes data on filter text change', () => {
cy
.get('@suplovaniTable')
.find('tbody > :nth-child(1) > :nth-child(2)')
.invoke('text')
.then((text) => {
cy.get('@filterTextbox').type(text)
})
cy
.get('@suplovaniTable')
.invoke('html')
.should(function ($html) {
expect(this.hash).to.not.equal(sha256($html))
})
})
}) |
It's a lot about a mindset. Being a freshman at Cypress and using Seeing this explanation now (and some discussion on gitter) I think it makes total sense not to support Just to be clear from that last example. Why is there cy
.get('@suplovaniTable')
.invoke('html')
.then(function ($html) {
expect(this.hash).to.not.equal(sha256($html))
}) PS: I think there is a small mistake of using |
Yes. Mocha does this as well (as does jQuery). Much of the time it doesn't matter since people use closures instead of You can see it here: https://github.com/cypress-io/cypress/blob/develop/packages/driver/src/cy/commands/connectors.coffee#L350. The type definitions also indicate the context is changed (although there isn't a good way to know what's on that context since interfaces cannot be mutated by a runtime) |
I will open a new issue for adding a We can make This would allow you to replace the: |
@FredyC Using Read what I'm writing below - and then apply that to both of the cases of using To be clear - it possible to use However - with that said, using In every use case of The root cause of the problem is documented here: https://docs.cypress.io/guides/core-concepts/conditional-testing.html# Whenever you use a For instance you're typing Of course, this isn't always the case, if you're using vanilla javascript or jQuery, then you can be sure things have rendered synchronously. But try this with modern frameworks and you'll potentially be digging yourself a hole that's tough to debug. The key to determinism is already knowing the desired result ahead of time. When that is known, you don't need to yield dynamic values - because they're not dynamic, they're static. Doing that will avoid even the need for yielding values from the DOM. |
@brian-mann with cy
.get('@suplovaniTable')
.invoke('html')
.then(sha256)
.should('not.equal', cy.alias('hash'))) |
Yes but only if its been evaluated already. It won't make anything easier - it's just a simple replacement for // nope, same problem
cy.wrap('foo').as('f')
cy.wrap('foo').should('eq', cy.alias('f')) // alias is not defined yet // yup, works
cy.wrap('foo').as('f').then((str) => {
expect(str).to.eq(cy.alias('f'))
}) |
Ah. In your example above, the
|
The following example shows the limitations of using it('should work?', async () => {
const body1 = await cy.get('body')
const body2 = await cy.get('body')
cy.get('body').then(body => console.log('body', body)) // logs jQuery-wrapped body tag
// "promises" have resolved by this point
console.log('body1', body1) // logs jQuery-wrapped body tag
console.log('body2', body2) // logs undefined
}) it('should work?', async () => {
const [body1, body2] = await Promise.all([
cy.get('body').toPromise(),
cy.get('body').toPromise(),
])
// "promises" have resolved by this point
console.log('body1', body1) // logs jQuery-wrapped body tag
console.log('body2', body2) // logs jQuery-wrapped body tag
}) I agree it is a bit strange especially with the prominence of I think it is best to think about Cypress commands as streams or pipelines with |
@NicholasBoll I am curious why did you call If this really is not working, then there should be some big red warning in docs about using Although it's really hard to understand why is it actually different. I mean |
On the other hand, I could imagine doing something like this. cy.get('body').then(async ($body) => {
const first = await doSomethingAsyncWithBody($body)
const second = await doSomethingElseAsync(first)
...etc
}).alias('result') In this case, it makes a great sense to have |
@FredyC due to the functional nature of Promises even this I disagree with. Having callback hell is always a sign of poor design.
You could just as easily write it this way... cy
.get('body')
.then(doSomethingAsyncWithBody)
.then(doSomethingElseAsync)
.as('result') |
Oops, I didn't mean to leave I'm using Typescript and |
@brian-mann is right. The blog post I linked explains composition of promises and how that composition applies to Cypress tests. I tend to not use the custom Cypress Command API and just use |
I understand for most use cases async await shouldn't be supported, however what if you wanted to slowly step through each command and do non-cypress things inbetween. The naive example would be to console log inbetween cypress commands. Obviously you can't gaurantee your previous commands have been run, before the console.log. If you resolved after the last operation of the stream was completed, then a programmer could work with the gaurantee. Cypress can still be as easy and fast as possible, while giving programmers more control over the operations, without entering callback hell. [side note: Reading this issue cleared up my understanding of the asyncronicity of Cypress. It's awkward that you guys neither use async, nor generators, NOR async generators, but it's also reasonable that you don't, because the way your streaming works would require an async generator and frankly most programmers would be baffled on how to work with it.] |
@funkjunky It might help to know your use-case. I've learned to embrace Cypress's chaining mechanism for the declarative API Promises were meant to be. For instance, promises allow mixing of sync and non-sync code: cy
.wrap('subject') // cy.wrap wraps any parameter passed into a Cypress chain
.then(subject => {
console.log(subject) // logs 'subject'
return 'foo' // synchronous, but you can't do any cy commands unless you return cy.wrap('foo'). Cypress will complain about mixing sync and async code in a then
})
.then(subject => {
console.log(subject) // logs 'foo'
})
.then(subject => {
console.log(subject) // logs 'foo' - the previous then didn't return anything, so Cypress maintains the subject
return Promise.resolve('bar') // You can return promises
})
.then(subject => {
console.log(subject) // logs 'bar'
}) At any point you can use let foo = ''
cy.wrap('foo').then(subject => {
foo = subject // 'foo'
})
console.log(foo) // ''
cy.wrap('').then(() => {
console.log(foo) // 'foo'
}) This is why the Cypress docs don't recommend using cy.wrap('foo').as('foo')
// something stuff
cy.get('@foo').then(subject => {
console.log(subject) // 'foo'
})
|
@funkjunky @FredyC I made a library to do this: https://www.npmjs.com/package/cypress-promise I wouldn't recommend it for elements, but it works great for other subject types. I've been using it for a couple weeks now and it seems to be working well and is stable. The readme has lots of examples. You can either import the function and pass any Cypress chain as an input, or you can register it to attach it to the |
@NicholasBoll That is very cool, I may try it once I muster the courage (have had issues w/ Cypress promises lately). |
We recently tried out adding We are strongly considering rewriting the command enqueueing algorithm to less asynchronous and more stream-like by preventing chains of commands from resolving independently of each other. Instead they will act as a function pipeline where one receives the subject of the previous synchronously. Even when we reach the end of the command chain, we will begin calling the next chain synchronously instead of asynchronously. Why? Because the DOM may change in between the effective Why does this matter to I'm considering trying to shoehorn To make matters worse - the Example: // nope no await
cy.visit()
// yup we get the $btn
const $btn = await cy.get('form').find('button').contains('Save').click()
const t = $btn.text()
// nope no await since we don't want to yield anything
cy.get('input').type('asdf') This will be perfectly valid syntax in Cypress and yet it makes no sense. Why would we only have to cy.visit()
cy.get('form').find('button').contains('Save').click().then(($btn) => {
const t = $btn.text()
cy.get('input').type('asdf') Well it's because Cypress handles the enqueuing for you because it has to and you only need to grab references to objects once it's 100% sure its passed all assertions and is stable to hand off to you - otherwise it's impossible to enqueue things out of order or fire off things before they are ready. This adds a level of guarantee but it's unlike how |
@brian-mann In my opinion async/await is more like for setup scenario. And I wan to add my 5 cents... Usually projects already have stuff that setup scenario like the user creation, setting email .... so any project might have already code that setup some stuff for tests. But maybe I am wrong in how I am thinking and you can help me. In our project, we should test the app every time in a new domain. So basically to create the scenario we should do a couple of request to the backend and create a domain, a user and stuff like that. We have a folder with classes that already do that. So from Cypress tests, we just should call those When the scenario is created we visit the page. But In production our app take the domain name from the URL, unfortunately in tests it is not possible. So basically I hack the window object let domainName=""
it("create domain", () => {
createDomain().then(data => domainName=data.domainName)
})
cy.visit("", {onBeforLoad: (win) => { win.domainName = domainName } }) I don't care so much regarding await let user:any
let domain: any
User.createRandomUser()
.then(rawJson => {
user = rawJson.data.user
return Domain.createRandomDomain()
})
.then(rawJson => {
domain = rawJson.data.domain
return cy.fixture("imageUrls")
})
.then(images => {
return User.addPictureToUser(images.url1)
})
.then(pictureId => user.pictureId = pictureId) In this case async/await is really helpful you can't say else
let user:any
let domain: any
user = (await User.createRandomUser()).data.user
doman = ( await Domain.createRandomDomain()).data.domain
images = await cy.fixture("imageUrls")
imageId = await User.addPictureToUser(images.url1)
user.pictureId = pictureId Maybe I am wrong but I think the "stumbling block" of every test framework is the test scenario Anothe point is the unit tests. I have read that Cypress want to suport that. IF somebody will write code with async/awain this mean Cypress will be unable to make unit tests for this kind of code? |
@brian-mann In a way to avoid the usage of const/let as you suggest here (#1417 (comment)) I found the wrap/invoke methods, but I get even more confounded :( cy.wrap({ createDomain: Domain.createDomain })
.invoke("createDomain")
.as("domain");
cy.get("@domain")
.wrap({ crateUser: User.createUser }) // confusion how to get the domain without const/let???
.invoke("crateUser", domain.name)
.as("user"); how it is suposed to get domain for the next method in the chain for using it inside createUser ? |
@andreiucm you just nest the Your first example doesn't really make a lot of sense to me because you're not using the return User.createRandomUser()
.then(res => {
const user = res.data.user
return Domain.createRandomDomain()
.then(res => {
// ?? no use here
const domain = res.data.domain
return cy.fixture('imageUrls')
})
.then(images => {
return User.addPictureToUser(images.url1)
})
.then(pictureId => user.pictureId = pictureId)
}) Your other example... return Domain.createDomain()
.then((domain) => {
return cy.wrap(domain).as('domain')
}) ...somewhere else in your code... cy.get("@domain")
.then((domain) => {
return User.createUser(domain.name)
})
.then((user) => {
return cy.wrap(user).as('user')
}) Really the problem is that you're mixing two different patterns - cypress commands and async returning utility functions. If the utility functions are just abstractions around Cypress commands you don't have to synchronize all of the async logic... but if they are then you'll need to interop between the two patterns. Have you read this guide? https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Asynchronous |
@andreiucm since I get that the one single advantage is that it prevents nesting We can allow that pattern but we'll be actively fighting against what the browser transpiles it into since we are essentially trying to prevent having commands forcibly run on the nextTick / microtask. |
I understand your problem with async/await but to be honest this nesting I have read the https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Asynchronous but let me ask again import {createDomain} from "../../..my-core/createDomain"
describe("..........
let domainName
it(" create domain and land on the page", () => {
createDomain()
.then(dn => domainName = dn)
cy.visit("", {onBeforeLand:(win)=>{win.domainName=domainName}})
}) and to work should be import {createDomain} from "../../..my-core/createDomain"
descrive("..........
it(" create domain and land on the page", () => {
createDomain()
.then(dn => {
cy.visit("", {onBeforeLand:(win)=>{win.domainName=dn}})
})
}) Note: this code is still wrong since Cypress doesn't wait for the promise createDomain to be resolved it("....
return createDomain(... but if we do that the in console Cypress display a warning saying something like we mess cypress commands with asynchronous code! Maybe is there a say how to wrap the external asynchronous code inside Cypress commands or how to deal when we have asynchronous function and cy. operation together? |
+1 |
And on mobile the subscription button is at the bottom of this page. |
Yeah, Playwright works fully with |
But why would then async/await work successfully in the interactive mode? |
No idea. |
Any update on this? |
I see it is removed from cypress app priorities? Will this be changed? Currently async/await can't be used for api requests in the tests, so we have to use multiple nested thens. |
Yes. Same here. I have several instances where I have to work around or just don't test because of that limitation. async/await would be very helpful. |
Do you have a workaround for that? I am having mulitple nested thens for the methods with the api requests i am calling inside of the tests and it doesnt look very good and readable. Is this a common problem? |
No.
Currently there is no way around that. |
Can Cypress aliases help there? They usually help avoiding nested |
I can't remember exactly in my cases. But I know about aliases and it didn't make sense at the time. |
@alewolf @mladshampion @HendrikJan you can use my experimental plugin https://github.com/bahmutov/cypress-await to do what you want, even skipping writing const table = await cy.get("@suplovaniTable");
const hash = sha256(table[0].innerHTML); But I would ask anyone to provide an example of what is difficult to do as is in Cypress. Then I can show how Cypress solves it without much of boilerplate, for example see https://glebbahmutov.com/blog/setup-cypress-data/ |
@mladshampion I think the workaround would be to use Playwright 😬 |
We've migrated to playwright recently because of this issue |
Thank you for your reply. I hope it helps other people. It's code like this that is difficult to do in Cypress: import { someFunction } from "somewhere";
const someValue = await someFunction();
await cy.get(someValue).click(); I do appreciate that there is a plugin, but I still hope that Cypress will support async/await out of the box in the future. |
@bahmutov
I am calling the method like this:
I am using this to do multiple actions through the api and observe the results or do actions between two users. |
Any ideas, please? |
Use plywright, |
we are considering moving to playwright just bcs of this issue. so many nested callback. specially if we need to do something with conditions. or accessing something deep down inside a table for example if value exist, press that button, if doesn't exist, do something before that and then add button. if we use jquery for everything, magically changes happen in the page without even cypress record anything. but if something goes wrong then assertion will fails, but maybe the problem was "do something before pressing that button". Also I couldn't find a way to group what is happening in test body. the test body is a mess. need to add log here and there to know what inner chain of functions is being called. instead of being able to group functions together. I'm not sure what cypress team is focusing on right now, but they are loosing edge against playwright very fast |
@bahmutov I have tests that test workflows on WooCommerce shops. I know this is a complex test. But there is no way to break it up into smaller tests, because all data and network calls on the purchase confirmation page depend on all previous steps. You can imagine that many of those tests normally require cy.X.then() and lead to nested tests. Apart from promise hell, those make code reuse more difficult. Thanks for providing the experimental await plugin available. I believe it will make things much easier. But I might only start using it once it becomes officially part of Cypress. I wonder why Cypress is pushing back so much on this. |
@alewolf Give me an example test like the one causing the problem and I will show how to refactor it to a simpler and more elegant solution. As far as the experimental await plugin - I will try to finish the remaining TODO tasks there to be able to use it in production. My personal opinion on why the Cypress team is ignoring this problem (which I see now being a bigger and bigger talking point by people who really dislike Cypress in general) is that ... Cypress team is pretty self-centered and does not really "get" the obstacles the users are facing. |
@bahmutov your plugin does not work anymore. I would love to use it otherwise. |
I want to clear up a common misconception: Cypress doesn't need to return something with const plainObject = {
then(onSuccess, onError) {
setTimeout(() => { onSuccess('hello'); }, 1000)
}
};
const returnValue = await plainObject;
console.log(returnValue); This is valid JavaScript, and TypeScript recognizes it. I'm not familiar with Cypress' internals, but to me it looks like this issue can be solved without breaking backwards compatibility by acting as a promise when |
That's strongly dismissive, and I don't think that's a fair assessment. 🤔 I don't think anyone would be here contributing their opinions and concerns if they generally disliked Cypress. People don't typically seek out ways to stir the pot. Instead, most of the people contributing more than the unprofessional "use PlayWright" have genuinely invested in Cypress, and it instead comes from having a vested interest in Cypress. Mismatch of Expectations: Language-barriers are frustratingI think the general frustration here has mainly centered around a Mismatch of Expectations:
It would be completely illogical for someone to characterize these decisions as intentionally malicious. 👍 But, more often than not, the ones writing the Unit Tests are also writing the Cypress tests. Unit Tests are testing while executing. Cypress tests work by executing the script separately, using it to build a queue, and performing the actions that have been queued. But the intuition is that Cypress tests will be executed like Unit Tests. Someone who is used to writing Unit Tests all day will see the syntax choices here of using Granted, Cypress' is more like a streaming task-runner. So, while it's pushing new actions onto the queue, it's also popping old actions off the queue and performing them. And it's very fast and efficient at this. In fact, this method of test running improves the perception that the tests are actually running faster. Throwing Thenables and Async/Await into the mixAnd then we got Until Async/Await, the Task Runner style of execution hasn't always been a major problem for everyone. Confusing and frustrating? Sometimes; Expectations of test execution weren't always meeting reality, and it confuses people trying to write tests. I think Async/Await is more like the straw that broke the camel's back. 😞 That's in stark contrast to this being another opportunity for someone who really dislikes Cypress to go out of their way to come in here and bash it because it lives rent-free in the dark corner of their mind. People have better things to do. I can't imagine the negative characterizations here help in the retention department very much either. 😞 |
Did you just explain Cypress to their ex CTO? |
@muratkeremozcan LOL. No, but I probably could have done a better job of delineating the contributions to the general conversation as a whole, and my specific response to @bahmutov. In general, the principal point of my response was not necessarily to explain the architecture, but the aspects of Cypress' architecture that contribute to the source of people's frustration:
My point was to highlight the aspects of Cypress' architecture that are confusing, in a way that also helps other readers also understand how Cypress works if they just now coming here and weren't aware. It's understandable they're frustrated, but to characterize the people here as those who Again, people have better things to do. Cypress is fast and efficient. That's not the problem. The reason people are writing Cypress tests incorrectly is because Cypress is That's why this argument completely misses the point:
That's like Again, there's a reason why modifying JavaScript built-ins is so frowned upon: expectations no longer meet reality, and things behave differently than people expect them to. Imagine if |
Adding onto this -- in terms of Vue code for component testing, there's not a lot of documentation around For example, lets say I'm testing a component with a button that upon click, it emits an event with an asynchronous callback. Most cases, vue suggests to wrap these in a In short, without minding the fact I'm probably handling it incorrectly in Cypress, there is a definite need for component tests to be able to handle asynchronous emits, but it's not essentially clear how this can happen. |
Current behavior:
await
-ing cypress chains yields undefinedDesired behavior:
await
-ing cypress chains yields the value of the chain (since the chain is a promise-like, it should work with await out of the box)Additional Info (images, stack traces, etc)
I understand the recommended way to use cypress is closures, however, when dealing with multiple variables, we run into callback hell's lil' brother closure hell.
The ability to use
await
on Cypress chains could be beneficial in many ways:Example code using closures
Example code using
await
The text was updated successfully, but these errors were encountered: