From bd72dd867b321fb31a2c6a27a5b7d8c593a1d9d5 Mon Sep 17 00:00:00 2001 From: Kizito Akhilome Date: Tue, 29 Jan 2019 21:16:04 +0100 Subject: [PATCH] feat(cancel-order): implement cancel order functionality - add button to order card for user to be able to cancel an order - implement order cancellation logic using redux - adequately test out all implementations - improve UX by adding cart count and moving toasts to bottom [Finishes #163354061] --- src/actions/index.js | 14 ++++++ src/actions/types/index.js | 2 + src/components/Nav.jsx | 11 ++++- src/components/OrderCard.jsx | 12 ++++- src/components/OrderHistory.jsx | 33 +++++++++----- src/index.jsx | 2 +- src/reducers/ordersReducer.js | 6 +++ src/tests/actions/orders.test.js | 40 ++++++++++++++++- src/tests/components/OrderHistory.test.jsx | 10 +++++ .../__snapshots__/App.test.jsx.snap | 2 + src/tests/reducers/orders.test.js | 44 +++++++++++++++++-- 11 files changed, 156 insertions(+), 20 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index cb7fef9..69652b0 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -140,6 +140,20 @@ export const getUserOrderHistory = () => async (dispatch) => { } }; +export const cancelOrder = orderId => async (dispatch) => { + dispatch(startFetching()); + try { + await axios.delete(`/orders/${orderId}`); + dispatch({ type: types.CANCEL_ORDER, payload: { orderId } }); + dispatch(stopFetching()); + return toast.success('Order cancelled successfully 🎉'); + } catch (error) { + dispatch({ type: types.CANCEL_ORDER_FAIL }); + dispatch(stopFetching(false, errorResponse(error))); + return toast.error(errorResponse(error)); + } +}; + export const logout = () => (dispatch) => { dispatch(emptyCart()); removeToken(); diff --git a/src/actions/types/index.js b/src/actions/types/index.js index 0fa6824..0db8a14 100644 --- a/src/actions/types/index.js +++ b/src/actions/types/index.js @@ -16,4 +16,6 @@ export default { EMPTY_CART: 'EMPTY_CART', GET_ORDER_HISTORY: 'GET_ORDER_HISTORY', GET_ORDER_HISTORY_FAIL: 'GET_ORDER_HISTORY_FAIL', + CANCEL_ORDER: 'CANCEL_ORDER', + CANCEL_ORDER_FAIL: 'CANCEL_ORDER_FAIL', }; diff --git a/src/components/Nav.jsx b/src/components/Nav.jsx index cd2912c..800957a 100644 --- a/src/components/Nav.jsx +++ b/src/components/Nav.jsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import LogoutLink from './Logout'; -export const Nav = ({ isLoggedIn, role }) => { +export const Nav = ({ isLoggedIn, role, cartItemsCount }) => { const navContent = () => { if (isLoggedIn && role === 'customer') { return ( @@ -13,7 +13,11 @@ export const Nav = ({ isLoggedIn, role }) => { Menu
  • - Cart + +Cart [ + {cartItemsCount} +] +
  • Order History @@ -69,15 +73,18 @@ export const Nav = ({ isLoggedIn, role }) => { Nav.propTypes = { isLoggedIn: PropTypes.bool, role: PropTypes.string.isRequired, + cartItemsCount: PropTypes.number, }; Nav.defaultProps = { isLoggedIn: false, + cartItemsCount: 0, }; const mapStateToProps = state => ({ isLoggedIn: state.user.isLoggedIn, role: state.user.role, + cartItemsCount: Object.keys(state.cart).length, }); export default connect(mapStateToProps)(Nav); diff --git a/src/components/OrderCard.jsx b/src/components/OrderCard.jsx index dcd7bc4..bcb364d 100644 --- a/src/components/OrderCard.jsx +++ b/src/components/OrderCard.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; const OrderCard = ({ - foodItems, orderPrice, date, orderStatus, + foodItems, orderPrice, date, orderStatus, cancelOrderCallback, }) => { const formattedFoodNames = foodItems.map(foodName => (

    {foodName}

    @@ -48,7 +48,14 @@ const OrderCard = ({ .join('/')}

    -
    +
    +
    +
    + +
    +
    ); }; @@ -58,6 +65,7 @@ OrderCard.propTypes = { orderPrice: PropTypes.number.isRequired, date: PropTypes.string.isRequired, orderStatus: PropTypes.string.isRequired, + cancelOrderCallback: PropTypes.func.isRequired, }; export default OrderCard; diff --git a/src/components/OrderHistory.jsx b/src/components/OrderHistory.jsx index 8586952..f6758a6 100644 --- a/src/components/OrderHistory.jsx +++ b/src/components/OrderHistory.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import OrderCard from './OrderCard'; -import { getUserOrderHistory } from '../actions'; +import { getUserOrderHistory, cancelOrder } from '../actions'; export class OrderHistory extends Component { async componentDidMount() { @@ -10,21 +10,31 @@ export class OrderHistory extends Component { await getOrderHistory(); } + onCancelOrder = async (orderId) => { + const { cancelOrder: deleteOrder } = this.props; + await deleteOrder(Number(orderId)); + }; + render() { const { orders } = this.props; return (
    - {orders.map(order => ( - - ))} + {orders.length ? ( + orders.map(order => ( + this.onCancelOrder(order.id)} + /> + )) + ) : ( +

    No Orders Yet!

    + )}
    ); @@ -34,6 +44,7 @@ export class OrderHistory extends Component { OrderHistory.propTypes = { getUserOrderHistory: PropTypes.func.isRequired, orders: PropTypes.instanceOf(Array).isRequired, + cancelOrder: PropTypes.func.isRequired, }; const mapStateToProps = state => ({ @@ -42,5 +53,5 @@ const mapStateToProps = state => ({ export default connect( mapStateToProps, - { getUserOrderHistory }, + { getUserOrderHistory, cancelOrder }, )(OrderHistory); diff --git a/src/index.jsx b/src/index.jsx index 6d61893..e83ca88 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -15,7 +15,7 @@ const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk))); const jsx = ( - + ); diff --git a/src/reducers/ordersReducer.js b/src/reducers/ordersReducer.js index 794586f..8bc5d24 100644 --- a/src/reducers/ordersReducer.js +++ b/src/reducers/ordersReducer.js @@ -8,7 +8,13 @@ export default (state = intialState, action) => { switch (action.type) { case types.GET_ORDER_HISTORY: return { ...intialState, history: action.payload.orders }; + case types.CANCEL_ORDER: + return { + ...state, + history: state.history.filter(order => order.id !== action.payload.orderId), + }; case types.GET_ORDER_HISTORY_FAIL: + case types.CANCEL_ORDER_FAIL: default: return state; } diff --git a/src/tests/actions/orders.test.js b/src/tests/actions/orders.test.js index 2fc30c7..055f716 100644 --- a/src/tests/actions/orders.test.js +++ b/src/tests/actions/orders.test.js @@ -1,7 +1,9 @@ -import { getUserOrderHistory } from '../../actions'; +import { getUserOrderHistory, cancelOrder } from '../../actions'; import jwt from '../../utils/jwt'; import axios from '../../services/axios'; +afterAll(() => jest.restoreAllMocks()); + describe('getUserOrderHistory', () => { jest.spyOn(jwt, 'decode').mockImplementation(() => ({ userId: 1 })); const dispatch = jest.fn(); @@ -35,3 +37,39 @@ describe('getUserOrderHistory', () => { }); }); }); + +describe('cancelOrder()', () => { + const dispatch = jest.fn(); + const errResponse = { response: { data: { message: 'failed to cancel order' } } }; + afterEach(() => jest.resetAllMocks()); + + it('should dispatch correct action to cancel an order', async () => { + jest.spyOn(axios, 'delete').mockImplementation(() => Promise.resolve()); + await cancelOrder(7)(dispatch); + + expect(dispatch).toHaveBeenCalledTimes(3); + expect(dispatch.mock.calls[1][0]).toEqual({ + type: 'CANCEL_ORDER', + payload: { orderId: 7 }, + }); + expect(dispatch).toHaveBeenLastCalledWith({ + type: 'STOP_FETCHING', + payload: { error: false, message: '' }, + }); + }); + + it('should dispatch correct action for order cancellation failure', async () => { + jest.spyOn(axios, 'delete').mockImplementation(() => Promise.reject(errResponse)); + await cancelOrder(2)(dispatch); + + expect(dispatch).toHaveBeenCalledTimes(3); + expect(dispatch.mock.calls[1][0]).toEqual({ type: 'CANCEL_ORDER_FAIL' }); + expect(dispatch).toHaveBeenLastCalledWith({ + type: 'STOP_FETCHING', + payload: { + error: true, + message: 'failed to cancel order', + }, + }); + }); +}); diff --git a/src/tests/components/OrderHistory.test.jsx b/src/tests/components/OrderHistory.test.jsx index ec98abe..3d02e0d 100644 --- a/src/tests/components/OrderHistory.test.jsx +++ b/src/tests/components/OrderHistory.test.jsx @@ -37,6 +37,7 @@ describe('', () => { const props = { orders, getUserOrderHistory: jest.fn(), + cancelOrder: jest.fn(), }; it('should render correctly', () => { @@ -47,4 +48,13 @@ describe('', () => { expect(orderCards.length).toEqual(4); wrapper.unmount(); }); + + it('should fire cancel order action creator upon order cancellation request', () => { + const wrapper = mount(); + const fourthOrderCard = wrapper.find('.order-card').last(); + fourthOrderCard.find('.cancel-order > button').simulate('click'); + + expect(props.cancelOrder).toHaveBeenCalled(); + expect(props.cancelOrder).toHaveBeenCalledWith(4); + }); }); diff --git a/src/tests/components/__snapshots__/App.test.jsx.snap b/src/tests/components/__snapshots__/App.test.jsx.snap index bf6db8b..8c67d5c 100644 --- a/src/tests/components/__snapshots__/App.test.jsx.snap +++ b/src/tests/components/__snapshots__/App.test.jsx.snap @@ -55,6 +55,7 @@ exports[`App component should render app correctly 1`] = ` >