Skip to content
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

Open
vacekj opened this issue Mar 5, 2018 · 121 comments
Open

Await-ing Cypress Chains #1417

vacekj opened this issue Mar 5, 2018 · 121 comments
Labels
type: feature New feature that does not currently exist

Comments

@vacekj
Copy link

vacekj commented Mar 5, 2018

Current behavior:

await-ing cypress chains yields undefined

Desired 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:

  • avoiding nesting closures many levels deep, thus preventing unreadable code
  • making it easy for programmers familiar with traditional async patterns to start using Cypress

Example code using closures

describe('Filter textbox', () => {
    beforeEach(() => {
        cy.get(test("suplovaniTable")).as("suplovaniTable");
        cy.get(test("filterTextbox")).as("filterTextbox");
    });

    it('changes data on filter text change', () => {
        cy.get("@suplovaniTable").then((el) => {
            return el[0].innerHTML;
        }).then((innerHTML) => {
            return sha256(innerHTML);
        }).then((hash) => {
            cy.get("[data-test=suplovaniTable] > tbody > :nth-child(1) > :nth-child(2)")
                .then((tds) => {
                    const text = tds[0].innerText;
                    cy.get("@filterTextbox").type(text);
                    cy.get("@suplovaniTable")
                        .then(el => el[0].innerHTML)
                        .then((innerHTML) => {
                            expect(hash).to.not.equal(sha256(innerHTML));
                        });
                });
        });
    });
});

Example code using await

describe('Filter textbox', () => {
    beforeEach(() => {
        cy.get(test("suplovaniTable")).as("suplovaniTable");
        cy.get(test("filterTextbox")).as("filterTextbox");
    });

    it('changes data on filter text change', async () => {
        const table = await cy.get("@suplovaniTable");
        const hash = sha256(table[0].innerHTML);

        const filterCell = await cy.get("[data-test=suplovaniTable] > tbody > :nth-child(1) > :nth-child(2)");
        await cy.get("@filterTextbox").type(filterCell[0].innerText);

        const newTable = await cy.get("@suplovaniTable");
        const newHash = sha256(newTable[0].innerHTML);
        expect(hash).to.not.equal(newHash);
    });
});
  • Operating System: Windows 10 x64
  • Cypress Version: Beta 2.1.0
  • Browser Version: Chrome 64
@jennifer-shehane jennifer-shehane added type: feature New feature that does not currently exist stage: proposal 💡 No work has been done of this issue labels Mar 5, 2018
@brian-mann
Copy link
Member

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 .then(fn) and they can assume other thenables and do the right thing, but they themselves are not true Promises.

This has serious implications where the async/await implementation would simply not work correctly in Cypress.

What is possible - is to use async/await to replace the .then(fn) but even that has problems.

You could not use try / catch because that is the same thing as .catch() for Promises, which Cypress does not and will never have.

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 await keyword when you need to work with the yielded value of a command - but then mostly not do this.

This also means you wouldn't use the async keyword on functions that return Cypress commands.

This IMO would end up being confusing. You would use async/await inconsistently from how you'd use it outside of Cypress. It would require a lot of explanation in the docs.

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 const.

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))
    })
  })
})

@danielkcz
Copy link

danielkcz commented Mar 6, 2018

It's a lot about a mindset. Being a freshman at Cypress and using async/await almost everywhere now it's tough getting used to the different paradigm for an async code.

Seeing this explanation now (and some discussion on gitter) I think it makes total sense not to support async/await. Might be worth to link this issue in docs as I believe many people will be asking for it, especially when seeing examples with .then can give a false impression that it's a great candidate for async/await.


Just to be clear from that last example. Why is there .then and .should acting like it's the same thing? I mean both are accepting callback and getting a value of the chain. I suppose that the major difference is that .should will retry till callback passes (or timeouts)? Using .then instead would work only if assertion would pass on the first run. Is that correct?

    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 function instead of fatarrow as this.hash won't be available? Unless all callbacks are bound to test context internally?

@NicholasBoll
Copy link
Contributor

PS: I think there is a small mistake of using function instead of fatarrow as this.hash won't be available? Unless all callbacks are bound to test context internally?

Yes. Mocha does this as well (as does jQuery). Much of the time it doesn't matter since people use closures instead of this.

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)

@brian-mann
Copy link
Member

I will open a new issue for adding a cy.alias function to avoid the use of this. I don't use this anymore in any of my code, and with fat arrow functions it'll never point to the right context anyway.

We can make cy.alias smart to point out that if you call it before an alias is defined, that it will throw a nice error explaining what you're doing wrong.

