How we seamlessly connected Redux with Cypress

Maurizio Carboni
Revel Systems Engineering Blog
6 min readDec 17, 2020

In Revel, we believe our customers should have the best experience possible, and the perfect experience is for sure bug-free. To maintain this experience, we check every change with various automated test frameworks, and one of those is Cypress.

We love Cypress here at Revel, and we want our tests to be easy to write, understand, and more than everything we want them to be stable.

The Problem

A recurring problem we found with Cypress, is how to know when the DOM is ready to be checked. Sometimes, waiting for an XHR call to happen is not enough, and waiting for 1 second after an XHR completes looks like and hack, and it’s a hack.

Because of that, we did think “what happens before the change is propagated to the DOM?”, and the response was simple, we dispatch a Redux action. Our frontend architecture uses a centralized Redux store, this means that every major change in the data displayed by our app passes through the Redux store.

Now that we have a clear idea of how to solve our initial problem, we just need to find a way to connect the Redux store of our App with the Cypress test.

Accessing the Redux Store

The first problem is that the store itself is not exposed, we don’t have a window.store to connect to, but we have something more interesting, a tool that we are already using: Redux inspector. This tool is used almost universally, it’s used even in Medium itself!

Redux inspector open on the main page of Medium
Redux inspector open on the main page of Medium

If the Redux Inspector can see everything that happens in our Redux Store, it means that we can bridge it and see it too.

How does the Redux Inspector connect?

The first step is to see how the Redux Inspector and our Redux Store communicate

In the code above we can see that our store adds to the middleware list window.__REDUX_DEVTOOLS_EXTENSION__() if it exists.
This means that __REDUX_DEVTOOLS_EXTENSION__ is just a function that returns a Store Enhancer (an example of an Enhancer is Redux-thunk), and the purpose of this Enhancer is to debug the connected store.

Let’s have a look at the type definition of Store Enhancer so we know how to create ours:

In these typings, we can see that a Store Enhancer is a function that accepts the argument next that returns another function that accepts the arguments reducer and preloadedState. And this returned function returns the same type of next. In short, it can simply call next :

Our first Store Enhancer

Now that we know how this Store Enhancer is supposed to look like, we can go one step further and write a simple Enhancer that will log on to console every action. To do this we need to write an actual next function, as we see, the next function receives a reducer, and in this case, it is the root reducer. This reducer receives every action (and the full state before these actions). And this is exactly what we were looking for, a way to see the actions.

With this, we can see every action in the console that has been dispatched in our store. We just need to connect it, to do so we need to override the __REDUX_DEVTOOLS_EXTENSION__ property of the window. To access the window in Cypress we can use support/index.js:

Now if we open the Developer tools, we will see the actions in the console. For the next step, we need a command to wait for them.

It’s important to remember that the Cypress runner that will run our command, and the Browser that is running our frontend are two separate processes, for this reason, we need to create a communication channel.

The communication channel

We did choose to use a Pub-Sub pattern, where the newly added command can subscribe to redux actions and our fake Redux Inspector can publish action to the commands that did subscribe.

The Pub-Sub will be fully implemented in the Browser window, we are going to expose a single method in the window to subscribe to an action type. Because we want to subscribe to a single action that will be fired only once, we are going to return a Promise that will resolve once the action is fired.

The Pub-Sub is ready, now we need to create the custom Cypress action that will Subscribe to the action, and connect the Pub-Sub to our fake Redux Inspector that will Publish the action.

Connecting the pieces

First, we are going to connect the Redux Inspector:

And finally, we can create the command. We want our command to be easy to use, for this reason, it will accept both a single action to wait or N actions to wait, it will also have an optional timeout.
With this in mind we can create the signature of the command:

In Cypress, we can create commands using the Cypress.Commands.add method. This method accepts the name of the command as the first argument, and the function to be called when the command is executed as the second argument.

Unfortunately, Cypress doesn’t log the name of custom commands in the commands list. To do that we will need to log it ourselves using Cypress.log.

To make the command work, we need to access the browser window, where the Pub-Sub function we created is available. We can use cy.window, which will return a Promise with an object referencing the window, from this object we can access subscribeToAction.

All that is left to do is to make the command wait until every actionType is fired, to wait for all of them we need first to transform every actionType from a string into a Promise using subscribeToAction, and then we need to wait for all these Promises using Cypress.Promise.all.

The result

Now that the code is finally complete we can see it working:

After adopting this new command, our tests did become more stable. Because now we are waiting for the right action to be dispatched instead of waiting for an XHR call + N milliseconds, we have fewer cases where the DOM check fails because was called too early, or the button was pressed at the wrong moment.

Further steps

Something we did notice after completing this implementation, is that Cypress can be a little bit late, and will start waiting for the Redux Action after it’s already dispatched. Obviously, if you wait after it’s already been dispatched the test will fail because it will not be dispatched a second time.

To solve this problem we needed to be flexible, and we added a configurable tolerance to waitForReduxAction, with this improvement, the command was able not only to wait for future actions but also stop waiting if the action was dispatched N milliseconds before the command was executed.

Another improvement we added to the script, is also the ability to be an invisible bridge between Redux Inspector and our store. The script we created in this article has the drawback of replacing Redux Inspector and with that breaking it. We wanted Redux Inspector to continue to work in Cypress, and to do that we needed to improve the script.

You can find the full code with all these improvements here:

--

--