javascripttestingcypress

Get element(s) text the synchronous way with Cypress


I have a multiselect dropdown on a page. This dropdown can have 0 or more selected values. Here is the function that reads those values and returns an array:

async readSelected(){
  const result = [];
  await cy.xpath("//*[@blabla='bla']//li").each(e => result.push(e.text()));
  return result;
}

It works for cases when 1 or more elements is found, but for 0 it throws an exception, which is not what I want. Instead, the result must be just an empty array.

How to make it work?

P.S. As the title may suggest, handling such cases with then(), should() and other Cypress chained methods when values must be processed from within callback functions with no option to explicitly await it and get the actual values instead of promise of promise-like object is not the solution I'm looking for


Solution

  • The command cy.xpath() and any other cy() queries are not returning promises, so you can't use await cy.xpath() unfortunately.

    There is a work-around for cy.xpath() deprecation here xpath plugin alternative in cypress which has some synchronous code that you can apply in your class method.

    As a close example, I used a function and asserted it's results.

    HTML fragment tested

    <ul blabla='bla'>
      <li>one</li>
      <li>two</li>
    </ul>
    

    Test code

    async function readSelected(){
      const doc = cy.state('document')
      const nodes = doc.evaluate("//*[@blabla='bla']//li", doc)
      let results = [], node;
      while((node = nodes.iterateNext()) !== null) {
        results.push(Cypress.$(node).text())
      }
      return results;
    }
    
    const results = await readSelected()
    expect(results).to.deep.eq(['one', 'two'])
    

    enter image description here

    The caveat here is that the page must have finished loading before you call readSelect(), because there is no retry within the function. By coding the test this way, you are kind of negating the advantages of Cypress commands.


    Using a Promise

    Since you're not doing anything with the synchronous result inside that method, I assume another method is using const result = await this.readSelect(), in which case a simple Promise wrapper works

    function readSelected(){
      return new Promise((resolve) => {
        const results = [];
        cy.xpath("//*[@blabla='bla']//li")
          .each(e => results.push(e.text()))
          .then(() => resolve(results))
      })
    }
    
    const results = await readSelected()
    expect(results).to.deep.eq(['one', 'two'])
    

    enter image description here

    You can use other variations such as mapping the elements to their text

    function readSelected(){
      return new Promise((resolve) => {
        cy.xpath("//*[@blabla='bla']//li")
          .then($li => resolve([...$li].map(li => li.innerText)))
      })
    }