This would allow you to replace the: this.hash with cy.alias('hash') which would return a synchronous value.

@brian-mann
Copy link
Member

brian-mann commented Mar 6, 2018

@FredyC .then() and .should() are not the same thing. They're completely different. That's the difference - they both accept a callback function but otherwise are completely dissimilar.

Using .should() retries which insulates you from having to know the precise moment the state has settled. You can simply "describe" what you want, and Cypress will retry until it matches.

Read what I'm writing below - and then apply that to both of the cases of using .then(). Both of these uses can possibly lead to non-deterministic results that will fail your tests.


To be clear - it possible to use async/await in Cypress anytime you would otherwise use .then(fn).

However - with that said, using .then() frequently enough to need this is generally a sign of an anti pattern.

In every use case of .then() we are "locking" ourselves into something that may or may not give us a guarantee of the correct value. The whole reason we're even using .then() is to make our tests "dynamic" - but abuse of that can quickly lead to non determinism.

The root cause of the problem is documented here: https://docs.cypress.io/guides/core-concepts/conditional-testing.html#

Whenever you use a .then() if you've provided no assertions then there is no guarantees its value is even correct.

For instance you're typing text after yielding. What is the guarantee that text is correct? What if it hasn't rendered yet? What if its about to change? You'd need to add an assertion before yielding text. And if you can add an assertion about its state then you already know what the value is, and don't need to yield it.

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.

@NicholasBoll
Copy link
Contributor

@brian-mann with cy.alias, the following would be possible, correct?

cy
  .get('@suplovaniTable')
  .invoke('html')
  .then(sha256)
  .should('not.equal', cy.alias('hash')))

@brian-mann
Copy link
Member

brian-mann commented Mar 6, 2018

Yes but only if its been evaluated already. It won't make anything easier - it's just a simple replacement for this and we can make it smart and throw when you attempt to use aliases which haven't been defined yet - as opposed to just returning undefined.

// 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'))
})

@NicholasBoll
Copy link
Contributor

Ah. In your example above, the .as('hash') is in the beforeEach, so it has been evaluated. That might still be a bit tricky to know when things have been evaluated.

cy.alias would have the benefit of being able to give a warning/error with a link to the documentation about this caveat. You can track the setting of items on the internal state object and issue a warning or error about cy.alias being used on a property that hasn't been set yet. A link to documentation about this in the message would help.

@NicholasBoll
Copy link
Contributor

The following example shows the limitations of using async/await in a Cypress test:

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 async/await

I think it is best to think about Cypress commands as streams or pipelines with .then as a way to unwrap (or compose). cy.get is a common entry to this stream of data (Cypress calls them subjects). Some Cypress commands mutate DOM state (.type()), some transform the subject (.its()), but almost all return a subject for the next command in the stream. When you're done with that stream, alias it if you need it later.

@danielkcz
Copy link

danielkcz commented Mar 6, 2018

@NicholasBoll I am curious why did you call .toPromise() in a second example? I don't even know where that API came from, don't see anywhere else. However, what happens if you do it the first example as well?

If this really is not working, then there should be some big red warning in docs about using async/await as it can only lead to headaches.

Although it's really hard to understand why is it actually different. I mean await uses .then under the hood, so why it wouldn't be able to get the value out of it? There is definitely something fishy about it.

@danielkcz
Copy link

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 async/await available otherwise it would become callback hell once again. Yea sure, it can be considered edge case, but it can happen.

@brian-mann
Copy link
Member

brian-mann commented Mar 6, 2018

@FredyC due to the functional nature of Promises even this I disagree with. Having callback hell is always a sign of poor design.

async/await is the imperative form of Promises.

You could just as easily write it this way...

cy
.get('body')
.then(doSomethingAsyncWithBody)
.then(doSomethingElseAsync)
.as('result')

@NicholasBoll
Copy link
Contributor

Oops, I didn't mean to leave .toPromise() in the example. I was attempting to find out how the subject was passed around and why the promise gets lost. Commands continue to get enqueued. The presence of toPromise() makes no difference.

I'm using Typescript and Promise.all doesn't consider a Cypress chainer to be a valid promise type even though it works at runtime. I did a custom command that returns a real Promise to make Promise.all happy.

@NicholasBoll
Copy link
Contributor

@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 .then(myFunction). Typescript throws a type error if myFunction gets the wrong input, which is what Cypress does at runtime with custom commands.

@funkjunky
Copy link

funkjunky commented Mar 22, 2018

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.
(Not saying this is easy to implement, but would it not be purely beneficial?)

[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.]

@NicholasBoll
Copy link
Contributor

