├── .gitignore ├── src ├── features │ ├── cart │ │ ├── selectors.js │ │ ├── index.js │ │ ├── actionCreators.js │ │ ├── components │ │ │ ├── Cart.js │ │ │ ├── CartContainer.js │ │ │ └── Cart.spec.js │ │ ├── reducer.js │ │ └── reducer.spec.js │ └── product │ │ ├── selectors.js │ │ ├── index.js │ │ ├── components │ │ ├── ProductsList.js │ │ ├── Product.js │ │ ├── Product.spec.js │ │ ├── ProductItem.js │ │ ├── ProductsList.spec.js │ │ ├── ProductsContainer.js │ │ └── ProductItem.spec.js │ │ ├── actionCreators.js │ │ ├── reducer.js │ │ └── reducer.spec.js ├── shared │ ├── api │ │ ├── products.json │ │ └── shop.js │ ├── reducers.js │ ├── constants │ │ └── ActionTypes.js │ ├── selectors.js │ └── reducers.spec.js ├── App.js └── index.js ├── package.json ├── public └── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/features/cart/selectors.js: -------------------------------------------------------------------------------- 1 | export const getQuantity = (state, productId) => 2 | state.cart.quantityById[productId] || 0 3 | 4 | export const getAddedIds = state => state.cart.addedIds -------------------------------------------------------------------------------- /src/features/product/selectors.js: -------------------------------------------------------------------------------- 1 | export const getProduct = (state, id) => ((state, id) => { debugger; return null;})(state, id) || 2 | state.products.byId[id] 3 | 4 | export const getVisibleProducts = state => 5 | state.visibleIds.map(id => getProduct(state, id)) 6 | -------------------------------------------------------------------------------- /src/shared/api/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2}, 3 | {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10}, 4 | {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5} 5 | ] 6 | -------------------------------------------------------------------------------- /src/shared/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { reducer as cart } from '../features/cart' 3 | import { reducer as products } from '../features/product' 4 | 5 | export default combineReducers({ 6 | cart, 7 | products 8 | }) 9 | 10 | -------------------------------------------------------------------------------- /src/features/cart/index.js: -------------------------------------------------------------------------------- 1 | import Cart from './components/CartContainer'; 2 | 3 | import * as actionCreators from './actionCreators'; 4 | import * as selectors from './selectors'; 5 | import reducer from './reducer'; 6 | 7 | export { actionCreators, selectors, reducer }; 8 | export default Cart; -------------------------------------------------------------------------------- /src/shared/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TO_CART = 'ADD_TO_CART' 2 | export const CHECKOUT_REQUEST = 'CHECKOUT_REQUEST' 3 | export const CHECKOUT_SUCCESS = 'CHECKOUT_SUCCESS' 4 | export const CHECKOUT_FAILURE = 'CHECKOUT_FAILURE' 5 | export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS' 6 | -------------------------------------------------------------------------------- /src/features/product/index.js: -------------------------------------------------------------------------------- 1 | import Product from './components/ProductsContainer'; 2 | 3 | import * as actionCreators from './actionCreators'; 4 | import * as selectors from './selectors'; 5 | import reducer from './reducer'; 6 | 7 | export { actionCreators, selectors, reducer }; 8 | 9 | export default Product; -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Products from './features/product' 3 | import Cart from './features/cart' 4 | 5 | const App = () => ( 6 |
7 |

Shopping Cart Example

