Understanding How Cypress Works

Let's have a closer look at how Cypress works.

How does Cypress work?

Although the Cypress test seems simple, it’s a tool where the apparent simplicity of the developer commands hides a fair amount of complexity behind the scenes. 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 many common JavaScript patterns either won’t work or aren’t recommended in Cypress. For example, using regular variable assignments is problematic.

cy commands

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

When the Cypress command is run as part of the test, it queues itself for a list of commands in the test and returns immediately. What we think of as the actual command, the get or click, 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 needs to be completely insular from other sections of our app. Normal JavaScript assignment or logic that is not mediated through Cypress does not recognize Cypress but, conversely, everything we do in a Cypress test needs to go through the cy command. Cypress has constructs to allow us to do things like variable assignment and logic inside Cypress, but if we 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.

Cypress commands are not exactly the same as JavaScript promises and we can’t directly mix the two. Specifically, using async/await in a Cypress test will not put our asynchronous code in the Cypress chain of commands. To use variables, make assertions, and do all the things we’d expect to do in a test, Cypress allows us to chain methods to the cy command and also allows us to use as to hold onto values for later use.

cy.get command

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

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

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

From Cypress’ perspective, making each command asynchronous gives it full control over how each test executes. When testing JavaScript, the fact that DOM elements are often changing, appearing, or disappearing makes the tests complicated to run. A 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 been fully completed. The first line (after login) of our test is cy.visit("/"). The second line, cy.get("#favorite-section").as("favorites"), only begins execution after the visit command has completed. Furthermore, if the cy.get("#favorite-section") command does not initially find any matching elements in the DOM, Cypress will automatically wait for the element to show up. Only once it shows up does Cypress move on to execute the as 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 that only uses get commands is still making implicit assertions about the structure of the page.

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

Navigation

Typically, the first thing we’ll want to do in a Cypress test is load a web page. We’ve already seen the cy.visit command that takes a URL argument and loads the resulting DOM. The visit command takes many optional arguments that allow us 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 we 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 our page would make one or more API calls to set up the page structure, such as:
cy.request(url).then((response) => {
  // Do something here
}
  • To verify that an HTTP request that we make would have the desired effect, as in a case where our 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, and a go command that allows us to interact with the browser web history, cy.go("back").

Finders

Once we’ve visited a site and set up DOM elements, we probably want to search them for specific elements. Cypress provides several 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, and 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]").

Finder Commands

Only a few of these finder commands 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, we will start with cy.get(). Alternatively, we can start with cy.contains("Text"), which searches for a DOM element containing the given text or matching a given regular expression.

Once we have a set of DOM elements via get or contains, we can traverse the DOM tree in several ways. We can continue to search with our 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 us to traverse the tree, like children, parents, and so on, but they are less common. We 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 find commands will wait up to the timeout period for matching elements to be found and will fail the test if no elements match.

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

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

Get hands-on with 1000+ tech skills courses.