Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CIF-1436 - Can't remove or edit items from shopping cart #314

Merged
merged 4 commits into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
******************************************************************************/

import React from 'react';
import { MockedProvider } from '@apollo/react-testing';
import { render, fireEvent } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';

import MUTATION_ADD_COUPON from '../../../queries/mutation_add_coupon.graphql';

import { CartProvider } from '../cartContext';
import CouponForm from '../couponForm';
import i18n from '../../../../__mocks__/i18nForTests';
Expand All @@ -24,9 +27,11 @@ describe('<CouponForm />', () => {
it('renders the component', () => {
const { asFragment } = render(
<I18nextProvider i18n={i18n}>
<CartProvider initialState={{}} reducerFactory={() => state => state}>
<CouponForm />
</CartProvider>
<MockedProvider mocks={[]}>
<CartProvider initialState={{}} reducerFactory={() => state => state}>
<CouponForm />
</CartProvider>
</MockedProvider>
</I18nextProvider>
);
expect(asFragment()).toMatchSnapshot();
Expand All @@ -39,37 +44,13 @@ describe('<CouponForm />', () => {

const { asFragment } = render(
<I18nextProvider i18n={i18n}>
<CartProvider initialState={initialState} reducerFactory={() => state => state}>
<CouponForm />
</CartProvider>
<MockedProvider mocks={[]}>
<CartProvider initialState={initialState} reducerFactory={() => state => state}>
<CouponForm />
</CartProvider>
</MockedProvider>
</I18nextProvider>
);
expect(asFragment()).toMatchSnapshot();
});

it('applies an coupon', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't have E2E tests, I would like to keep the tests that make sure that clicks on "Apply coupon" calls the useCouponForm resp. "Remove coupon" calls the useCouponItem hooks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of implementing some tests for the useXXX "hooks" I added, but you're right, we still need to test the wiring here.

const mockFn = jest.fn();

const initialState = {
addCoupon: mockFn
};

const { getByText, getByPlaceholderText } = render(
<I18nextProvider i18n={i18n}>
<CartProvider initialState={initialState} reducerFactory={() => state => state}>
<CouponForm />
</CartProvider>
</I18nextProvider>
);

// Add coupon to input
fireEvent.change(getByPlaceholderText('Enter your code'), { target: { value: 'my-coupon' } });

// Click on button
fireEvent.click(getByText('Apply Coupon'));

// Expect mock function to be called with coupon
expect(mockFn.mock.calls.length).toEqual(1);
expect(mockFn.mock.calls[0][0]).toBe('my-coupon');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
******************************************************************************/

import React from 'react';
import { MockedProvider } from '@apollo/react-testing';

import { render, fireEvent } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';

Expand All @@ -32,35 +34,13 @@ describe('<CouponItem />', () => {

const { asFragment } = render(
<I18nextProvider i18n={i18n}>
<CartProvider initialState={initialState} reducerFactory={() => state => state}>
<CouponItem />
</CartProvider>
<MockedProvider mocks={[]}>
<CartProvider initialState={initialState} reducerFactory={() => state => state}>
<CouponItem />
</CartProvider>
</MockedProvider>
</I18nextProvider>
);
expect(asFragment()).toMatchSnapshot();
});

it('removes a coupon', () => {
const mockFn = jest.fn();

const initialState = {
removeCoupon: mockFn,
cart: {
applied_coupon: {
code: 'my-sample-coupon'
}
}
};

const { getByText } = render(
<I18nextProvider i18n={i18n}>
<CartProvider initialState={initialState} reducerFactory={() => state => state}>
<CouponItem />
</CartProvider>
</I18nextProvider>
);

fireEvent.mouseDown(getByText('Remove coupon'));
expect(mockFn.mock.calls.length).toEqual(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import React from 'react';
import Product from '../product';
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { MockedProvider } from '@apollo/react-testing';

import { CartProvider } from '../cartContext';
import i18n from '../../../../__mocks__/i18nForTests';
Expand Down Expand Up @@ -48,11 +49,13 @@ describe('<Product />', () => {
const { asFragment } = render(
<I18nextProvider i18n={i18n}>
<CartProvider initialState={{}} reducerFactory={() => state => state}>
<Product
beginEditItem={mockBeginEditItem}
removeItemFromCart={mockRemoveItemFromCart}
item={mockCartItem}
/>
<MockedProvider mocks={[]}>
<Product
beginEditItem={mockBeginEditItem}
removeItemFromCart={mockRemoveItemFromCart}
item={mockCartItem}
/>
</MockedProvider>
</CartProvider>
</I18nextProvider>
);
Expand Down
7 changes: 2 additions & 5 deletions react-components/src/components/Minicart/cartContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ export const initialState = {
cartId: null,
cart: null,
errorMessage: null,
couponError: null,
addItem: () => {},
removeItem: () => {}
couponError: null
};

export const reducerFactory = setCartCookie => {
Expand Down Expand Up @@ -68,8 +66,7 @@ export const reducerFactory = setCartCookie => {
case 'cartId':
return {
...state,
cartId: action.cartId,
...action.methods
cartId: action.cartId
};
case 'reset':
setCartCookie('', 0);
Expand Down
47 changes: 3 additions & 44 deletions react-components/src/components/Minicart/cartInitializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,9 @@
******************************************************************************/
import { useEffect } from 'react';
import { useCartState } from './cartContext';
import { useCookieValue, useAwaitQuery } from '../../utils/hooks';
import { useMutation } from '@apollo/react-hooks';
import { useCookieValue } from '../../utils/hooks';
import { useUserContext } from '../../context/UserContext';

import { removeItemFromCart, addCoupon, removeCoupon } from '../../actions/cart';

import MUTATION_REMOVE_ITEM from '../../queries/mutation_remove_item.graphql';
import MUTATION_ADD_COUPON from '../../queries/mutation_add_coupon.graphql';
import MUTATION_REMOVE_COUPON from '../../queries/mutation_remove_coupon.graphql';
import CART_DETAILS_QUERY from '../../queries/query_cart_details.graphql';

const CartInitializer = props => {
const [{ cartId: stateCartId }, dispatch] = useCartState();
const [{ cartId: registeredCartId }] = useUserContext();
Expand All @@ -32,41 +24,9 @@ const CartInitializer = props => {

const [cartId, setCartCookie] = useCookieValue(CART_COOKIE);

const [removeItemMutation] = useMutation(MUTATION_REMOVE_ITEM);
const [addCouponMutation] = useMutation(MUTATION_ADD_COUPON);
const [removeCouponMutation] = useMutation(MUTATION_REMOVE_COUPON);
const cartDetailsQuery = useAwaitQuery(CART_DETAILS_QUERY);

const createCartHandlers = (cartId, dispatch) => {
return {
removeItem: async itemId => {
dispatch({ type: 'beginLoading' });
await removeItemFromCart({ cartId, itemId, dispatch, cartDetailsQuery, removeItemMutation });
dispatch({ type: 'endLoading' });
},
addCoupon: async couponCode => {
dispatch({ type: 'beginLoading' });
await addCoupon({
cartId,
couponCode,
cartDetailsQuery,
addCouponMutation,
dispatch
});

dispatch({ type: 'endLoading' });
},
removeCoupon: async () => {
dispatch({ type: 'beginLoading' });
await removeCoupon({ cartId, removeCouponMutation, cartDetailsQuery, dispatch });
dispatch({ type: 'endLoading' });
}
};
};

useEffect(() => {
if (cartId && cartId.length > 0 && !stateCartId) {
dispatch({ type: 'cartId', cartId, methods: createCartHandlers(cartId, dispatch) });
dispatch({ type: 'cartId', cartId });
}
}, [cartId]);

Expand All @@ -81,8 +41,7 @@ const CartInitializer = props => {
setCartCookie(registeredCartId);
dispatch({
type: 'cartId',
cartId: registeredCartId,
methods: createCartHandlers(registeredCartId, dispatch)
cartId: registeredCartId
});
}
}, [registeredCartId]);
Expand Down
6 changes: 3 additions & 3 deletions react-components/src/components/Minicart/couponForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../Button';
import { useCartState } from './cartContext';

import classes from './couponForm.css';
import useCouponForm from './useCouponForm';

const CouponForm = () => {
const [{ addCoupon, couponError }] = useCartState();
const [{ couponError }, { addCouponToCart }] = useCouponForm();
const [couponCode, setCouponCode] = useState('');
const [t] = useTranslation('cart');

const addCouponHandler = () => {
return addCoupon(couponCode);
return addCouponToCart(couponCode);
};

const errorFragment = couponError ? <div className={classes.error}>{couponError}</div> : '';
Expand Down
9 changes: 4 additions & 5 deletions react-components/src/components/Minicart/couponItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,24 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import { useCartState } from './cartContext';
import Kebab from './kebab';
import Section from './section';

import classes from './couponItem.css';
import useCouponItem from './useCouponItem';

const CouponItem = () => {
const [{ cart, removeCoupon }] = useCartState();
const [t] = useTranslation('cart');
const [{ appliedCoupon }, { removeCouponFromCart }] = useCouponItem();

const appliedCoupon = cart.applied_coupon ? cart.applied_coupon.code : null;
const [t] = useTranslation('cart');

return (
<div className={classes.root}>
<div className={classes.couponName}>
Coupon <strong>{appliedCoupon}</strong> applied.
</div>
<Kebab>
<Section text={t('cart:remove-coupon', 'Remove coupon')} onClick={removeCoupon} icon="Trash" />
<Section text={t('cart:remove-coupon', 'Remove coupon')} onClick={removeCouponFromCart} icon="Trash" />
</Kebab>
</div>
);
Expand Down
12 changes: 3 additions & 9 deletions react-components/src/components/Minicart/product.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ import makeUrl from '../../utils/makeUrl';
import Kebab from './kebab';
import Section from './section';

import { useCartState } from './cartContext';
import useProduct from './useProduct';

const imageWidth = 80;
const imageHeight = 100;

const Product = props => {
const { item } = props;
const [{ removeItem }, dispatch] = useCartState();
const [t] = useTranslation('cart');

const { product = {}, quantity = 0, id = '', prices } = item;
const { thumbnail, name } = product;
const [, { removeItem, editItem }] = useProduct({ item });

let { price, row_total } = prices;

Expand Down Expand Up @@ -69,13 +69,7 @@ const Product = props => {
</div>
</div>
<Kebab>
<Section
text={t('cart:edit-item', 'Edit item')}
onClick={() => {
dispatch({ type: 'beginEditing', item: item });
}}
icon="Edit2"
/>
<Section text={t('cart:edit-item', 'Edit item')} onClick={editItem} icon="Edit2" />
<Section text={t('cart:remove-item', 'Remove item')} onClick={handleRemoveItem} icon="Trash" />
</Kebab>
</li>
Expand Down
42 changes: 42 additions & 0 deletions react-components/src/components/Minicart/useCouponForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*******************************************************************************
*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
******************************************************************************/
import { useMutation } from '@apollo/react-hooks';

import { useCartState } from './cartContext';
import { addCoupon } from '../../actions/cart';
import { useAwaitQuery } from '../../utils/hooks';

import MUTATION_ADD_COUPON from '../../queries/mutation_add_coupon.graphql';
import CART_DETAILS_QUERY from '../../queries/query_cart_details.graphql';

export default () => {
const [{ cartId, couponError }, dispatch] = useCartState();

const [addCouponMutation] = useMutation(MUTATION_ADD_COUPON);
const cartDetailsQuery = useAwaitQuery(CART_DETAILS_QUERY);

const addCouponToCart = async couponCode => {
dispatch({ type: 'beginLoading' });
await addCoupon({
cartId,
couponCode,
cartDetailsQuery,
addCouponMutation,
dispatch
});

dispatch({ type: 'endLoading' });
};
return [{ couponError }, { addCouponToCart }];
};
Loading