Waiting for an AJAX request
Do you remember the list of problems that could make the signup flow break? 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. They were the following ones:
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 test still does not help us with these errors. When it fails, we need to spend some time debugging what's happening in the web app (while it's under test) to discover that the issue was related to a wrong AJAX request... Do not despair, Cypress is here to improve the E2E testing experience!
Server contract
When we test the front-end of our web app, we need to consider it a closed block (blackbox testing) and we need to check how it interacts with the external world. We need to test the contracts between the front-end and all the involved entities. What are the contracts to be respected by the front-end app?
functional contract with the user
presentational contract with the user
contract with the server
The first one is the subject of the all the E2E tests, the second one is the subject for the visual regression tests, but we need to concentrate on the last one. A lot of times the front-end stops working because of a misaligned communication with the back-end.
AJAX request waiting
If we know that an AJAX request happens systematically (it is going to happen every time) we need to consider it in our Cypress test. The signup flow is a good example. We are going to see what are the APIs that allow us to do that.
First of all, we need to set up AJAX call interception:
it("The happy path should work", () => {
+ cy.server();
+ cy.route("POST", "**/api/users");
cy.visit(paths.register);
// the rest of the test code
});
cy.server()
tells Cypress to act as a proxy, intercepting and allowing to wait for AJAX requestscy.route
tells Cypress to intercept some AJAX requests. We can use a lot of options to match precisely which AJAX request we want to intercept.cy.route("POST", "**/api/users")
tells Cypress to intercept everyPOST
request to every URL that ends with**/api/users
(the signup form makes a request to thehttp://localhost:3100/api/users
, Cypress uses minimatch to 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.server();
cy.route("POST", "**/api/users")
+ .as("signup-request");
cy.visit(paths.register);
// the rest of the test code
});
Third: we must "wait" the AJAX request triggered by the front-end app
it("The happy path should work", () => {
cy.server();
cy.route("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" main for an AJAX request? Here we can see the advantages of automatic Cypress waitings mixed with AJAX requests management! By asking Cypress to cy.wait("@signup-request");
, it's going to wait up to 5 seconds for the front-end to start the request and up to 30 seconds for the back-end to fulfill the request (both of the timeouts are customizable).
Last change: do you remember why did we add the custom timeout to the cy.findByText(noArticles, { timeout: 10000 })
call? We added it while speaking about the stability of the test because we faced 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 it 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
it("The happy path should work", () => {
+ cy.server();
+ cy.route("POST", "**/api/users")
+ .as("signup-request");
cy.visit(paths.register);
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");
cy.get("form")
.within(() => cy.findByText(strings.signUp))
.click();
+ cy.wait("@signup-request");
- cy.findByText(noArticles, { timeout: 10000 }).should("be.visible");
+ cy.findByText(noArticles).should("be.visible");
});
and that's the test with some comments
File: cypress/integration/examples/signup/signup-6-ajax-request-waiting.e2e.spec.js
/// <reference types="Cypress" />
import { paths } from "../../../../realworld/frontend/src/components/App";
import { noArticles } from "../../../../realworld/frontend/src/components/ArticleList";
import { strings } from "../../../../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 way more robust because
we do not wait only for something that "reflects" the result of the AJAX request (the "No articles are here" string) but for the AJAX request itself
we do not wait up to a "random" (the previous
{timeout: 10000}
) amount of time but up to a longer time. 10 seconds could not be enough (if you have ever consumed some AWS Lambdas you know what I'm talking about) because of the back-end wakeup/caching times
Author: Stefano Magni