How to access Redux state in Cypress

Set up our application#

I borrowed the code from react-redux.js.org which showcases react + redux in action using a todo list application.

Set up and scaffold Cypress#

Cypress is a fantastic testing framework. It is easy to set up and can be picked up pretty quickly.

Setting up Cypress is pretty straight forward - just run:

$ npm install cypress
or
$ yarn add cypress

We will also install the recommeded dependency

$ npm install -D start-server-and-test

start-server-and-test is a cool tool that basically

Starts server, waits for URL, then runs test command; when the tests end, shuts down server

as explained on their github repo.

How to access the store in Cypress#

We don’t have access to store() object ordinarily, but cypress can access window() object. We use this to attach the store to window in our index.js.

import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './redux/store'

import TodoApp from './TodoApp'

import * as serviceWorker from './serviceWorker'

ReactDOM.render(
  <Provider store={store}>
    <TodoApp />
  </Provider>,
  document.getElementById('root')
)

if (window.Cypress) {
  window.store = store
}

Now we have access to store() and more importantly the state inside it.

Writing the test#

Now that we have the store available, we can access redux state at

cy.window().its('store').invoke('getState')

The final version of the test will look like this, with redux code highlighted.

/// <reference types="Cypress" />

describe('Tests functionality and redux state', () => {
  it('Successfully uses todo application', () => {
    cy.visit('/')

      // assertions on view, tabs and redux
      .get('[data-cy=Header]')
      .should('have.text', 'Todo List')
      .get('.add-todo')
      .should('have.text', 'Add Todo')
      .get('.todo-list')
      .should('have.text', 'No todos, yay!')
      .get('.visibility-filters')
      .should('include.text', 'all')
      .get('.visibility-filters')
      .should('include.text', 'completed')
      .get('.visibility-filters')
      .should('include.text', 'incomplete')

      .window()
      .its('store')
      .invoke('getState')
      .then((state) => {
        expect(state.visibilityFilter).to.be.a('string').and.equal('all')
        expect(state.todos.allIds).to.be.a('array').and.to.be.empty
        expect(state.todos.byIds).to.be.a('object').and.to.be.empty
      })

      // add a todo, add another todo , mark the first one as complete
      .get('input')
      .type('My First Todo')
      .get('.add-todo')
      .click()
      .get('input')
      .type('My Second Todo')
      .get('.add-todo')
      .click()
      .get('.todo-list')
      .eq(0)
      .click()

      // assertions on view, tabs and redux
      .get('.filter')
      .eq(0)
      .should('include.text', 'all')
      .click()
      .get('.todo-item')
      .should('include.text', '👋 My First Todo')

      .get('.filter')
      .eq(2)
      .should('include.text', 'incomplete')
      .click()
      .get('.todo-item')
      .should('include.text', '👋 My First Todo')

      .get('.filter')
      .eq(1)
      .should('include.text', 'completed')
      .click()
      .get('.todo-item')
      .should('have.text', '👌 My Second Todo')
      .eq(0)
      .children()
      .should('have.class', 'todo-item__text--completed')
      .get('.filter')
      .eq(0)
      .should('include.text', 'all')
      .click()

      .window()
      .its('store')
      .invoke('getState')
      .then((state) => {
        expect(state.visibilityFilter).to.be.a('string').and.equal('all')
        expect(state.todos.allIds).to.be.a('array').and.to.have.lengthOf(2)
        expect(state.todos.byIds)
          .to.be.a('object')
          .and.to.deep.equal({
            1: { content: 'My First Todo', completed: false },
            2: { content: 'My Second Todo', completed: true },
          })
      })
  })
})

Watch it Run!#

cypress successful test running

Want more ?#

We can also dispatch redux actions like this.

cy.window()
  .its('store')
  .invoke('dispatch', {
    type: 'ADD_TODO',
    payload: { content: 'Dispatched TODO', completed: true },
  })