@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 .as('name') to save the subject for later use. The real gotcha is:

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 let or const. The previous could be written differently:

cy.wrap('foo').as('foo')
// something stuff
cy.get('@foo').then(subject => {
  console.log(subject) // 'foo'
})

.as can be used to prevent callback hell

@NicholasBoll
Copy link
Contributor

@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 cy object. I've done both and feel the latter is more natural.

@EirikBirkeland
Copy link
Contributor

EirikBirkeland commented Jun 9, 2018

@NicholasBoll That is very cool, I may try it once I muster the courage (have had issues w/ Cypress promises lately).
I wonder if your lib can be included in Cypress itself natively? If you think that's possible, It'd be awesome to see an issue or PR. Anyway, thanks a lot for sharing!

@brian-mann
Copy link
Member

brian-mann commented Jun 9, 2018

We recently tried out adding async/await to Cypress and it is theoretically possible to add - however it will almost certainly be grounds for more confusion and cause separate issues.

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 nextTick calls - and that means testing is less deterministic. The only way to make it truly deterministic is to remove any artificial asynchronousity that can potentially introduce flake.

Why does this matter to async/await? Because it will forcibly introduce async code where we are trying to prevent it at all costs. We've written our own thenable implementation to make the API familiar to people but we sacrifice ergonomics for determinism, which is the entire point of writing tests and using Cypress. I don't believe its worth the trade-off.

I'm considering trying to shoehorn async/await into here because I want to design an API that is familiar to people, because it takes so much explanation to try to change their behavior away from doing something they want to do, but don't understand why it's not a good idea to do this in the first place. They think Cypress should behave the same way "as every other JS framework" which is fine, but because of Cypress's architecture it is actually like asking it to give up its sole differentiating power to make this happen.

To make matters worse - the await keyword will not even be used in most cypress chains unless you want to yield the subject. So even if we shoehorn it in, it will not behave the same as other frameworks.

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 await some things and not others? The above code is equivalent to the bottom code...

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 async/await would normally work.

@andreiucm
Copy link

andreiucm commented Jun 28, 2018

@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 cy.get.... but code get much cleaner with await
for scenario where I should do:

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

                                          VS
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
since nowbody want to rewrite the code they already have. But maybe I am miss something, what do you sugest?

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?

@andreiucm
Copy link

andreiucm commented Jun 28, 2018

@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 ?
I guess there should be a solution based on the chain "philosophy" but I can see it?

@brian-mann
Copy link
Member

brian-mann commented Jul 1, 2018

@andreiucm you just nest the .thens() inside of the previous so that you have a closure around the variables you want to use. It's not anymore complicated than that. Hoisting using let is a common antipattern and it's unnecessary. That creates mutable state which is always less preferred than functional patterns.

Your first example doesn't really make a lot of sense to me because you're not using the domain variable anywhere. You do use the user... but this code really below doesn't have much to do with Cypress since I'm assuming these other functions are async returning utilities not going through Cypress commands...

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

@brian-mann
Copy link
Member

brian-mann commented Jul 1, 2018

@andreiucm since async/await is literally just sugar on top of Promises, there is no actual functional gain to utilizing that pattern -> it is a direct and straight 1:1 transpilation over Promises. Whatever you can do with async/await you can do with promises.

I get that the one single advantage is that it prevents nesting .then() callbacks. That is a real advantage. However it will never really "fit" within the Cypress command ecosystem because as mentioned before that cypress commands themselves are not true promises. They are promise like but more akin to steams. There is no way to interop these things correctly. Doing so will lead to nondeterminism and flakiness.

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.

@andreiucm
Copy link

andreiucm commented Jul 1, 2018

I understand your problem with async/await but to be honest this nesting .then blocks are really annoying

I have read the https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Asynchronous but let me ask again
If I have some asynchronous function that doesn't use any Cypress commands like cy.request (I use superagent library to make the request to backend). Do this mean next code is wrong?

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
so to make it work we should switch to

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?

@yannicklerestif
Copy link

+1
Protractor / Selenium are deprecating their old specflow (that works just like Cypress) and replacing it with a native promise based one.
In my previous assignment, we migrated our tests to use async / await and really makes things way clearer.
Using Cypress now and it's clearly an improvement over protractor / selenium on many topics, but this is a big disappointment.

@mladshampion
Copy link

I have the same issue trying to use async/await in my tests. I use it for awaiting multiple cy.request responses instead of using multiple then. What I don't understand is why the async/await works successfully when I run the tests in the interactive mode with cypress open and hang on the first await as if it's never resolved in the command line mode (cypress run). Does anyone have an idea why is that behavior and why is there a difference? Is there anything I could do to make it work (as I need to use multiple cy.requests, so I have multiple nested then)? Is it possible to overwrite the then method to achieve this?

