How to wait for a successful response in Cypress tests

Viewed 5838

Background

I use 3 back-end servers to provide fault tolerance for one of my online SaaS application. All important API calls, such as getting user data, contact all 3 servers and use value of first successfully resolved response, if any.

export function getSuccessValueOrThrow$<T>(
        observables$: Observable<T>[],
        tryUntilMillies = 30000,
    ): Observable<T> {
        return race(
            ...observables$.map(observable$ => {
                return observable$.pipe(
                    timeout(tryUntilMillies),
                    catchError(err => {
                        return of(err).pipe(delay(5000), mergeMap(_err => throwError(_err)));
                    }),
                );
            })
        );
    }

getSuccessValueOrThrow$ get called as following:

const shuffledApiDomainList = ['server1-domain', 'server2-domain', 'server3-domain';
const sessionInfo = await RequestUtils.getSuccessValueOrThrow(
                        ...(shuffledApiDomainList.map(shuffledDomain => this.http.get<SessionDetails>(`${shuffledDomain}/file/converter/comm/session/info`))),
                    ).toPromise();

Note: if one request resolve faster than others, usually the case, race rxjs function will cancel the other two requests. On Chrome dev network tab it will look like bellow where first request sent out was cancelled due to being too slow.

enter image description here

Question:

I use /file/converter/comm/session/info (lets call it Endpoint 1) to get some data related to a user. This request dispatched to all 3 back-end servers. If one resolve, then remaining 2 request will be cancelled, i.e. they will return null.

On my Cypress E2E test I have

cy.route('GET', '/file/converter/comm/session/info').as('getSessionInfo');
cy.visit('https://www.ps2pdf.com/compress-mp4');
cy.wait('@getSessionInfo').its('status').should('eq', 200)

This sometimes fails if the since getSessionInfo alias was hooked on to a request that ultimately get cancelled by getSuccessValueOrThrow$ because it wasn't the request that succeeded.Bellow image shows how 1 out of 3 request aliased with getSessionInfo succeeded but the test failed since the first request failed.

enter image description here

In Cypress, how do I wait for a successful i.e. status = 200 request?

3 Answers

Approach 1

Use .should() callback and repeat the cy.wait call if status was not 200:

function waitFor200(routeAlias, retries = 2) {
  cy.wait(routeAlias).then(xhr => {
    if (xhr.status === 200) return // OK
    else if (retries > 0) waitFor200(routeAlias, retries - 1); // wait for the next response
    else throw "All requests returned non-200 response";
  })
}

// Usage example.
// Note that no assertions are chained here,
// the check has been performed inside this function already.
waitFor200('@getSessionInfo'); 

// Proceed with your test
cy.get('button').click(); // ...

Approach 2

Revise what it is that you want to test in the first place. Chances are - there is something on the page that tells the user about a successful operation. E.g. show/hide a spinner or a progress bar, or just that the page content is updated to show new data fetched from the backend.

So in this approach you would remove cy.wait() altogether, and focus on what the user sees on the page - do some assertions on the actual page content.

cy.wait() yields an object containing the HTTP request and response properties of the XHR. The error you're getting is because you're looking for property status in the XHR object, but it is a property of the Response Object. You first have to get to the Response Object:

cy.wait('@getSessionInfo').should(xhr => {
    expect(xhr.response).to.have.property('status', 200);
});

Edit: Since our backend uses graphql, all calls use the single /graphql endpoint. So I had to come up with a solution to differentiate each call.

I did that by using the onResponse() method of cy.route() and accumulating the data in Cypress environment object:

cy.route({
  method: 'GET',
  url: '/file/converter/comm/session/info',
  onResponse(xhr) {
    if (xhr.status === 200) {
      Cypress.env('sessionInfo200') = xhr;
    }
  }
})

You can then use it like this:

cy.wrap(Cypress.env()).should('have.property', 'sessionInfo200');

I wait like this:

const isOk = cy.wait("@getSessionInfo").then((xhr) => {
  return (xhr.status === 200);
});
Related