├── .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 |
--------------------------------------------------------------------------------