Skip to content

Commit bd40fd3

Browse files
authored
Testing redux store (#211)
* 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
1 parent 78048de commit bd40fd3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+24644
-8829
lines changed

.travis.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
language: node_js
22
node_js:
3-
# 10.3 includes npm@6 which has good "npm ci" command
4-
- 10.3
3+
# 10.13 includes npm@6 which has good "npm ci" command, and is LTS
4+
- 10.13
55

66
cache:
77
directories:
@@ -43,6 +43,10 @@ jobs:
4343
env:
4444
- DIR=blogs__e2e-snapshots
4545
<<: *defaults
46+
- stage: test
47+
env:
48+
- DIR=blogs__testing-redux-store
49+
<<: *defaults
4650
- stage: test
4751
env:
4852
- DIR=blogs__vue-vuex-rest

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Recipe | Category | Description
1919
[E2E API Testing](#e2e-api-testing) | Blogs | Run your API Tests with a GUI
2020
[E2E Snapshots](#e2e-snapshots) | Blogs | End-to-End Snapshot Testing
2121
[Codepen.io Testing](#codepen-testing) | Blogs | Test a HyperApp Codepen demo
22+
[Testing Redux Store](#testing-redux-store) | Blogs | Test an application that uses Redux data store
2223
[Vue + Vuex + REST Testing](#vue--vuex--rest-testing) | Blogs | Test an application that uses central data store
2324
[Stubbing Functions](#stubbing-functions) | Stubbing, Spying | Use `cy.stub()` to test function calls
2425
[Stubbing `window.fetch`](#stubbing-windowfetch) | Stubbing, Spying | Use `cy.stub()` to control fetch requests
@@ -208,6 +209,13 @@ Get around the lack of a `.hover()` command.
208209
- Use [`cy.request()`](https://on.cypress.io/api/request) to load a document into test iframe.
209210
- Test [HyperApp.js](https://hyperapp.js.org/) application through the DOM and through actions.
210211

212+
### [Testing Redux Store](./examples/blogs__testing-redux-store)
213+
214+
- control application via DOM and check that Redux store has been properly updated
215+
- drive application by dispatching Redux actions
216+
- use Redux actions directly from tests
217+
- load initial Redux state from a fixture file
218+
211219
### [Vue + Vuex + REST Testing](./examples/blogs__vue-vuex-rest)
212220

213221
- [Blog post](https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/)

circle.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defaults: &defaults
55
parallelism: 1
66
working_directory: ~/app
77
docker:
8-
- image: cypress/browsers:chrome65-ff57
8+
- image: cypress/base:10
99
steps:
1010
- attach_workspace:
1111
at: ~/
@@ -26,7 +26,7 @@ jobs:
2626
build:
2727
working_directory: ~/app
2828
docker:
29-
- image: cypress/browsers:chrome65-ff57
29+
- image: cypress/base:10
3030
steps:
3131
- checkout
3232
- restore_cache:
@@ -59,6 +59,8 @@ jobs:
5959
<<: *defaults
6060
blogs__e2e-snapshots:
6161
<<: *defaults
62+
blogs__testing-redux-store:
63+
<<: *defaults
6264
blogs__vue-vuex-rest:
6365
<<: *defaults
6466
extending-cypress__chai-assertions:
@@ -121,6 +123,9 @@ workflows:
121123
- blogs__e2e-snapshots:
122124
requires:
123125
- build
126+
- blogs__testing-redux-store:
127+
requires:
128+
- build
124129
- blogs__vue-vuex-rest:
125130
requires:
126131
- build
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SKIP_PREFLIGHT_CHECK=true
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Testing Redux Store
2+
3+
Testing Redux store using Cypress.
4+
5+
## Shows how to
6+
7+
- control application via DOM and check that Redux store has been properly updated
8+
- drive application by dispatching Redux actions
9+
- use Redux actions directly from tests
10+
- load initial Redux state from a fixture file
11+
- use automatic user function retries with [cypress-pipe](https://github.com/NicholasBoll/cypress-pipe#readme)
12+
- use snapshot testing via [meinaart/cypress-plugin-snapshots](https://github.com/meinaart/cypress-plugin-snapshots) plugin
13+
14+
## Application
15+
16+
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.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"baseUrl": "http://localhost:3000",
3+
"ignoreTestFiles": [
4+
"**/*.snap",
5+
"**/__snapshot__/*"
6+
],
7+
"env": {
8+
"cypress-plugin-snapshots": {}
9+
}
10+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"id": 0,
4+
"text": "first",
5+
"completed": true
6+
},
7+
{
8+
"id": 1,
9+
"text": "second",
10+
"completed": false
11+
},
12+
{
13+
"id": 2,
14+
"text": "third",
15+
"completed": true
16+
}
17+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Using fixtures to represent data",
3+
"email": "[email protected]",
4+
"body": "Fixtures are a great way to mock data for responses to routes"
5+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
exports[`snapshots #0`] =
2+
{
3+
"todos": [
4+
{
5+
"completed": false,
6+
"id": 0,
7+
"text": "Use Redux"
8+
},
9+
{
10+
"completed": false,
11+
"id": 1,
12+
"text": "first"
13+
},
14+
{
15+
"completed": true,
16+
"id": 2,
17+
"text": "second"
18+
},
19+
{
20+
"completed": false,
21+
"id": 3,
22+
"text": "third"
23+
}
24+
],
25+
"visibilityFilter": "show_completed"
26+
};
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/// <reference types="cypress" />
2+
import { addTodo, deleteTodo } from '../../src/actions';
3+
4+
it('loads', () => {
5+
cy.visit('/')
6+
cy
7+
.focused()
8+
.should('have.class', 'new-todo')
9+
.and('have.attr', 'placeholder', 'What needs to be done?')
10+
cy.get('.todo-list li').should('have.length', 1).contains('Use Redux')
11+
})
12+
13+
it('has expected state on load', () => {
14+
cy.visit('/')
15+
cy.window().its('store').invoke('getState').should('deep.equal', {
16+
todos: [
17+
{
18+
completed: false,
19+
id: 0,
20+
text: 'Use Redux'
21+
}
22+
],
23+
visibilityFilter: 'show_all'
24+
})
25+
})
26+
27+
it('is updated by the DOM actions', () => {
28+
cy.visit('/')
29+
cy.focused().type('Test with Cypress{enter}')
30+
cy.contains('li', 'Test with Cypress').find('input[type=checkbox]').click()
31+
cy.contains('.filters a', 'Completed').click()
32+
cy.window().its('store').invoke('getState').should('deep.equal', {
33+
todos: [
34+
{
35+
completed: false,
36+
id: 0,
37+
text: 'Use Redux'
38+
},
39+
{
40+
completed: true,
41+
id: 1,
42+
text: 'Test with Cypress'
43+
}
44+
],
45+
visibilityFilter: 'show_completed'
46+
})
47+
})
48+
49+
it('can wait for delayed updates', () => {
50+
cy.visit('/')
51+
cy.focused().type('first{enter}').type('second{enter}')
52+
// check the dom
53+
cy.get('.todo-list li').should('have.length', 3)
54+
// now redux store should have been updated
55+
cy.window().its('store').invoke('getState').its('todos').should('have.length', 3)
56+
})
57+
58+
it('can wait for delayed updates using pipe', () => {
59+
cy.visit('/')
60+
cy.focused().type('first{enter}').type('second{enter}')
61+
const getTodos = (win) =>
62+
win.store.getState().todos
63+
// using cypress-pipe the "getTodos" will be retried until
64+
// should('have.length', 3) passes
65+
// or
66+
// default command timeout ends
67+
cy.window().pipe(getTodos).should('have.length', 3)
68+
})
69+
70+
it('can drive app by dispatching actions', () => {
71+
cy.visit('/')
72+
// dispatch Redux action
73+
cy
74+
.window()
75+
.its('store')
76+
.invoke('dispatch', { type: 'ADD_TODO', text: 'Test dispatch' })
77+
// check if the app has updated its UI
78+
cy.get('.todo-list li').should('have.length', 2).contains('Test dispatch')
79+
})
80+
81+
const dispatch = action => cy.window().its('store').invoke('dispatch', action)
82+
83+
it('can use actions from application code', () => {
84+
cy.visit('/')
85+
dispatch(addTodo('Share code'))
86+
dispatch(deleteTodo(0))
87+
cy.get('.todo-list li').should('have.length', 1).contains('Share code')
88+
})
89+
90+
it('can set initial todos', () => {
91+
cy.visit('/', {
92+
onBeforeLoad: win => {
93+
win.initialState = [
94+
{
95+
id: 0,
96+
text: 'first',
97+
completed: true
98+
},
99+
{
100+
id: 1,
101+
text: 'second',
102+
completed: false
103+
},
104+
{
105+
id: 2,
106+
text: 'third',
107+
completed: true
108+
}
109+
]
110+
}
111+
})
112+
// there should be 3 items in the UI
113+
cy.get('.todo-list li').should('have.length', 3)
114+
// and 2 of them should be completed
115+
cy.get('.todo-list li.completed').should('have.length', 2)
116+
})
117+
118+
const initialVisit = (url, fixture) => {
119+
cy.fixture(fixture).then(todos => {
120+
cy.visit(url, {
121+
onBeforeLoad: win => {
122+
win.initialState = todos
123+
}
124+
})
125+
})
126+
}
127+
128+
it('can set initial todos from a fixture', () => {
129+
initialVisit('/', '3-todos.json')
130+
// there should be 3 items in the UI
131+
cy.get('.todo-list li').should('have.length', 3)
132+
// and 2 of them should be completed
133+
cy.get('.todo-list li.completed').should('have.length', 2)
134+
})
135+
136+
it('snapshots', () => {
137+
cy.visit('/')
138+
cy.focused().type('first{enter}').type('second{enter}').type('third{enter}')
139+
cy.contains('.todo-list li', 'second').find('input[type=checkbox]').click()
140+
cy.contains('.filters a', 'Completed').click()
141+
cy.window().its('store').invoke('getState').toMatchSnapshot()
142+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { initPlugin } = require('cypress-plugin-snapshots/plugin')
2+
module.exports = (on, config) => {
3+
initPlugin(on, config)
4+
return config
5+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// ***********************************************
2+
// This example commands.js shows you how to
3+
// create various custom commands and overwrite
4+
// existing commands.
5+
//
6+
// For more comprehensive examples of custom
7+
// commands please read more here:
8+
// https://on.cypress.io/custom-commands
9+
// ***********************************************
10+
//
11+
//
12+
// -- This is a parent command --
13+
// Cypress.Commands.add("login", (email, password) => { ... })
14+
//
15+
//
16+
// -- This is a child command --
17+
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18+
//
19+
//
20+
// -- This is a dual command --
21+
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22+
//
23+
//
24+
// -- This is will overwrite an existing command --
25+
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import 'cypress-pipe';
2+
import 'cypress-plugin-snapshots/commands';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "blogs__testing-redux-store",
3+
"version": "1.0.0",
4+
"description": "Testing Redux store using Cypress",
5+
"private": true,
6+
"scripts": {
7+
"start": "../../node_modules/.bin/react-scripts start",
8+
"cypress:run": "../../node_modules/.bin/cypress run",
9+
"cypress:open": "../../node_modules/.bin/cypress open"
10+
},
11+
"browserslist": [
12+
">0.2%",
13+
"not dead",
14+
"not ie <= 11",
15+
"not op_mini all"
16+
],
17+
"devDependencies": {
18+
"cypress-plugin-snapshots": "1.0.6"
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Redux TodoMVC Example</title>
7+
</head>
8+
<body>
9+
<div class="todoapp" id="root"></div>
10+
<!--
11+
This HTML file is a template.
12+
If you open it directly in the browser, you will see an empty page.
13+
14+
You can add webfonts, meta tags, or analytics to this file.
15+
The build step will place the bundled scripts into the <body> tag.
16+
17+
To begin the development, run `npm start` in this folder.
18+
To create a production bundle, use `npm run build`.
19+
-->
20+
</body>
21+
</html>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as types from '../constants/ActionTypes'
2+
3+
export const addTodo = text => ({ type: types.ADD_TODO, text })
4+
export const deleteTodo = id => ({ type: types.DELETE_TODO, id })
5+
export const editTodo = (id, text) => ({ type: types.EDIT_TODO, id, text })
6+
export const completeTodo = id => ({ type: types.COMPLETE_TODO, id })
7+
export const completeAllTodos = () => ({ type: types.COMPLETE_ALL_TODOS })
8+
export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED })
9+
export const setVisibilityFilter = filter => ({ type: types.SET_VISIBILITY_FILTER, filter})

0 commit comments

Comments
 (0)