mXeIn
mXeIn

Reputation: 229

How to make Cypress wait for all requests to finish

I'm using cypress to test our web application and in certain pages there are different endpoint requests that are executed multiple times, e.g. GET /A, GET /B, GET /A.

What would be the best practice in Cypress to wait for all requests to finish and guarantee that page has been fully loaded?

I don't want to use a lot of cy.wait() commands to wait for all request to be processed. (there are many different sets of requests in each page).

Upvotes: 21

Views: 46450

Answers (8)

Ron Newcomb
Ron Newcomb

Reputation: 3302

In most SPAs the app will somehow let the user know it isn't ready for use yet, through the use of Loading Spinners, Progress Bars, Skeletons, etc.

In your Loading component(s) place a data-testid="Loading" attribute on such, and tell Cypress to wait until none of them exist. cy.get('[data-testid="Loading"]').should('not.exist');

Depending on how your SPA is structured, fetch requests returning may cause more fetch requests to occur, in a cascade. It is possible that the Loading indicators all disappear from the app for a tick before re-appearing for the cascaded fetches, which will defeat the above cypress line.

Although you could fix this in cypress by, say, waiting a few ticks even after the indicators disappear, it would be best to fix the app, perhaps by making the indicators themselves remain in the DOM a few extra ticks just in case. This also helps cure visual artifacts like the loading animation jumping and restarting.

Also depending on the app structure, the fetch requests may not be started immediately, but a tick or two after the navigation. For this a similar cypress line to wait until a loading indicator DOES exist, then immediately afterward, wait until no loading indicators exist.

EDIT TO ADD: the code that decides to show a Loading indicator can pass a parameter to it with the reason it is displaying. This can be placed in a data-reason attribute or somesuch to expose details about the loading state when multiple loadings are occurring.

Upvotes: 0

WrRaThY
WrRaThY

Reputation: 1978

based on a great answer by @ttqa - I made a function that I can use as a wrapper

export const cyWaitUntilResponseComes = (taskThatTriggersRequests: () => void) => {
  const dedicatedAlias = faker.random.numeric(16);

  cy.intercept('POST', ENV_VARS.SERVICE_URL).as(dedicatedAlias);

  taskThatTriggersRequests();

  cy.wait(`@${dedicatedAlias}`);
};

then you can use it as follows:

cyWaitUntilResponseComes(() => cy.get('#the-button').click())
cy.get('#response').should('be.visible')

I hope that it helps someone

Upvotes: 0

John Leonard
John Leonard

Reputation: 929

I'm sure this is not recommended practice but here's what I came up with. It effectively waits until there's no response for a certain amount of time:

function debouncedWait({ debounceTimeout = 3000, waitTimeout = 4000 } = {}) {
  cy.intercept('/api/*').as('ignoreMe');
    
  let done = false;
  const recursiveWait = () => {
    if (!done) {
      // set a timeout so if no response within debounceTimeout 
      // send a dummy request to satisfy the current wait
      const x = setTimeout(() => {
        done = true; // end recursion
    
        fetch('/api/blah');
      }, debounceTimeout);
    
      // wait for a response
      cy.wait('@ignoreMe', { timeout: waitTimeout }).then(() => {
        clearTimeout(x);  // cancel this wait's timeout
        recursiveWait();  // wait for the next response
      });
    }
  };
    
  recursiveWait();
}

Upvotes: 4

Rosen Mihaylov
Rosen Mihaylov

Reputation: 1427

According to Cypress FAQ there is no definite way. But I will share some solutions I use:

  1. Use the JQuery syntax supported by cypress

    $('document').ready(function() {
        //Code to run after it is ready
    });
    

The problem is that after the initial load - some action on the page can initiate a second load.

  1. Select an element like an image or select and wait for it to load. The problem with this method is that some other element might need more time.

  2. Decide on a mandatory time you will wait for the API requests (I personally use 4000 for my app) and place a cy.wait(mandatoryWaitTime) where you need your page to be loaded.

Upvotes: 2

Lola Ichingbola
Lola Ichingbola

Reputation: 4956

IMO the best practice is not to worry about the stream of requests.

Instead use appropriate timeouts on the actual page element queries that you need in your test.

The communications between app and server are likely to change over time as the app changes, it you try to handle 100% of the calls you create a lot of re-work.

Start asserting on elements straight after the cy.visit(). If the test fails because a request is still in-progress, increase the timeout option on the query.

This approach is simple and effective. You don't need a guarantee that the page is 100% loaded, just a guarantee that the element is in a state to test.

A simple .should() gives you that:

cy.get(some-selector)
  .should('have.text', 'text-loaded-by-request-call')

Upvotes: 24

Denis P
Denis P

Reputation: 776

Let me try to formulate a complete answer (repeating some great ideas from other replies), as the previous answers were partial and, most importantly, didn't describe the right way to use cy.wait with multiple requests.

There is no universal solution for 'waiting for every single request to complete', as described in Cypress docs.

