Understanding How Cypress Works

Let's understand how Cypress works in this lesson.

We'll cover the following

Although the Cypress test seems simple, Cypress is a tool where the apparent simplicity of the developer commands hides a fair amount of complexity behind the scenes. In order to effectively write tests in Cypress, it is important to take a step back to understand how Cypress works to avoid a lot of confusion later on. Cypress’ asynchronous structure means that a lot of common JavaScript patterns either won’t work or aren’t recommended in Cypress. For example, using regular variable assignment is going to cause you trouble.

The most important thing to understand about Cypress is that although Cypress commands appear to be regular JavaScript function calls, in fact, all Cypress commands are asynchronous. In effect, each time you use the cy command, you are creating something like a JavaScript promise, and subsequent cy commands are in the then clause of the preceding command, meaning that each successive cy command only executes after the previous one has completed.

When the Cypress command is actually run as part of the test, it queues itself a list of commands in the test and returns immediately. What we think of as the actual command—the get or click or whatever—only executes as part of this queue of promises once the entire test has loaded.

The most important implication of this behavior is that Cypress is a world unto itself. Normal JavaScript assignment or logic that is not mediated through Cypress does not see Cypress stuff at all, and conversely, everything you do in a Cypress test needs to go through the cy command. Cypress has constructs to allow you to do things like variable assignment and logic inside Cypress-land, but if you write normal variable assignments with let or const, those assignments will happen as the test loads. They will not be able to see Cypress data, and Cypress commands won’t be able to see those variables.

I do need to point out here that Cypress commands are not exactly the same as JavaScript promises, and you can’t directly mix the two. Specifically, using async/await in a Cypress test will not put your async code in the Cypress chain of commands.

In order to use variables, make assertions, and do all the things you’d expect to do in a test, Cypress allows you to chain methods to the cy command, and also allows you to use as to hold on to values for later use.

So, in our test, the second line

cy.get(".day-body").first().as("day")

allows us to hold on to the value returned by first() using the alias day, such that subsequent Cypress commands can use cy.get("@day") to access the same DOM element. Later lines of code chain the method should into the cy command so that the assertions take place inside the Cypress asynchronous commands.

Although we don’t use it in this test, you can chain the method then to any Cypress command to execute arbitrary code inside Cypress, like this:

cy.get(".day-body").first().then(element => {
	// do whatever we want in here...
}

From Cypress’s perspective, making each command asynchronous gives Cypress full control over how each test executes. When testing JavaScript, the fact that DOM elements are often changing, appearing, or disappearing makes the tests very complicated to run, and an extremely common problem is that the test and the timing of the DOM changes don’t quite line up, leaving tests that intermittently fail for no reason.

Cypress attempts to bypass those flaky timing errors by guaranteeing that each command is only executed after the previous command has fully completed. The first line of our test is cy.visit("/"). The second line, cy.get(".day-body").first().as("day") only begins execution after the visit command has completed. Furthermore, if the cy.get(".day-body) command does not initially find any matching elements in the DOM, Cypress will automatically wait—by default, up to four seconds—for the element to show up. Only once it shows up does Cypress move on to execute the first command. A side effect of this behavior is that many Cypress commands, even those that are not explicitly making assertions, are making existence checks, and a Cypress test can use just get commands to make some assertions about the structure of the page.

Before we write more Cypress tests, let’s take a break to step through some of the most common commands that you can send to the cy object.

Navigation

Typically, the first thing you’ll want to do in a Cypress test is load a web page. We’ve already seen the cy.visit command, which takes a URL argument and loads the resulting DOM. The visit command takes a lot of optional arguments that allow you to specify the HTTP method or pass data, as in:

cy.visit("/tickets",
	{method: 'POST',
   body: {concert_id: '3', row: '2', seat: '1'}
  })

The visit command places the DOM into the Cypress system. If, for some reason, you want to yield from a visit command like cy.visit.then((window) => {}), the window object is yielded.

Cypress also provides the request command, which is used to help test APIs. The request command takes a similar set of arguments but yields a response object. There are a couple of common ways to use a request:

  • To seed data, especially in cases where your page would make one or more API calls to set up the page structure, as in:
cy.request(url).then((response) => {
	// Do something here
}
  • To verify that an HTTP request that you make would have the desired effect, as in a case where your page might make a call to change something on the server, like this:
cy.request(url).should((response) => {
	expect(response).to // something
}

Cypress also has commands for interacting with cookies and local storage. It also has a go command that allows you to interact with the browser web history, cy.go("back").

Finders

Once you’ve visited a site and set up DOM elements, you probably want to search them for specific elements. Cypress provides a lot of find and search methods that are heavily influenced by jQuery. Which is to say that the methods typically take a selector and yield a Cypress object that contains a list of DOM elements that match the selector. The selector syntax is the same as jQuery’s: . indicates a DOM class, # is a DOM ID, an [attribute=value] matches a DOM attribute. If you are concerned about selectors changing in your code due to design changes, Cypress recommends fixing elements by setting a data-cy attribute on them, and then querying with something like cy.get("[data-cy=myelementidentifier]").

Only a few of these finder commands actually are callable directly on the cy object, like cy.get(".thing"). Most of them chain off the result of the get, as in cy.get(".thing").first(). Most of the time, you will start with cy.get(). Alternately, you can start with cy.contains("Text"), which searches for a DOM element containing the given text or matching a given regular expression.

Once you have a set of DOM elements via get or contains, you can traverse the DOM tree in several ways. You can continue to search with your DOM elements with find, which filters on another selector, cy.get(".thing").find(".other"), and yields elements with DOM class other that are inside elements with DOM class thing. A series of other commands allow you to traverse the tree, like children, parents, and so on, but they are less common. You can get a specific element from a list with first, last, or eq(index). The index in eq either starts with 0 to get the first element and goes up to move forward or with -1 to get the last element and goes down to move backward.

If the result of these find commands is empty, all of these find commands will wait up to the timeout period for matching elements to be found and will fail the test if no matching element is found at the end of the timeout period.

Cypress works asynchronously, so you can’t return the results of a finder method to a variable; you typically continue to chain method calls that send actions or assertions.

That said, if you want to hold on to a DOM element or elements that you’ve located, you can use the as command to store those elements as an alias, as we’ve seen with cy.get(".day-body").first().as("day"). You can that access that alias later in the test, with cy.get("@day"). Aliases are not limited to DOM elements; you can use them to store other data, and aliases that you define in a beforeEach method are accessible in the tests that use that method.

Get hands-on with 1400+ tech skills courses.