8 |
9 | 10 |
11 | 12 |
13 | ) 14 | 15 | export default App 16 | -------------------------------------------------------------------------------- /src/shared/api/shop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocking client-server processing 3 | */ 4 | import _products from './products.json' 5 | 6 | const TIMEOUT = 100 7 | 8 | export default { 9 | getProducts: (cb, timeout) => setTimeout(() => cb(_products), timeout || TIMEOUT), 10 | buyProducts: (payload, cb, timeout) => setTimeout(() => cb(), timeout || TIMEOUT) 11 | } 12 | -------------------------------------------------------------------------------- /src/features/product/components/ProductsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ProductsList = ({ title, children }) => ( 5 |
6 |

{title}

7 |
{children}
8 |
9 | ) 10 | 11 | ProductsList.propTypes = { 12 | children: PropTypes.node, 13 | title: PropTypes.string.isRequired 14 | } 15 | 16 | export default ProductsList 17 | -------------------------------------------------------------------------------- /src/features/product/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as types from '../../shared/constants/ActionTypes' 2 | import shop from '../../shared/api/shop' 3 | 4 | const receiveProducts = products => ({ 5 | type: types.RECEIVE_PRODUCTS, 6 | products: products 7 | }) 8 | 9 | export const getAllProducts = () => dispatch => { 10 | shop.getProducts(products => { 11 | dispatch(receiveProducts(products)) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/features/product/components/Product.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Product = ({ price, quantity, title }) => ( 5 |
6 | {title} - ${price}{quantity ? ` x ${quantity}` : null} 7 |
8 | ) 9 | 10 | Product.propTypes = { 11 | price: PropTypes.number, 12 | quantity: PropTypes.number, 13 | title: PropTypes.string 14 | } 15 | 16 | export default Product 17 | -------------------------------------------------------------------------------- /src/shared/selectors.js: -------------------------------------------------------------------------------- 1 | import { getProduct } from '../features/product/selectors'; 2 | import { getAddedIds, getQuantity } from '../features/cart/selectors'; 3 | 4 | export const getTotal = state => 5 | getAddedIds(state) 6 | .reduce((total, id) => 7 | total + getProduct(state, id).price * getQuantity(state, id), 8 | 0 9 | ) 10 | .toFixed(2) 11 | 12 | export const getCartProducts = state => 13 | getAddedIds(state).map(id => ({ 14 | ...getProduct(state, id), 15 | quantity: getQuantity(state, id) 16 | })) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopping-cart", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^2.8.2", 7 | "react-scripts": "^1.0.1", 8 | "react-test-renderer": "^15.6.1" 9 | }, 10 | "dependencies": { 11 | "prop-types": "^15.5.10", 12 | "react": "^15.5.0", 13 | "react-dom": "^15.5.0", 14 | "react-redux": "^5.0.5", 15 | "redux": "^3.5.2", 16 | "redux-logger": "^3.0.6", 17 | "redux-thunk": "^2.1.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "eject": "react-scripts eject", 23 | "test": "react-scripts test" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Shopping Cart Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Shopping Cart Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | This is a very simplistic use of feature folders rather than the original project structure -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import { createLogger } from 'redux-logger' 6 | import thunk from 'redux-thunk' 7 | 8 | import reducer from './shared/reducers' 9 | import { getAllProducts } from './features/product/actionCreators' 10 | import App from './App' 11 | 12 | const middleware = [ thunk ]; 13 | if (process.env.NODE_ENV !== 'production') { 14 | middleware.push(createLogger()); 15 | } 16 | 17 | const store = createStore( 18 | reducer, 19 | applyMiddleware(...middleware) 20 | ) 21 | 22 | store.dispatch(getAllProducts()) 23 | 24 | render( 25 | 26 | 27 | , 28 | document.getElementById('root') 29 | ) 30 | -------------------------------------------------------------------------------- /src/features/product/components/Product.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Product from './Product' 4 | 5 | const setup = props => { 6 | const component = shallow( 7 | 8 | ) 9 | 10 | return { 11 | component: component 12 | } 13 | } 14 | 15 | describe('Product component', () => { 16 | it('should render title and price', () => { 17 | const { component } = setup({ title: 'Test Product', price: 9.99 }) 18 | expect(component.text()).toBe('Test Product - $9.99') 19 | }) 20 | 21 | describe('when given quantity', () => { 22 | it('should render title, price, and quantity', () => { 23 | const { component } = setup({ title: 'Test Product', price: 9.99, quantity: 6 }) 24 | expect(component.text()).toBe('Test Product - $9.99 x 6') 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/features/product/components/ProductItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Product from './Product' 4 | 5 | const ProductItem = ({ product, onAddToCartClicked }) => ( 6 |
7 | 10 | 15 |
16 | ) 17 | 18 | ProductItem.propTypes = { 19 | product: PropTypes.shape({ 20 | title: PropTypes.string.isRequired, 21 | price: PropTypes.number.isRequired, 22 | inventory: PropTypes.number.isRequired 23 | }).isRequired, 24 | onAddToCartClicked: PropTypes.func.isRequired 25 | } 26 | 27 | export default ProductItem 28 | -------------------------------------------------------------------------------- /src/features/product/components/ProductsList.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import ProductsList from './ProductsList' 4 | 5 | const setup = props => { 6 | const component = shallow( 7 | {props.children} 8 | ) 9 | 10 | return { 11 | component: component, 12 | children: component.children().at(1), 13 | h3: component.find('h3') 14 | } 15 | } 16 | 17 | describe('ProductsList component', () => { 18 | it('should render title', () => { 19 | const { h3 } = setup({ title: 'Test Products' }) 20 | expect(h3.text()).toMatch(/^Test Products$/) 21 | }) 22 | 23 | it('should render children', () => { 24 | const { children } = setup({ title: 'Test Products', children: 'Test Children' }) 25 | expect(children.text()).toMatch(/^Test Children$/) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/features/cart/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as types from '../../shared/constants/ActionTypes' 2 | import shop from '../../shared/api/shop' 3 | 4 | const addToCartUnsafe = productId => ({ 5 | type: types.ADD_TO_CART, 6 | productId 7 | }) 8 | 9 | export const addToCart = productId => (dispatch, getState) => { 10 | if (getState().products.byId[productId].inventory > 0) { 11 | dispatch(addToCartUnsafe(productId)) 12 | } 13 | } 14 | 15 | export const checkout = products => (dispatch, getState) => { 16 | const { cart } = getState() 17 | 18 | dispatch({ 19 | type: types.CHECKOUT_REQUEST 20 | }) 21 | shop.buyProducts(products, () => { 22 | dispatch({ 23 | type: types.CHECKOUT_SUCCESS, 24 | cart 25 | }) 26 | // Replace the line above with line below to rollback on failure: 27 | // dispatch({ type: types.CHECKOUT_FAILURE, cart }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/features/cart/components/Cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Product from '../../product/components/Product' 4 | 5 | const Cart = ({ products, total, onCheckoutClicked }) => { 6 | const hasProducts = products.length > 0 7 | const nodes = hasProducts ? ( 8 | products.map(product => 9 | 15 | ) 16 | ) : ( 17 | Please add some products to cart. 18 | ) 19 | 20 | return ( 21 |
22 |

Your Cart

23 |
{nodes}
24 |

Total: ${total}

25 | 29 |
30 | ) 31 | } 32 | 33 | Cart.propTypes = { 34 | products: PropTypes.array, 35 | total: PropTypes.string, 36 | onCheckoutClicked: PropTypes.func 37 | } 38 | 39 | export default Cart 40 | -------------------------------------------------------------------------------- /src/features/cart/components/CartContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { checkout } from '../actionCreators' 5 | import { getTotal, getCartProducts } from '../../../shared/selectors' 6 | import Cart from '../components/Cart' 7 | 8 | const CartContainer = ({ products, total, checkout }) => ( 9 | checkout(products)} /> 13 | ) 14 | 15 | CartContainer.propTypes = { 16 | products: PropTypes.arrayOf(PropTypes.shape({ 17 | id: PropTypes.number.isRequired, 18 | title: PropTypes.string.isRequired, 19 | price: PropTypes.number.isRequired, 20 | quantity: PropTypes.number.isRequired 21 | })).isRequired, 22 | total: PropTypes.string, 23 | checkout: PropTypes.func.isRequired 24 | } 25 | 26 | const mapStateToProps = (state) => ({ 27 | products: getCartProducts(state), 28 | total: getTotal(state) 29 | }) 30 | 31 | export default connect( 32 | mapStateToProps, 33 | { checkout } 34 | )(CartContainer) 35 | -------------------------------------------------------------------------------- /src/features/product/components/ProductsContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { addToCart } from '../../cart/actionCreators' 5 | import { getVisibleProducts } from '../reducer' 6 | import ProductItem from '../components/ProductItem' 7 | import ProductsList from '../components/ProductsList' 8 | 9 | const ProductsContainer = ({ products, addToCart }) => ( 10 | 11 | {products.map(product => 12 | addToCart(product.id)} /> 16 | )} 17 | 18 | ) 19 | 20 | ProductsContainer.propTypes = { 21 | products: PropTypes.arrayOf(PropTypes.shape({ 22 | id: PropTypes.number.isRequired, 23 | title: PropTypes.string.isRequired, 24 | price: PropTypes.number.isRequired, 25 | inventory: PropTypes.number.isRequired 26 | })).isRequired, 27 | addToCart: PropTypes.func.isRequired 28 | } 29 | 30 | const mapStateToProps = state => ({ 31 | products: getVisibleProducts(state.products) 32 | }) 33 | 34 | export default connect( 35 | mapStateToProps, 36 | { addToCart } 37 | )(ProductsContainer) 38 | -------------------------------------------------------------------------------- /src/features/cart/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART, 3 | CHECKOUT_REQUEST, 4 | CHECKOUT_FAILURE 5 | } from '../../shared/constants/ActionTypes' 6 | 7 | const initialState = { 8 | addedIds: [], 9 | quantityById: {} 10 | } 11 | 12 | const addedIds = (state = initialState.addedIds, action) => { 13 | switch (action.type) { 14 | case ADD_TO_CART: 15 | if (state.indexOf(action.productId) !== -1) { 16 | return state 17 | } 18 | return [ ...state, action.productId ] 19 | default: 20 | return state 21 | } 22 | } 23 | 24 | const quantityById = (state = initialState.quantityById, action) => { 25 | switch (action.type) { 26 | case ADD_TO_CART: 27 | const { productId } = action 28 | return { ...state, 29 | [productId]: (state[productId] || 0) + 1 30 | } 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | const cart = (state = initialState, action) => { 37 | switch (action.type) { 38 | case CHECKOUT_REQUEST: 39 | return initialState 40 | case CHECKOUT_FAILURE: 41 | return action.cart 42 | default: 43 | return { 44 | addedIds: addedIds(state.addedIds, action), 45 | quantityById: quantityById(state.quantityById, action) 46 | } 47 | } 48 | } 49 | 50 | export default cart 51 | -------------------------------------------------------------------------------- /src/features/cart/reducer.spec.js: -------------------------------------------------------------------------------- 1 | import cart from './reducer' 2 | 3 | describe('reducers', () => { 4 | describe('cart', () => { 5 | const initialState = { 6 | addedIds: [], 7 | quantityById: {} 8 | } 9 | 10 | it('should provide the initial state', () => { 11 | expect(cart(undefined, {})).toEqual(initialState) 12 | }) 13 | 14 | it('should handle CHECKOUT_REQUEST action', () => { 15 | expect(cart({}, { type: 'CHECKOUT_REQUEST' })).toEqual(initialState) 16 | }) 17 | 18 | it('should handle CHECKOUT_FAILURE action', () => { 19 | expect(cart({}, { type: 'CHECKOUT_FAILURE', cart: 'cart state' })).toEqual('cart state') 20 | }) 21 | 22 | it('should handle ADD_TO_CART action', () => { 23 | expect(cart(initialState, { type: 'ADD_TO_CART', productId: 1 })).toEqual({ 24 | addedIds: [ 1 ], 25 | quantityById: { 1: 1 } 26 | }) 27 | }) 28 | 29 | describe('when product is already in cart', () => { 30 | it('should handle ADD_TO_CART action', () => { 31 | const state = { 32 | addedIds: [ 1, 2 ], 33 | quantityById: { 1: 1, 2: 1 } 34 | } 35 | 36 | expect(cart(state, { type: 'ADD_TO_CART', productId: 2 })).toEqual({ 37 | addedIds: [ 1, 2 ], 38 | quantityById: { 1: 1, 2: 2 } 39 | }) 40 | }) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/features/product/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { RECEIVE_PRODUCTS, ADD_TO_CART } from '../../shared/constants/ActionTypes' 3 | 4 | const products = (state, action) => { 5 | switch (action.type) { 6 | case ADD_TO_CART: 7 | return { 8 | ...state, 9 | inventory: state.inventory - 1 10 | } 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | const byId = (state = {}, action) => { 17 | switch (action.type) { 18 | case RECEIVE_PRODUCTS: 19 | return { 20 | ...state, 21 | ...action.products.reduce((obj, product) => { 22 | obj[product.id] = product 23 | return obj 24 | }, {}) 25 | } 26 | default: 27 | const { productId } = action 28 | if (productId) { 29 | return { 30 | ...state, 31 | [productId]: products(state[productId], action) 32 | } 33 | } 34 | return state 35 | } 36 | } 37 | 38 | const visibleIds = (state = [], action) => { 39 | switch (action.type) { 40 | case RECEIVE_PRODUCTS: 41 | return action.products.map(product => product.id) 42 | default: 43 | return state 44 | } 45 | } 46 | 47 | export default combineReducers({ 48 | byId, 49 | visibleIds 50 | }) 51 | 52 | export const getProduct = (state, id) => 53 | state.byId[id] 54 | 55 | export const getVisibleProducts = state => 56 | state.visibleIds.map(id => getProduct(state, id)) 57 | -------------------------------------------------------------------------------- /src/shared/reducers.spec.js: -------------------------------------------------------------------------------- 1 | import { getTotal, getCartProducts } from './selectors' 2 | 3 | describe('selectors', () => { 4 | describe('getTotal', () => { 5 | it('should return price total', () => { 6 | const state = { 7 | cart: { 8 | addedIds: [ 1, 2, 3 ], 9 | quantityById: { 10 | 1: 4, 11 | 2: 2, 12 | 3: 1 13 | } 14 | }, 15 | products: { 16 | byId: { 17 | 1: { 18 | id: 1, 19 | price: 1.99 20 | }, 21 | 2: { 22 | id: 1, 23 | price: 4.99 24 | }, 25 | 3: { 26 | id: 1, 27 | price: 9.99 28 | } 29 | } 30 | } 31 | } 32 | expect(getTotal(state)).toBe('27.93') 33 | }) 34 | }) 35 | 36 | describe('getCartProducts', () => { 37 | it('should return products with quantity', () => { 38 | const state = { 39 | cart: { 40 | addedIds: [ 1, 2, 3 ], 41 | quantityById: { 42 | 1: 4, 43 | 2: 2, 44 | 3: 1 45 | } 46 | }, 47 | products: { 48 | byId: { 49 | 1: { 50 | id: 1, 51 | price: 1.99 52 | }, 53 | 2: { 54 | id: 1, 55 | price: 4.99 56 | }, 57 | 3: { 58 | id: 1, 59 | price: 9.99 60 | } 61 | } 62 | } 63 | } 64 | 65 | expect(getCartProducts(state)).toEqual([ 66 | { 67 | id: 1, 68 | price: 1.99, 69 | quantity: 4 70 | }, 71 | { 72 | id: 1, 73 | price: 4.99, 74 | quantity: 2 75 | }, 76 | { 77 | id: 1, 78 | price: 9.99, 79 | quantity: 1 80 | } 81 | ]) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/features/product/components/ProductItem.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Product from './Product' 4 | import ProductItem from './ProductItem' 5 | 6 | const setup = product => { 7 | const actions = { 8 | onAddToCartClicked: jest.fn() 9 | } 10 | 11 | const component = shallow( 12 | 13 | ) 14 | 15 | return { 16 | component: component, 17 | actions: actions, 18 | button: component.find('button'), 19 | product: component.find(Product) 20 | } 21 | } 22 | 23 | let productProps 24 | 25 | describe('ProductItem component', () => { 26 | beforeEach(() => { 27 | productProps = { 28 | title: 'Product 1', 29 | price: 9.99, 30 | inventory: 6 31 | } 32 | }) 33 | 34 | it('should render product', () => { 35 | const { product } = setup(productProps) 36 | expect(product.props()).toEqual({ title: 'Product 1', price: 9.99 }) 37 | }) 38 | 39 | it('should render Add To Cart message', () => { 40 | const { button } = setup(productProps) 41 | expect(button.text()).toMatch(/^Add to cart/) 42 | }) 43 | 44 | it('should not disable button', () => { 45 | const { button } = setup(productProps) 46 | expect(button.prop('disabled')).toEqual('') 47 | }) 48 | 49 | it('should call action on button click', () => { 50 | const { button, actions } = setup(productProps) 51 | button.simulate('click') 52 | expect(actions.onAddToCartClicked).toBeCalled() 53 | }) 54 | 55 | describe('when product inventory is 0', () => { 56 | beforeEach(() => { 57 | productProps.inventory = 0 58 | }) 59 | 60 | it('should render Sold Out message', () => { 61 | const { button } = setup(productProps) 62 | expect(button.text()).toMatch(/^Sold Out/) 63 | }) 64 | 65 | it('should disable button', () => { 66 | const { button } = setup(productProps) 67 | expect(button.prop('disabled')).toEqual('disabled') 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /src/features/cart/components/Cart.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Cart from './Cart' 4 | // Importing the view directly here instead of the "default" container 5 | import Product from '../../product/components/Product'; 6 | 7 | const setup = (total, products = []) => { 8 | const actions = { 9 | onCheckoutClicked: jest.fn() 10 | } 11 | 12 | const component = shallow( 13 | 14 | ) 15 | 16 | return { 17 | component: component, 18 | actions: actions, 19 | button: component.find('button'), 20 | products: component.find(Product), 21 | em: component.find('em'), 22 | p: component.find('p') 23 | } 24 | } 25 | 26 | describe('Cart component', () => { 27 | it('should display total', () => { 28 | const { p } = setup('76') 29 | expect(p.text()).toMatch(/^Total: \$76/) 30 | }) 31 | 32 | it('should display add some products message', () => { 33 | const { em } = setup() 34 | expect(em.text()).toMatch(/^Please add some products to cart/) 35 | }) 36 | 37 | it('should disable button', () => { 38 | const { button } = setup() 39 | expect(button.prop('disabled')).toEqual('disabled') 40 | }) 41 | 42 | describe('when given product', () => { 43 | const product = [ 44 | { 45 | id: 1, 46 | title: 'Product 1', 47 | price: 9.99, 48 | quantity: 1 49 | } 50 | ] 51 | 52 | it('should render products', () => { 53 | const { products } = setup('9.99', product) 54 | const props = { 55 | title: product[0].title, 56 | price: product[0].price, 57 | quantity: product[0].quantity 58 | } 59 | 60 | expect(products.at(0).props()).toEqual(props) 61 | }) 62 | 63 | it('should not disable button', () => { 64 | const { button } = setup('9.99', product) 65 | expect(button.prop('disabled')).toEqual('') 66 | }) 67 | 68 | it('should call action on button click', () => { 69 | const { button, actions } = setup('9.99', product) 70 | button.simulate('click') 71 | expect(actions.onCheckoutClicked).toBeCalled() 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/features/product/reducer.spec.js: -------------------------------------------------------------------------------- 1 | import reducer, * as products from './reducer' 2 | 3 | describe('reducers', () => { 4 | describe('products', () => { 5 | let state 6 | 7 | describe('when products are received', () => { 8 | 9 | beforeEach(() => { 10 | state = reducer({}, { 11 | type: 'RECEIVE_PRODUCTS', 12 | products: [ 13 | { 14 | id: 1, 15 | title: 'Product 1', 16 | inventory: 2 17 | }, 18 | { 19 | id: 2, 20 | title: 'Product 2', 21 | inventory: 1 22 | } 23 | ] 24 | }) 25 | }) 26 | 27 | it('contains the products from the action', () => { 28 | expect(products.getProduct(state, 1)).toEqual({ 29 | id: 1, 30 | title: 'Product 1', 31 | inventory: 2 32 | }) 33 | expect(products.getProduct(state, 2)).toEqual({ 34 | id: 2, 35 | title: 'Product 2', 36 | inventory: 1 37 | }) 38 | }) 39 | 40 | it ('contains no other products', () => { 41 | expect(products.getProduct(state, 3)).toEqual(undefined) 42 | }) 43 | 44 | it('lists all of the products as visible', () => { 45 | expect(products.getVisibleProducts(state)).toEqual([ 46 | { 47 | id: 1, 48 | title: 'Product 1', 49 | inventory: 2 50 | }, { 51 | id: 2, 52 | title: 'Product 2', 53 | inventory: 1 54 | } 55 | ]) 56 | }) 57 | 58 | describe('when an item is added to the cart', () => { 59 | 60 | beforeEach(() => { 61 | state = reducer(state, { type: 'ADD_TO_CART', productId: 1 }) 62 | }) 63 | 64 | it('the inventory is reduced', () => { 65 | expect(products.getVisibleProducts(state)).toEqual([ 66 | { 67 | id: 1, 68 | title: 'Product 1', 69 | inventory: 1 70 | }, { 71 | id: 2, 72 | title: 'Product 2', 73 | inventory: 1 74 | } 75 | ]) 76 | }) 77 | 78 | }) 79 | 80 | }) 81 | }) 82 | }) 83 | --------------------------------------------------------------------------------