Go to the documentation.

This is one of the reasons I moved to PlayWrigth.

So there's no way to do this? I have multiple nested thens and it makes it hard to read. Chatgpt says i can overwrite the then command to treat the standard Promise as Cypress promise, which is done in the interactive mode. Does PlayWrigth support async/await?

@Swivelgames
Copy link

Swivelgames commented Apr 27, 2023

@mladshampion Unfortunately, no, async/await is not supported, which is the reason for this issue ticket. If you'd like to follow the progress and discussion, be sure to subscribe to notifications at the top of this thread in the right-hand sidebar.

Also, probably better to keep PlayWright out of this issue ticket 🙂

@HendrikJan
Copy link

And on mobile the subscription button is at the bottom of this page.

@SalahAdDin
Copy link

SalahAdDin commented Apr 28, 2023

I have the same issue trying to use async/await in my tests. I use it for awaiting multiple cy.request responses instead of using multiple then. What I don't understand is why the async/await works successfully when I run the tests in the interactive mode with cypress open and hang on the first await as if it's never resolved in the command line mode (cypress run). Does anyone have an idea why is that behavior and why is there a difference? Is there anything I could do to make it work (as I need to use multiple cy.requests, so I have multiple nested then)? Is it possible to overwrite the then method to achieve this?

Go to the documentation.
This is one of the reasons I moved to PlayWrigth.

So there's no way to do this? I have multiple nested thens and it makes it hard to read. Chatgpt says i can overwrite the then command to treat the standard Promise as Cypress promise, which is done in the interactive mode. Does PlayWrigth support async/await?

Yeah, Playwright works fully with async/await.

@mladshampion
Copy link

But why would then async/await work successfully in the interactive mode?

@SalahAdDin
Copy link

But why would then async/await work successfully in the interactive mode?

No idea.

@mladshampion
Copy link

Any update on this?

@mladshampion
Copy link

mladshampion commented Nov 10, 2023

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.

@alewolf
Copy link

alewolf commented Nov 10, 2023

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.

@mladshampion
Copy link

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?

@alewolf
Copy link

alewolf commented Nov 10, 2023

Do you have a workaround for that?

No.

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?

Currently there is no way around that.
And yes, it is a common problem with Cypress.

@mbolotov
Copy link

mbolotov commented Nov 10, 2023

Currently there is no way around that.

Can Cypress aliases help there? They usually help avoiding nested then calls.

@alewolf
Copy link

alewolf commented Nov 10, 2023

Can Cypress aliases help there? They usually help avoiding nested then calls.

I can't remember exactly in my cases. But I know about aliases and it didn't make sense at the time.

@bahmutov
Copy link
Contributor

@alewolf @mladshampion @HendrikJan you can use my experimental plugin https://github.com/bahmutov/cypress-await to do what you want, even skipping writing = await cy everywhere:

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/

@thomasaull
Copy link

Do you have a workaround for that?

@mladshampion I think the workaround would be to use Playwright 😬

@maxnowack
Copy link

We've migrated to playwright recently because of this issue

@HendrikJan
Copy link

@alewolf @mladshampion @HendrikJan you can use my experimental plugin https://github.com/bahmutov/cypress-await to do what you want, even skipping writing = await cy everywhere:

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/

Thank you for your reply. I hope it helps other people.
Unfortunate for Cypress, I moved over to another framework that currently better fits my needs.

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.

@mladshampion
Copy link

mladshampion commented Nov 13, 2023

@alewolf @mladshampion @HendrikJan you can use my experimental plugin https://github.com/bahmutov/cypress-await to do what you want, even skipping writing = await cy everywhere:

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/

@bahmutov
I have api requests like this one:

postFriendInvite(userId: string, token: string): Cypress.Chainable<any> {
        return cy.request({
            method: "POST",
            url: Endpoints.baseUrl + "/invite/" + userId,
            headers: { Authorization: token }
        });
    }

I am calling the method like this:

