├── .babelrc
├── .gitignore
├── README.md
├── package.json
├── src
├── AddToRegistryForm.js
├── RegistryItem.js
├── RegistryList.js
├── actions.js
├── index.js
├── reducer.js
└── store.js
└── tests
└── RegistryItem.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | "es2015",
5 | "stage-2"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-redux-checkpoint-prep
2 |
3 | 1. npm install
4 | 2. npm test
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "egghead-react-testing",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha './tests/**/*.spec.js' --compilers js:babel-register"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "react": "^15.3.2",
14 | "react-dom": "^15.3.2",
15 | "redux": "^3.6.0"
16 | },
17 | "devDependencies": {
18 | "babel-core": "^6.17.0",
19 | "babel-preset-es2015": "^6.16.0",
20 | "babel-preset-react": "^6.16.0",
21 | "babel-preset-stage-2": "^6.17.0",
22 | "babel-register": "^6.16.3",
23 | "chai": "^3.5.0",
24 | "chai-enzyme": "^0.5.2",
25 | "chai-jsx": "^1.0.1",
26 | "chalk": "^1.1.3",
27 | "enzyme": "^2.4.1",
28 | "expect": "^1.20.2",
29 | "faker": "^3.1.0",
30 | "lodash": "^4.16.4",
31 | "mocha": "^3.1.0",
32 | "react-addons-test-utils": "^15.3.2",
33 | "sinon": "^1.17.6"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/AddToRegistryForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class extends React.Component {
4 |
5 | constructor () {
6 | super();
7 | this.state = {
8 | itemName: '',
9 | itemPrice: ''
10 | };
11 | this.updateItemName = this.updateItemName.bind(this);
12 | this.updateItemPrice = this.updateItemPrice.bind(this);
13 | }
14 |
15 | updateItemName (e) {
16 | this.setState({ itemName: e.target.value });
17 | }
18 |
19 | updateItemPrice (e) {
20 | this.setState({ itemPrice: e.target.value });
21 | }
22 |
23 | render () {
24 | return (
25 |
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/RegistryItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default ( {itemDetails, markAsPurchased} ) => (
4 |
5 |
6 | Item name: {itemDetails.name}
7 | Item price: {itemDetails.price}
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/RegistryList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import RegistryItem from './RegistryItem';
3 | import store from './store';
4 |
5 | export default class extends React.Component {
6 |
7 | constructor() {
8 | super();
9 | this.state = store.getState();
10 | }
11 |
12 | componentWillMount () {
13 | store.subscribe(() => this.setState(store.getState()));
14 | }
15 |
16 | render() {
17 |
18 | return (
19 |
20 |
My Registry
21 | {this.state.registryItems.map(registryItem => {
22 | return
23 | })}
24 |
25 | );
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | export const ADD_ITEM_TO_REGISTRY = "ADD_ITEM_TO_REGISTRY";
2 |
3 | export const createNewItemAction = item => ({
4 | type: ADD_ITEM_TO_REGISTRY,
5 | item: item
6 | });
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Provider } from "react-redux";
4 | import store from "./store";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("app")
11 | );
12 |
--------------------------------------------------------------------------------
/src/reducer.js:
--------------------------------------------------------------------------------
1 | import { ADD_ITEM_TO_REGISTRY } from "./actions";
2 |
3 | const initialState = {
4 | registryItems: []
5 | };
6 |
7 | export default (state = initialState, action) => {
8 | switch(action.type) {
9 | case ADD_ITEM_TO_REGISTRY: return Object.assign({}, state, {
10 | registryItems: state.registryItems.concat(action.item)
11 | });
12 | default: return state;
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | // This file does not need to be modified in any way.
2 |
3 | import { createStore } from 'redux';
4 | import rootReducer from './reducer';
5 |
6 | export default createStore(rootReducer);
7 |
--------------------------------------------------------------------------------
/tests/RegistryItem.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {createStore} from 'redux';
3 | import {range, last} from 'lodash';
4 |
5 | import chai, {expect} from 'chai';
6 | import chaiEnzyme from 'chai-enzyme';
7 | chai.use(chaiEnzyme());
8 | import {shallow} from 'enzyme';
9 | import {spy} from 'sinon';
10 | import faker from 'faker';
11 |
12 | import RegistryItem from '../src/RegistryItem';
13 | import RegistryList from '../src/RegistryList';
14 | import AddToRegistryForm from '../src/AddToRegistryForm';
15 | import rootReducer from '../src/reducer';
16 | import actualStore from '../src/store';
17 | import {createNewItemAction} from '../src/actions';
18 |
19 | const createRandomItems = amount => {
20 | return range(0, amount).map(index => {
21 | return {
22 | id: index + 1,
23 | name: faker.lorem.words(),
24 | price: faker.random.number()
25 | };
26 | });
27 | };
28 | const testUtilities = {
29 | createRandomItems,
30 | createOneRandomItem: () => createRandomItems(1)[0]
31 | };
32 |
33 | describe('RegistryItem', () => {
34 |
35 | describe('visual content', () => {
36 |
37 | let itemData, itemWrapper;
38 | beforeEach('Create wrapper', () => {
39 | itemData = {
40 | id: 5,
41 | name: 'Curtains',
42 | price: 100
43 | };
44 | itemWrapper = shallow();
45 | });
46 |
47 | it('includes "name" line as an h1', () => {
48 | expect(itemWrapper.find('h1')).to.have.html('Item name: Curtains
');
49 | });
50 |
51 | it('includes "price" line as h2', () => {
52 | expect(itemWrapper.find('h2')).to.have.html('Item price: 100
');
53 | });
54 |
55 | it('is not hardcoded', () => {
56 | const aDifferentItem = {
57 | id: 6,
58 | name: 'Wine glasses',
59 | price: 200
60 | };
61 | const differentItemWrapper = shallow();
62 | expect(differentItemWrapper.find('h1')).to.have.html('Item name: Wine glasses
');
63 | expect(differentItemWrapper.find('h2')).to.have.html('Item price: 200
');
64 | });
65 |
66 | });
67 |
68 | describe('interactivity', () => {
69 |
70 | let itemData, itemWrapper, markAsPurchasedSpy;
71 | beforeEach('Create ', () => {
72 | itemData = testUtilities.createOneRandomItem();
73 | markAsPurchasedSpy = spy();
74 | itemWrapper = shallow();
75 | });
76 |
77 | it('when clicked, invokes a function passed in as the markAsPurchased property with the item id', () => {
78 | itemWrapper.simulate('click');
79 | expect(markAsPurchasedSpy.called).to.be.true;
80 | expect(markAsPurchasedSpy.calledWith(itemData.id)).to.be.true;
81 | });
82 |
83 | });
84 |
85 | });
86 |
87 | describe('RegistryList', () => {
88 |
89 | let randomItems;
90 | beforeEach('Create random example items', () => {
91 | randomItems = testUtilities.createRandomItems(10);
92 | });
93 |
94 | let registryListWrapper;
95 | beforeEach('Create ', () => {
96 | registryListWrapper = shallow();
97 | });
98 |
99 | it('starts with an initial state having an empty registryItems array', () => {
100 | const currentState = registryListWrapper.state();
101 | expect(currentState.registryItems).to.be.deep.equal([]);
102 | });
103 |
104 | describe('visual content', () => {
105 |
106 | it('is a and has a first child element
with the text "My Registry"', () => {
107 |
108 | expect(registryListWrapper.is('div')).to.be.true;
109 |
110 | const hopefullyH1 = registryListWrapper.children().first();
111 | expect(hopefullyH1.is('h1')).to.be.true;
112 | expect(hopefullyH1.text()).to.be.equal('My Registry');
113 |
114 | });
115 |
116 | it('is comprised of components based on what gets placed on the state', () => {
117 |
118 | registryListWrapper.setState({registryItems: randomItems});
119 |
120 | expect(registryListWrapper.find(RegistryItem)).to.have.length(10);
121 |
122 | const firstItem = registryListWrapper.find(RegistryItem).at(0);
123 | expect(firstItem.equals()).to.be.true;
124 |
125 | registryListWrapper.setState({registryItems: randomItems.slice(4)});
126 | expect(registryListWrapper.find(RegistryItem)).to.have.length(6);
127 |
128 | });
129 |
130 | });
131 |
132 | });
133 |
134 | describe('AddToRegistryForm', () => {
135 |
136 | let sendSpy;
137 | beforeEach('Create spy function to pass in', () => {
138 | sendSpy = spy();
139 | });
140 |
141 | let addToRegistryFormWrapper;
142 | beforeEach('Create wrapper', () => {
143 | addToRegistryFormWrapper = shallow();
144 | });
145 |
146 | it('sets local state when inputs change', () => {
147 |
148 | expect(addToRegistryFormWrapper.state()).to.be.deep.equal({
149 | itemName: '',
150 | itemPrice: ''
151 | });
152 |
153 | const itemNameInput = addToRegistryFormWrapper.find('#item-name-field');
154 | itemNameInput.simulate('change', {target: {value: 'sheets'}});
155 | expect(addToRegistryFormWrapper.state().itemName).to.be.equal('sheets');
156 |
157 | const itemPriceInput = addToRegistryFormWrapper.find('#item-price-field');
158 | itemPriceInput.simulate('change', {target: {value: '50'}});
159 | expect(addToRegistryFormWrapper.state().itemPrice).to.be.equal('50');
160 |
161 | });
162 |
163 | it('invokes passed in `onSend` function with local state when form is submitted', () => {
164 |
165 | const formInfo = {
166 | itemName: 'sheets',
167 | itemPrice: '50',
168 | };
169 |
170 | addToRegistryFormWrapper.setState(formInfo);
171 |
172 | addToRegistryFormWrapper.simulate('submit');
173 |
174 | expect(sendSpy.called).to.be.true;
175 | expect(sendSpy.calledWith(formInfo)).to.be.true;
176 |
177 | });
178 |
179 | });
180 |
181 | describe('Redux architecture', () => {
182 |
183 | describe('action creators', () => {
184 |
185 | describe('createNewItemAction', () => {
186 |
187 | it('returns expected action description', () => {
188 |
189 | const item = testUtilities.createOneRandomItem();
190 |
191 | const actionDescriptor = createNewItemAction(item);
192 |
193 | expect(actionDescriptor).to.be.deep.equal({
194 | type: 'ADD_ITEM_TO_REGISTRY',
195 | item: item
196 | });
197 |
198 | });
199 |
200 | });
201 |
202 | });
203 |
204 | describe('store/reducer', () => {
205 |
206 | let testingStore;
207 | beforeEach('Create testing store from reducer', () => {
208 | testingStore = createStore(rootReducer);
209 | });
210 |
211 | it('has an initial state as described', () => {
212 | const currentStoreState = testingStore.getState();
213 | expect(currentStoreState.registryItems).to.be.deep.equal([]);
214 | });
215 |
216 | describe('reducing on ADD_ITEM_TO_REGISTRY', () => {
217 |
218 | let existingRandomItems;
219 | beforeEach(() => {
220 | existingRandomItems = testUtilities.createRandomItems(5);
221 | testingStore = createStore(
222 | rootReducer,
223 | {registryItems: existingRandomItems}
224 | );
225 | });
226 |
227 | it('affects the state by appending dispatched items to state registryItems', () => {
228 |
229 | const dispatchedItem = testUtilities.createOneRandomItem();
230 |
231 | testingStore.dispatch({
232 | type: 'ADD_ITEM_TO_REGISTRY',
233 | item: dispatchedItem
234 | });
235 |
236 | const newState = testingStore.getState();
237 | const lastItemOnState = last(newState.registryItems);
238 |
239 | expect(newState.registryItems).to.have.length(6);
240 | expect(lastItemOnState).to.be.deep.equal(dispatchedItem);
241 |
242 | });
243 |
244 | it('sets items to different array from previous state', () => {
245 |
246 | const originalState = testingStore.getState();
247 | const dispatchedItem = testUtilities.createOneRandomItem();
248 |
249 | testingStore.dispatch({
250 | type: 'ADD_ITEM_TO_REGISTRY',
251 | item: dispatchedItem
252 | });
253 |
254 | const newState = testingStore.getState();
255 |
256 | expect(newState.registryItems).to.not.be.equal(originalState.registryItems);
257 | expect(originalState.registryItems).to.have.length(5);
258 |
259 | });
260 |
261 | });
262 |
263 | });
264 |
265 | });
266 |
--------------------------------------------------------------------------------