Search code examples
typescriptcypress

How to make synchronous API calls and return chainable in a function?


Here is what I am trying to do in the code below.

  1. Make three API calls inside the function areAllTypesFound. Use the results of those API calls to decide whether I got all the necessary data.

  2. If I get all the necessary data from the calls, then return a boolean wrapped inside a chainable.

  3. In the function doStuff, I make a call to areAllTypesFound. only after that call is completed, I will do something with the boolean result. After that, I will execute some more code.

The problem is that areAllTypesFound always returns false even when all the data is found. I am quite sure this has to do something with the async nature of Cypress. Can someone please show me how to execute everything synchronously in the below code?

class MyClass {

    static areAllTypesFound(params) : Cypress.Chainable<boolean> {
        let idsType1 : number[];
        let idsType2 :number[];
        let idsType3 :number[];

        let [type1Present, type2Present, type3Present] = [false, false, false];

        // Here response = Cypress.Response<ItemType1 2 or 3>
        ApiCaller.getItemType1(param1).then((response) => {
            idsType1 = response.body.items.map((item) => item.id);
            type1Present = idsType1.length > 0;
        }
    
        ApiCaller.getItemType2(param2).then((response) => {
            idsType2 = response.body.items.map((item) => item.id);
            type2Present = idsType2.length > 0;
        }

        ApiCaller.getItemType3(param3).then((response) => {
            idsType3 = response.body.items.map((item) => item.id);
            type3Present = idsType3.length > 0;
        }

        const allTypesPresent = type1Present && type2Present && type3Present;
        // optional - maybe print all the "idsType" arrays.

        return cy.wrap(allTypesPresent);
    } 

    static doStuff() : void {
        this.areAllTypesFound(params).then((areFoundBoolean) => {
            if(areFoundBoolean) {
                // do something
            }else {
                // do something else
            }
        });

        // do more stuff only AFTER the above code is completed !
    }

}

Sample code for what itemType function looks like -

  public static getItemType1<T = ItemType1>(param1: string): Cypress.Chainable<Cypress.Response<T>> {
    return cy.request({
      method: "GET",
      url: "/api/items/itemType1",
      qs: {
        itemType: param1,
      },
      headers: {
        accept: "application/json",
      },
    });
  }

Solution

  • You could nest the API calls. There may be a more elegant way, but nesting the calls and returning at each level ensures you have completed each call before the final return is made.

    const allResults = []  // gather the results in an array
    
    return ApiCaller.getItemType1(param1).then((response) => {
      idsType1 = response.body.items.map((item) => item.id);
      allResults.push(idsType1)
        
      return ApiCaller.getItemType2(param2).then((response) => {
        idsType2 = response.body.items.map((item) => item.id);
        allResults.push(idsType2)
    
        return ApiCaller.getItemType3(param3).then((response) => {
          idsType2 = response.body.items.map((item) => item.id);
          allResults.push(idsType2)
     
          return allResults
        }
      }
    }
    

    You can put anything in the allResults, as long as you get 3 things in the array.


    An Example

    I wanted to explore the code in question in a reproducible form.

    There's a useful website requestly.com

    Build, Test & Debug your web apps faster by Intercepting & Modifying Network Traffic, Session Replays, Mock Server & API Client.

    This site can add delays (or other modifications) to your cy.request() calls, which allows you to stress-test your test code to make sure it's not flaky.

    The way you use it is to embed the delay time in milliseconds and the actual API URL like this:

    https://app.requestly.io/delay/<delay_in_millisecods>/<URL_to_be_delayed
    

    The API I want to test is https://jsonplaceholder.typicode.com/todos/, requesting records with id's 1, 2, and 3.

    The goal is to ensure all three results are returned from the function (or in the OP it's a class method, same idea).

    function apiCalls() {
      const myApiBase = 'https://jsonplaceholder.typicode.com/todos/'
      const urlWithDelay = 'https://app.requestly.io/delay/1000/' + myApiBase
    
      return cy.request(urlWithDelay + '1')  // record id 1
        .then(response1 => {
    
          return cy.request(urlWithDelay + '2')  // record id 2
          .then(response2 => {
    
            return cy.request(urlWithDelay + '3')  // record id 3
            .then(response3 => {
    
              return [response1.body.title, response2.body.title, response3.body.title]
            })
          })
        })
    }
    
    apiCalls().then(console.log)
      .should(results => {
        expect(results[0]).to.eq('delectus aut autem')
        expect(results[1]).to.eq('quis ut nam facilis et officia qui')
        expect(results[2]).to.eq('fugiat veniam minus')
      })
    

    enter image description here


    Sequential (rather than nested) calls

    It turns out, after testing, that you don't need nested cy.request() calls, you can force the function to wait for all requests by using cy.then() (which was my original off-the-cuff answer BTW).

    function apiCalls() {
      let api1
      let api2
      let api3
    
      const myApiBase = 'https://jsonplaceholder.typicode.com/todos/'
      const urlWithDelay = 'https://app.requestly.io/delay/1000/' + myApiBase
    
      cy.request(urlWithDelay + '1')
        .then(response => { api1 = response.body.title })
      
      cy.request(urlWithDelay + '2')
        .then(response => { api2 = response.body.title })
    
      cy.request(urlWithDelay + '3')
        .then(response => { api3 = response.body.title })
    
      return cy.then(() => [api1, api2, api3])
    }
    
    apiCalls()
      .should(results => {
        expect(results[0]).to.eq('delectus aut autem')
        expect(results[1]).to.eq('quis ut nam facilis et officia qui')
        expect(results[2]).to.eq('fugiat veniam minus')
      })
    

    So cy.then() behaves like async/await in plain Javascript.