Waiting for an Ajax Request
Explore techniques to handle AJAX requests in front-end integration tests using Cypress. Learn how to intercept, alias, and wait for these requests to enhance test accuracy and reduce flaky results. This lesson shows how to synchronize tests with backend responses, making your E2E tests more robust and easier to maintain.
We'll cover the following...
AJAX request
Do you remember the list of problems that would break the signup flow? We fixed the DOM-related ones by retrieving the elements based on the contents instead of the order, but there were a lot of problems related to the AJAX request itself:
-
The AJAX call does not start.
-
The AJAX call has the wrong request payload.
-
The API does not work and it does not respond.
-
The API returns the wrong payload.
-
The user already exists.
The tests we’ve covered so far still do not solve these errors. When they fail, we need to spend some time debugging the web app during testing to discover that the issue is related to the incorrect AJAX request. Don’t worry, Cypress will improve the E2E testing experience!
Server contract
When we test the front-end of our web app, we need to consider it as a closed block (i.e. blackbox testing) and check its interactions with the external world. We need to test the contracts between the front-end and all other involved entities. What contracts should be recognized by the front-end app?
-
Functional contract with the user: The subject of all E2E tests.
-
Presentational contract with the user: The subject for all visual regression tests (which will be discussed later).
-
Contract with the server: The subject of front-end communication with the back-end.
We need to concentrate on the last one because the front-end often stops working due to a misaligned communication with the back-end.
AJAX request waiting
Because we know that an AJAX request happens systematically, we know that an AJAX request will occur every time we run the app. Thus, we need to consider the AJAX request in our Cypress test. The signup flow is a good example. Let’s see what APIs allow us to do consider the AJAX request.
First, we need to set up AJAX call interception:
it("The happy path should work", () => {
+ cy.intercept("POST", "**/api/users");
cy.visit(paths.register);
// the rest of the test code
});
cy.intercepttells Cypress to intercept some requests. We can use a lot of options to match precisely which AJAX request we want to intercept.cy.intercept("POST", "**/api/users")tells Cypress to intercept everyPOSTrequest to every URL that ends with**/api/usersas the signup form makes a request to thehttp://localhost:3100/api/usersto take advantage of * and ** glob support.
Second, we need to set a Cypress alias to reference it later on.
it("The happy path should work", () => {
cy.intercept("POST", "**/api/users")
+ .as("signup-request");
cy.visit(paths.register);
// the rest of the test code
});
Third, we must “wait” for the AJAX request triggered by the front-end app.
it("The happy path should work", () => {
cy.intercept("POST", "**/api/users")
.as("signup-request");
cy.visit(paths.register);
// form filling code
cy.get("form")
.within(() => cy.findByText(strings.signUp).click());
+ cy.wait("@signup-request");
cy.findByText(noArticles, { timeout: 10000 }).should("be.visible");
});
What does “waiting” mean when applied to an AJAX request? The test will then wait up to 5 seconds for the front-end to begin the AJAX request and up to 30 seconds for the back-end to fulfill the request (both of the timeouts are customizable). Here, we can see the advantages of automatic Cypress waitings mixed with AJAX requests management! Essentially, AJAX will ask Cypress to wait viacy.wait("@signup-request");
Do you remember why we added the custom timeout to the cy.findByText(noArticles, { timeout: 10000 }) call? We initially added the timeout due to the problem that cy.contain (later replaced by cy.findByText) sometimes failed because the AJAX request took too long (and the default cy.contain/cy.findByText timeout is 4000 milliseconds). Now we do not need this timeout anymore because the cy.wait knows that an AJAX request could take a really long time!
- cy.findByText(noArticles, { timeout: 10000 }).should("be.visible");
+ cy.findByText(noArticles).should("be.visible");
That’s all the changes we applied to the test
and that’s the test with some comments given below.
Note: You can see the Cypress UI better by opening the link next to Your app can be found at:
import { paths } from "/educative-cypress-course/realworld/frontend/src/components/App";
import { noArticles } from "/educative-cypress-course/realworld/frontend/src/components/ArticleList";
import { strings } from "/educative-cypress-course/realworld/frontend/src/components/Register";
context("Signup flow", () => {
it("The happy path should work", () => {
// set up AJAX call interception
cy.server();
cy.route("POST", "**/api/users").as("signup-request");
cy.visit(paths.register);
// form filling
const random = Math.floor(Math.random() * 100000);
cy.findByPlaceholderText(strings.username).type(`Tester${random}`);
cy.findByPlaceholderText(strings.email).type(`user+${random}@realworld.io`);
cy.findByPlaceholderText(strings.password).type("mysupersecretpassword");
// form submit...
cy.get("form")
.within(() => cy.findByText(strings.signUp).click());
// ... and AJAX call waiting
cy.wait("@signup-request");
// end of the flow
cy.findByText(noArticles).should("be.visible");
});
});
The test is now much more robust because:
-
we do not only wait for something that “reflects” the result of the AJAX request (the “No articles are here” string), but also for the AJAX request itself.
-
we do not need to wait for a “random timeout” (the previous
{timeout: 10000}), but the appropriate amount of time needed to complete the AJAX request.