5

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!!

Michael Hines
  • 928
  • 1
  • 7
Jorge
  • 182
  • 1
  • 4
  • 12

5 Answers5

5

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)
})
Michael Hines
  • 928
  • 1
  • 7
2

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.

  • Under this way you also should be careful not mixing sycn and async code. So the following code won't work as you might expect: `cy.get("myOpenElementSelector").click()` // shows the target element `let myValue = getValueFromElement();` // problem here: at this time, the target element is yet not shown – Mikhail Bolotov Sep 11 '21 at 05:21
2

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?

Jorge
  • 182
  • 1
  • 4
  • 12
0

there is a very simple solution that I figured out after scratching my head for hours and trying different methods, we can't wait for most of the cypress methods and can't deal the .then() as the one in Javascript.

first we need to write a function which returns a pure promise not the cypress one

we need to use the asynchronous command inside the Promise's callback and call the resolve method with the value which needs to be returned.

Now we can use the async/await syntax to call our function which is returning pure Promise

Note: need to make sure that it handle has the async callback

  it('your test', async () => {
    

    function getValueFromElement(): Promise<string> {
      return new Promise((reject, resolve) => {
        cy.log("Obtaining the Value");

        // let myNumber: string; // Here I'm declaring my variable, => no need to declare here as we are returning value from promise
      
        cy.get(this.labelWithText).then(($element) => {
      
          let originalLabelText: string = $element.text();
          let splittedText: string[];
          splittedText = originalLabelText.split(": ");
      
          let myNumber = splittedText[1]; // Here I'm assigning the value 
          cy.log("Inside the THEN" + myNumber); //This logs the number correctly
          resolve(myNumber)

          // you can use reject in case of any exception
        });
      })
    }

    const myNumber = await getValueFromElement();

  })
TheMysterious
  • 219
  • 3
  • 8
-1

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.

tromgy
  • 4,937
  • 3
  • 17
  • 18