How to wait for Cypress then() command to finish before returning a value?

Viewed 2720

I'm trying to set a variable within a .then() command which is declared outside of it, and after the whole block finished (the .then()) I'm returning that value.

The problem is, when I return the value, the variable is undefined, but within the .then() block, the variable is loaded.

Here is the example code:

public getValueFromElement(): string {
  cy.log("Obtaining the Value");

  let myNumber: string; // Here I'm declaring my variable

  cy.get(this.labelWithText).then(($element) => {

    let originalLabelText: string = $element.text();
    let splittedText: string[];
    splittedText = originalLabelText.split(": ");

    myNumber = splittedText[1]; // Here I'm assigning the value 
    cy.log("Inside the THEN" + myNumber); //This logs the number correctly

  });
        
  return myNumber; // But after I return it using the function, the value is `undefined`!
}

I'm assuming this could be related to the async / sync problem, as the return statement is being executed immediately when the function is called, and the promise created by the .then() is still running, but I don't know how to fix this.

Do you know how I can wait for the .then() to finish first before returning the value?

Thanks!!

4 Answers

You say "The problem is, when I return the value, the variable is undefined".

That's because the return myNumber line runs before the cy.get(this.labelWithText).then(($element) => { completes, because the command is running asynchronously.

You need to return the command itself, and also the derived myNumber is returned from inside the .then().

public getValueFromElement(): Chainable<string> {  // cannot return the raw string 
  cy.log("Obtaining the Value");
        
  return cy.get(this.labelWithText).then(($element) => {
    ...
    const myNumber = splittedText[1]; 
    cy.log("Inside the THEN " + myNumber)
    
    return myNumber
  })
}

Use it like this

getValueFromElement().then(myNumber => {
  cy.log("Outside the function " + myNumber)
})

You can do it synchronously like this

public getValueFromElement(): string {  
  cy.log("Obtaining the Value");
        
  const $element = Cypress.$(this.labelWithText)

  const originalLabelText: string = $element.text()
  const splitText = originalLabelText.split(": ")
  const myNumber = splitText[1]
    
  return myNumber
}

Here you sacrifice the retry options which are built into asynchronous commands.

Cypress says to use it only if you are sure the element exists already, which depends on the context of your text.

@MikhailBolotov indeed. This is how you'd handle that

cy.get("myOpenElementSelector").click()     // async code
  .then(() => {                             // must wrap sync code in then
    const myNumber = getValueFromElement()  // to ensure correct sequence 
    expect(+myNumber).to.eq(64)
  })

@Mihi has the idomatic way, but it's sometimes difficult when composing page object methods.

I've come to the conclusion that this works:

   public async getTheNumber(): Promise<string> { 

        return new Promise((resolve, reject) => {

            cy.log("Retrieving the number");

            cy.get(this.selector).then(($element) => {
                let myNumber = $element.text().split(": ")[1];

                cy.log(`The Number is ${myNumber}`);
                resolve(myNumber);
            });
        });
    }

and when reading it from the test I'm doing this:

myNumberAtTestLevel = await myObject.getTheNumber();

Thing is that I've seen that I have to change my it() method to async in order for this to work.

However, I've come across this documentation of Cypress: https://docs.cypress.io/api/utilities/promise#Syntax

I'm trying to implement the same using Cypress.Promises but I'm not able to.

Any ideas?

This was an incorrect answer, but I'm keeping it here for education purposes in case someone else stumbles upon the same issue.

You can NOT use await like this:

public async getValueFromElement(): string {
        cy.log("Obtaining the Value");
        
        let myNumber: string; // Here I'm declaring my variable

        let $element = await cy.get(this.labelWithText);

        let originalLabelText: string = $element.text();
        let splittedText: string[];
        splittedText = originalLabelText.split(": ");

        myNumber = splittedText[1];
        
        return myNumber
}

But be aware that now this function being async, itself returns a promise.


Why?

Here's what the docs say:

If you're a modern JS programmer you might hear "asynchronous" and think: why can't I just use async/await instead of learning some proprietary API?

Cypress's APIs are built very differently from what you're likely used to: but these design patterns are incredibly intentional. We'll go into more detail later in this guide.

Related