requestHelper.postRegister(email, lang, password).then((regResponse) => {
                requestHelper.postLogin(email, password).then((loginResponse) => {
                           profileRequests.postFriendInvite(id, token).then((likeResponse) => {

I am using this to do multiple actions through the api and observe the results or do actions between two users.
So this way i have at least 3 nested thens. In most tests i have to do even more and sometimes i end up with 5-6-7 nested thens.
The most interesting thing is that using async/await works when i run the tests with cypress:open and does not work when i run them using cypress run

@mladshampion
Copy link

Any ideas, please?

@SalahAdDin
Copy link

Any ideas, please?

Use plywright,

@mhamri
Copy link

mhamri commented Dec 8, 2023

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.
or for example, find the column number with that name, then find the row, inside that row find the td based on column index , inside that td, check the state, if it's this state click this, if that state click that. and god help the developer if they need to check multiple state to decide what button to press

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

@alewolf
Copy link

alewolf commented Dec 31, 2023

@bahmutov I have tests that test workflows on WooCommerce shops.
In many of them, the workflow is to open a page, wait until certain functions have loaded, then run some of those functions, and only after they are finished, click on a button (like the checkout button) and continue to another page. At the same time, I check network calls, check if they send the correct data, and check if the network calls are not duplicated. And on top of that I am using the same tests with small variations because I have two versions of my app (a free and a pro version). So, I have to save some variables to re-use them. One of them needs to retrieve a part of the purchase confirmation URL to get the order ID and then use it in two different tests later on.

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.

@bahmutov
Copy link
Contributor

bahmutov commented Jan 1, 2024

@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.

@vesper8
Copy link

vesper8 commented Jan 11, 2024

@bahmutov your plugin does not work anymore. I would love to use it otherwise.

@TimVee
Copy link

TimVee commented Jan 19, 2024

I want to clear up a common misconception: Cypress doesn't need to return something with Promise in the prototype chain for await to work. It only needs to implement the Thenable interface. Just try the following in your console:

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 .then() is called with two functions, and keeping the current behaviour otherwise. Additionally, according to the Cypress documentation, they get "excellent async support" from Mocha, so that shouldn't be a problem either.

@Swivelgames
Copy link

@bahmutov

by people who really dislike Cypress in general) is that ... Cypress team is pretty self-centered

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 frustrating

I think the general frustration here has mainly centered around a Mismatch of Expectations:

  • Task Runner style execution while using Unit Test syntax (like describe(), it(), etc...)
  • Non-standard .then implementation

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 .then, describe(), it(), etc... and be not-so-pleasantly surprised that it doesn't function like a Unit Test, and instead functions more like Gulp. That kind of confusion is understandably frustrating, and it feels like a bait-and-switch. 😞

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 mix

And then we got .then() 🎉 But unfortunately, people have been using .then() in their Cypress tests this whole time, and now they're finally realizing that, just like their Unit Testing expectations, these aren't real Promises. 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 .toString() returned a String Buffer, instead of a String, in some implementations. That's what people feel is happening here.

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. 😞

@muratkeremozcan
Copy link

@bahmutov

by people who really dislike Cypress in general) is that ... Cypress team is pretty self-centered

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 frustrating

I think the general frustration here has mainly centered around a Mismatch of Expectations:

  • Task Runner style execution while using Unit Test syntax (like describe(), it(), etc...)
  • Non-standard .then implementation

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 .then, describe(), it(), etc... and be not-so-pleasantly surprised that it doesn't function like a Unit Test, and instead functions more like Gulp. That kind of confusion is understandably frustrating, and it feels like a bait-and-switch. 😞

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 mix

And then we got .then() 🎉 But unfortunately, people have been using .then() in their Cypress tests this whole time, and now they're finally realizing that, just like their Unit Testing expectations, these aren't real Promises. 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 .toString() returned a String Buffer, instead of a String, in some implementations. That's what people feel is happening here.

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?

@Swivelgames
Copy link

Swivelgames commented Jan 24, 2024

@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:

  • Cypress uses Unit Test-like syntax, but executes like a Task Runner
  • Cypress introduced another bait-and-switch with .then(), where user's expectations were again led astray, when they were under the impression that they were using actual Promises. Maybe something like .pipe() would have been better, albeit maybe scarier to read for some users.
  • Now people are asking for Async/Await, and realizing that Cypress functions far differently than they felt they were lead to believe.

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 really dislike Cypress in general falls flat when you take into account that the majority of the people here have a genuine frustration that's borne from their vested interest in Cypress. I'd say that's a lot different than people going out of their way to find new reason to come bash Cypress.

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 co-opting syntax that already has deeply-ingrained expectations attached to them.

That's why this argument completely misses the point:

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.

That's like changing the fundamental definition of words, and then blaming other people for not understanding how to talk to you. And @bahmutov can't rewrite every company's tests for them. The fact that this concern and frustration is so widespread is because co-opting syntax makes it unintuitive because of the expectations already attached to them.

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 .toString() returned a Buffer, instead of a String. That's what people feel is happening here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature New feature that does not currently exist
Projects
None yet
Development

No branches or pull requests