Skip to content

Commit

Permalink
Testing redux store (#211)
Browse files Browse the repository at this point in the history
* copy app

* working app

* a lot of tests

* snapshot testing

* update readme

* test delayed actions

* use cypress-pipe

* use root level cypress-plugin-snapshots

* test redux example on circle and travis

* update readme

* add circle job to workflow

* use cypress base 10 image

* use node 10 LTS on Travis

* prev patch of react-scripts with har-validator
  • Loading branch information
bahmutov committed Nov 13, 2018
1 parent 78048de commit bd40fd3
Show file tree
Hide file tree
Showing 47 changed files with 24,644 additions and 8,829 deletions.
8 changes: 6 additions & 2 deletions .travis.yml
@@ -1,7 +1,7 @@
language: node_js
node_js:
# 10.3 includes npm@6 which has good "npm ci" command
- 10.3
# 10.13 includes npm@6 which has good "npm ci" command, and is LTS
- 10.13

cache:
directories:
Expand Down Expand Up @@ -43,6 +43,10 @@ jobs:
env:
- DIR=blogs__e2e-snapshots
<<: *defaults
- stage: test
env:
- DIR=blogs__testing-redux-store
<<: *defaults
- stage: test
env:
- DIR=blogs__vue-vuex-rest
Expand Down
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -19,6 +19,7 @@ Recipe | Category | Description
[E2E API Testing](#e2e-api-testing) | Blogs | Run your API Tests with a GUI
[E2E Snapshots](#e2e-snapshots) | Blogs | End-to-End Snapshot Testing
[Codepen.io Testing](#codepen-testing) | Blogs | Test a HyperApp Codepen demo
[Testing Redux Store](#testing-redux-store) | Blogs | Test an application that uses Redux data store
[Vue + Vuex + REST Testing](#vue--vuex--rest-testing) | Blogs | Test an application that uses central data store
[Stubbing Functions](#stubbing-functions) | Stubbing, Spying | Use `cy.stub()` to test function calls
[Stubbing `window.fetch`](#stubbing-windowfetch) | Stubbing, Spying | Use `cy.stub()` to control fetch requests
Expand Down Expand Up @@ -208,6 +209,13 @@ Get around the lack of a `.hover()` command.
- Use [`cy.request()`](https://on.cypress.io/api/request) to load a document into test iframe.
- Test [HyperApp.js](https://hyperapp.js.org/) application through the DOM and through actions.

### [Testing Redux Store](./examples/blogs__testing-redux-store)

- control application via DOM and check that Redux store has been properly updated
- drive application by dispatching Redux actions
- use Redux actions directly from tests
- load initial Redux state from a fixture file

### [Vue + Vuex + REST Testing](./examples/blogs__vue-vuex-rest)

- [Blog post](https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/)
Expand Down
9 changes: 7 additions & 2 deletions circle.yml
Expand Up @@ -5,7 +5,7 @@ defaults: &defaults
parallelism: 1
working_directory: ~/app
docker:
- image: cypress/browsers:chrome65-ff57
- image: cypress/base:10
steps:
- attach_workspace:
at: ~/
Expand All @@ -26,7 +26,7 @@ jobs:
build:
working_directory: ~/app
docker:
- image: cypress/browsers:chrome65-ff57
- image: cypress/base:10
steps:
- checkout
- restore_cache:
Expand Down Expand Up @@ -59,6 +59,8 @@ jobs:
<<: *defaults
blogs__e2e-snapshots:
<<: *defaults
blogs__testing-redux-store:
<<: *defaults
blogs__vue-vuex-rest:
<<: *defaults
extending-cypress__chai-assertions:
Expand Down Expand Up @@ -121,6 +123,9 @@ workflows:
- blogs__e2e-snapshots:
requires:
- build
- blogs__testing-redux-store:
requires:
- build
- blogs__vue-vuex-rest:
requires:
- build
Expand Down
1 change: 1 addition & 0 deletions examples/blogs__testing-redux-store/.env
@@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true
16 changes: 16 additions & 0 deletions examples/blogs__testing-redux-store/README.md
@@ -0,0 +1,16 @@
# Testing Redux Store

Testing Redux store using Cypress.

## Shows how to

- control application via DOM and check that Redux store has been properly updated
- drive application by dispatching Redux actions
- use Redux actions directly from tests
- load initial Redux state from a fixture file
- use automatic user function retries with [cypress-pipe](https://github.com/NicholasBoll/cypress-pipe#readme)
- use snapshot testing via [meinaart/cypress-plugin-snapshots](https://github.com/meinaart/cypress-plugin-snapshots) plugin

## Application

The example TodoMVC application in this folder was copied from [https://github.com/reduxjs/redux/tree/master/examples/todomvc](https://github.com/reduxjs/redux/tree/master/examples/todomvc) on November 2018.
10 changes: 10 additions & 0 deletions examples/blogs__testing-redux-store/cypress.json
@@ -0,0 +1,10 @@
{
"baseUrl": "http://localhost:3000",
"ignoreTestFiles": [
"**/*.snap",
"**/__snapshot__/*"
],
"env": {
"cypress-plugin-snapshots": {}
}
}
17 changes: 17 additions & 0 deletions examples/blogs__testing-redux-store/cypress/fixtures/3-todos.json
@@ -0,0 +1,17 @@
[
{
"id": 0,
"text": "first",
"completed": true
},
{
"id": 1,
"text": "second",
"completed": false
},
{
"id": 2,
"text": "third",
"completed": true
}
]
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
@@ -0,0 +1,26 @@
exports[`snapshots #0`] =
{
"todos": [
{
"completed": false,
"id": 0,
"text": "Use Redux"
},
{
"completed": false,
"id": 1,
"text": "first"
},
{
"completed": true,
"id": 2,
"text": "second"
},
{
"completed": false,
"id": 3,
"text": "third"
}
],
"visibilityFilter": "show_completed"
};
142 changes: 142 additions & 0 deletions examples/blogs__testing-redux-store/cypress/integration/spec.js
@@ -0,0 +1,142 @@
/// <reference types="cypress" />
import { addTodo, deleteTodo } from '../../src/actions';

it('loads', () => {
cy.visit('/')
cy
.focused()
.should('have.class', 'new-todo')
.and('have.attr', 'placeholder', 'What needs to be done?')
cy.get('.todo-list li').should('have.length', 1).contains('Use Redux')
})

it('has expected state on load', () => {
cy.visit('/')
cy.window().its('store').invoke('getState').should('deep.equal', {
todos: [
{
completed: false,
id: 0,
text: 'Use Redux'
}
],
visibilityFilter: 'show_all'
})
})

it('is updated by the DOM actions', () => {
cy.visit('/')
cy.focused().type('Test with Cypress{enter}')
cy.contains('li', 'Test with Cypress').find('input[type=checkbox]').click()
cy.contains('.filters a', 'Completed').click()
cy.window().its('store').invoke('getState').should('deep.equal', {
todos: [
{
completed: false,
id: 0,
text: 'Use Redux'
},
{
completed: true,
id: 1,
text: 'Test with Cypress'
}
],
visibilityFilter: 'show_completed'
})
})

it('can wait for delayed updates', () => {
cy.visit('/')
cy.focused().type('first{enter}').type('second{enter}')
// check the dom
cy.get('.todo-list li').should('have.length', 3)
// now redux store should have been updated
cy.window().its('store').invoke('getState').its('todos').should('have.length', 3)
})

it('can wait for delayed updates using pipe', () => {
cy.visit('/')
cy.focused().type('first{enter}').type('second{enter}')
const getTodos = (win) =>
win.store.getState().todos
// using cypress-pipe the "getTodos" will be retried until
// should('have.length', 3) passes
// or
// default command timeout ends
cy.window().pipe(getTodos).should('have.length', 3)
})

it('can drive app by dispatching actions', () => {
cy.visit('/')
// dispatch Redux action
cy
.window()
.its('store')
.invoke('dispatch', { type: 'ADD_TODO', text: 'Test dispatch' })
// check if the app has updated its UI
cy.get('.todo-list li').should('have.length', 2).contains('Test dispatch')
})

const dispatch = action => cy.window().its('store').invoke('dispatch', action)

it('can use actions from application code', () => {
cy.visit('/')
dispatch(addTodo('Share code'))
dispatch(deleteTodo(0))
cy.get('.todo-list li').should('have.length', 1).contains('Share code')
})

it('can set initial todos', () => {
cy.visit('/', {
onBeforeLoad: win => {
win.initialState = [
{
id: 0,
text: 'first',
completed: true
},
{
id: 1,
text: 'second',
completed: false
},
{
id: 2,
text: 'third',
completed: true
}
]
}
})
// there should be 3 items in the UI
cy.get('.todo-list li').should('have.length', 3)
// and 2 of them should be completed
cy.get('.todo-list li.completed').should('have.length', 2)
})

const initialVisit = (url, fixture) => {
cy.fixture(fixture).then(todos => {
cy.visit(url, {
onBeforeLoad: win => {
win.initialState = todos
}
})
})
}

it('can set initial todos from a fixture', () => {
initialVisit('/', '3-todos.json')
// there should be 3 items in the UI
cy.get('.todo-list li').should('have.length', 3)
// and 2 of them should be completed
cy.get('.todo-list li.completed').should('have.length', 2)
})

it('snapshots', () => {
cy.visit('/')
cy.focused().type('first{enter}').type('second{enter}').type('third{enter}')
cy.contains('.todo-list li', 'second').find('input[type=checkbox]').click()
cy.contains('.filters a', 'Completed').click()
cy.window().its('store').invoke('getState').toMatchSnapshot()
})
5 changes: 5 additions & 0 deletions examples/blogs__testing-redux-store/cypress/plugins/index.js
@@ -0,0 +1,5 @@
const { initPlugin } = require('cypress-plugin-snapshots/plugin')
module.exports = (on, config) => {
initPlugin(on, config)
return config
}
25 changes: 25 additions & 0 deletions examples/blogs__testing-redux-store/cypress/support/commands.js
@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
2 changes: 2 additions & 0 deletions examples/blogs__testing-redux-store/cypress/support/index.js
@@ -0,0 +1,2 @@
import 'cypress-pipe';
import 'cypress-plugin-snapshots/commands';
20 changes: 20 additions & 0 deletions examples/blogs__testing-redux-store/package.json
@@ -0,0 +1,20 @@
{
"name": "blogs__testing-redux-store",
"version": "1.0.0",
"description": "Testing Redux store using Cypress",
"private": true,
"scripts": {
"start": "../../node_modules/.bin/react-scripts start",
"cypress:run": "../../node_modules/.bin/cypress run",
"cypress:open": "../../node_modules/.bin/cypress open"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"devDependencies": {
"cypress-plugin-snapshots": "1.0.6"
}
}
21 changes: 21 additions & 0 deletions examples/blogs__testing-redux-store/public/index.html
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Redux TodoMVC Example</title>
</head>
<body>
<div class="todoapp" id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` in this folder.
To create a production bundle, use `npm run build`.
-->
</body>
</html>
9 changes: 9 additions & 0 deletions examples/blogs__testing-redux-store/src/actions/index.js
@@ -0,0 +1,9 @@
import * as types from '../constants/ActionTypes'

export const addTodo = text => ({ type: types.ADD_TODO, text })
export const deleteTodo = id => ({ type: types.DELETE_TODO, id })
export const editTodo = (id, text) => ({ type: types.EDIT_TODO, id, text })
export const completeTodo = id => ({ type: types.COMPLETE_TODO, id })
export const completeAllTodos = () => ({ type: types.COMPLETE_ALL_TODOS })
export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED })
export const setVisibilityFilter = filter => ({ type: types.SET_VISIBILITY_FILTER, filter})

0 comments on commit bd40fd3

Please sign in to comment.