├── .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 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 | 35 |
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 | --------------------------------------------------------------------------------