Just for completeness, to wait for a page to load you just use cy.visit, which automatically waits for all embedded resources loading to complete before it resolves. But that's not what the OP's question was about. It was about waiting for all endpoint requests to complete a.k.a. XHR requests. Let's move on to that topic.

Waiting for all endpoint (XHR/Ajax) requests to complete

Again, citing the same Cypress docs section: There is no magical way to wait for all of your XHRs or Ajax requests. The docs have good grounding why so. Based on my investigation I suggest the following two approaches here:

  1. Slowing down tests but easy to implement. If you cannot put down the list of URL patterns to wait for, then use the cypress-network-idle plugin, as mentioned in earlier replies. This plugin relies on catching certain amount of time (a few seconds) without network activity, so it will definitely delay each of the tests it's used in, though. The overall delay for bigger test suites can make a big difference.

  2. Proper but needs manual work. If you can list the requests you require to be finished, then use cy.intercept and cy.wait. Here is the right way to do it (at least at the time of writing with Cypress 12.14), based on my research and testing. Using OP's example of 2 GET requests to A and one GET request to B:

    cy.intercept('GET', '/A').as('getA');
    cy.intercept('GET', '/B').as('getB');
    
    // ... Here goes the code that causes those requests to fire
    
    cy.wait(['@getA', '@getA', '@getB']); // waits for all the requests in the array to finish, with '@getA' being waited for twice
    

    Similarly, if your script updates 10 fields on a page, with each sending separate XHR request to the same URL, you can craft the array for cy.wait call dynamically:

    cy.intercept('POST', '/updateField').as('updateField');
    
    // ... Here goes the code that causes 10 updates to fire
    
    // Now wait for all the 10 requests to complete
    cy.wait(Array(10).fill('@updateField'));
    

    Let me highlight that cy.wait('@updateField') waits for exactly one request matching the '@updateField' to finish, as described in the docs and as my testing confirms. (It is easy to see if you add delays into replies using cy.intercept response's setDelay method - look for setDelay here). You have to pass the exact number of requests (as an array) to cy.wait to make sure they all have completed.

Accounting for network errors

There is another case that requires special handling in code - possible network errors.

Let's use our last example with 10 requests to the same URL. Some of those 10 requests do run into a network error or something similar from time to time - then browser automatically tries to re-send them. I'm seeing this in my XHR-intensive tests regularly. Then you'll have 11 requests (if there was one retry) or more, while your code will wait for 10 ones only.

My suggested workaround for this situation would be using cy.get to check that there were exactly 10 successful requests. If not, you can retry the failing test (the details on automatic retries of failing tests are here). IMO it is the most reasonable way, which is also reliable enough. Whereas writing code to catch requests happening because of network errors and making sure at least one of requests for each unique payload has completed successfully is way too complex IMO for typical cases, though may also be implemented for 100% reliability if that's necessary.

So, for our example - if we know the server returns status code of 200 in case of success, then our example together with verification code looks as follows. Note that we use a special form of the alias in cy.get ending with .all that results in getting full list of matches for that alias.

cy.intercept('POST', '/updateField').as('updateField');

// ... Here goes the code that causes 10 updates to fire

// Now wait for all the 10 requests to complete
cy.wait(Array(10).fill('@updateField'));
cy.get('@updateField.all')
    .should('have.length', 10)
    .each((match) => {
        expect(match)
            .has.nested.property('response.statusCode')
            .that.equals(200);
    });

Upvotes: 1

ttqa
ttqa

Reputation: 602

I faced the same issue with our large Angular application doing tens of requests as you navigate through it.

At first I tried what you are asking: to automatically wait for all requests to complete. I used https://github.com/bahmutov/cypress-network-idle as suggested by @Xiao Wang in this post. This worked and did the job, but I eventually realized I was over-optimizing my tests. Tests became slow. Test was waiting for all kinds of calls to finish, even those that weren't needed at that point in time to finish (like 3rd party analytics etc).

So I'd suggest not trying to wait for everything at a step, but instead finding the key API calls (you don't need to know the full path, even api/customers is enough) in your test step, use cy.intercept() and create an alias for it. Then use cy.wait() with your alias. The result is that you are waiting only when needed and only for the calls that really matter.

// At this point, there are lots of GET requests that need to finish in order to continue the test
// Intercept calls that contain a GET request with a request path containing /api/customer/
cy.intercept({ method: 'GET', url: '**/api/customer/**' }).as("customerData");

// Wait for all the GET requests with path containing /api/customer/ to complete
cy.wait("@customerData");

// Continue my test knowing all requested data is available..
cy.get(".continueMyTest").click()

Upvotes: 3

Alapan Das
Alapan Das

Reputation: 18650

You can use the cy.route() feature from cypress. Using this you can intercept all your Get requests and wait till all of them are executed:

cy.server()
cy.route('GET', '**/users').as('getusers')
cy.visit('/')
cy.wait('@getusers')

Upvotes: 0

Related Questions