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!#
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 },
})