Skip to content

Commit

Permalink
Use graphql to add item to cart (#1987)
Browse files Browse the repository at this point in the history
* Rebase and use graphql invoker in add item to cart

* Update invalid cart test to use message

* Fix test

* Fixup test

* Feedback: Pass single mutation to action and remove unreachable trycatch

* Fix tests

* Disable addToCart if unsupported product type.

* Oops

* Add noCartId retry logic back to addItem

*  Immediately create a new cart after removing the old one.

* Oops

* Handle order completion, cart creation, and receipt state correctly

* Fix tests

* Use addItemMutation for update

* remove unused imports
  • Loading branch information
sirugh authored and dpatil-magento committed Dec 11, 2019
1 parent 96a2e32 commit 8e675f4
Show file tree
Hide file tree
Showing 26 changed files with 357 additions and 254 deletions.
135 changes: 35 additions & 100 deletions packages/peregrine/lib/store/actions/cart/__tests__/asyncActions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
mockRemoveItem,
mockSetItem
} from '../../../../util/simplePersistence';
import checkoutActions from '../../checkout';
import actions from '../actions';
import {
addItemToCart,
Expand Down Expand Up @@ -77,11 +76,10 @@ describe('createCart', () => {
})(...thunkArgs);

expect(mockGetItem).toHaveBeenCalledWith('cartId');
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
expect(dispatch).toHaveBeenNthCalledWith(2, actions.getCart.request());
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, actions.getCart.request());
expect(dispatch).toHaveBeenNthCalledWith(
3,
2,
actions.getCart.receive(storedCartId)
);

Expand All @@ -98,13 +96,12 @@ describe('createCart', () => {
fetchCartId
})(...thunkArgs);

expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
expect(dispatch).toHaveBeenNthCalledWith(2, actions.getCart.request());
expect(dispatch).toHaveBeenNthCalledWith(1, actions.getCart.request());
expect(dispatch).toHaveBeenNthCalledWith(
3,
2,
actions.getCart.receive('CART_ID_FROM_GRAPHQL')
);
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(mockSetItem).toHaveBeenCalledWith(
'cartId',
'CART_ID_FROM_GRAPHQL'
Expand Down Expand Up @@ -138,13 +135,12 @@ describe('createCart', () => {
fetchCartId
})(...thunkArgs);

expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
expect(dispatch).toHaveBeenNthCalledWith(2, actions.getCart.request());
expect(dispatch).toHaveBeenNthCalledWith(1, actions.getCart.request());
expect(dispatch).toHaveBeenNthCalledWith(
3,
2,
actions.getCart.receive(new Error(errors))
);
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledTimes(2);
});

test('its thunk dispatches actions with error on error', async () => {
Expand All @@ -161,54 +157,33 @@ describe('createCart', () => {
fetchCartId
})(...thunkArgs);

expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
expect(dispatch).toHaveBeenNthCalledWith(2, actions.getCart.request());
expect(dispatch).toHaveBeenNthCalledWith(1, actions.getCart.request());
expect(dispatch).toHaveBeenNthCalledWith(
3,
2,
actions.getCart.receive(error)
);
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledTimes(2);
});
});

describe('addItemToCart', () => {
const payload = { item: 'ITEM', quantity: 1 };
const payload = {
item: { sku: 'ITEM' },
quantity: 1,
addItemMutation: jest.fn().mockResolvedValue()
};

test('it returns a thunk', () => {
expect(addItemToCart()).toBeInstanceOf(Function);
});

test('its thunk returns undefined', async () => {
const result = await addItemToCart()(...thunkArgs);
const result = await addItemToCart(payload)(...thunkArgs);

expect(result).toBeUndefined();
});

// test('addItemToCart thunk dispatches actions on success', async () => {
// const payload = { item: 'ITEM', quantity: 1 };
// const cartItem = 'CART_ITEM';

// request.mockResolvedValueOnce(cartItem);
// await addItemToCart(payload)(...thunkArgs);

// expect(dispatch).toHaveBeenNthCalledWith(
// 1,
// actions.addItem.request(payload)
// );
// expect(dispatch).toHaveBeenNthCalledWith(2, expect.any(Function));
// expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
// expect(dispatch).toHaveBeenNthCalledWith(
// 4,
// actions.addItem.receive({ cartItem, ...payload })
// );
// expect(dispatch).toHaveBeenCalledTimes(4);
// });

test('its thunk dispatches actions on success', async () => {
// Test setup.
const cartItem = 'CART_ITEM';
request.mockResolvedValueOnce(cartItem);

// Call the function.
await addItemToCart(payload)(...thunkArgs);

Expand All @@ -225,47 +200,22 @@ describe('addItemToCart', () => {
expect(dispatch).toHaveBeenNthCalledWith(4, actions.addItem.receive());
});

// test('it calls writeImageToCache', async () => {
// writeImageToCache.mockImplementationOnce(() => {});

// await updateItemInCart(payload)(...thunkArgs);

// expect(writeImageToCache).toHaveBeenCalled();
// });

test('its thunk dispatches special failure if cartId is not present', async () => {
getState.mockImplementationOnce(() => ({
cart: {
/* Purposefully no cartId here */
},
user: { isSignedIn: false }
}));

const error = new Error('Missing required information: cartId');
error.noCartId = true;

await addItemToCart(payload)(...thunkArgs);

expect(dispatch).toHaveBeenNthCalledWith(
1,
actions.addItem.request(payload)
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
actions.addItem.receive(error)
);
// createCart
expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
});

test('its thunk tries to recreate a cart on 404 failure', async () => {
test('its thunk tries to recreate a cart on non-network, invalid cart failure', async () => {
const error = new Error('ERROR');
error.response = {
status: 404
};
request.mockRejectedValueOnce(error);
error.networkError = false;
error.graphQLErrors = [
{
message: 'Could not find a cart'
}
];

await addItemToCart(payload)(...thunkArgs);
const customPayload = {
...payload,
addItemMutation: jest.fn().mockRejectedValueOnce(error)
};
await addItemToCart({
...customPayload
})(...thunkArgs);

expect(dispatch).toHaveBeenCalledTimes(9);

Expand All @@ -275,7 +225,7 @@ describe('addItemToCart', () => {

expect(dispatch).toHaveBeenNthCalledWith(
1,
actions.addItem.request(payload)
actions.addItem.request(customPayload)
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
Expand All @@ -293,7 +243,7 @@ describe('addItemToCart', () => {
*/
expect(dispatch).toHaveBeenNthCalledWith(
6,
actions.addItem.request(payload)
actions.addItem.request(customPayload)
);
// getCartDetails
expect(dispatch).toHaveBeenNthCalledWith(7, expect.any(Function));
Expand All @@ -302,21 +252,6 @@ describe('addItemToCart', () => {
// addItem.receive
expect(dispatch).toHaveBeenNthCalledWith(9, actions.addItem.receive());
});

test('its thunk uses the appropriate endpoint when user is signed in', async () => {
getState.mockImplementationOnce(() => ({
cart: { cartId: 'SOME_CART_ID', details: { id: 'HASH_ID' } },
user: { isSignedIn: true }
}));

await addItemToCart(payload)(...thunkArgs);

const authedEndpoint = '/rest/V1/carts/mine/items';
expect(request).toHaveBeenCalledWith(authedEndpoint, {
method: 'POST',
body: expect.any(String)
});
});
});

describe('removeItemFromCart', () => {
Expand Down Expand Up @@ -783,7 +718,7 @@ describe('writeImageToCache', () => {
media_gallery_entries: []
};

await addItemToCart(emptyImages);
await writeImageToCache(emptyImages);

expect(mockGetItem).not.toHaveBeenCalled();
});
Expand Down
59 changes: 21 additions & 38 deletions packages/peregrine/lib/store/actions/cart/asyncActions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Magento2 } from '../../../RestApi';
import BrowserPersistence from '../../../util/simplePersistence';
import { toggleDrawer } from '../app';
import checkoutActions from '../checkout';
import actions from './actions';

const { request } = Magento2;
Expand All @@ -17,10 +16,6 @@ export const createCart = payload =>
return;
}

// reset the checkout workflow
// in case the user has already completed an order this session
dispatch(checkoutActions.reset());

// Request a new cart.
dispatch(actions.getCart.request());

Expand Down Expand Up @@ -52,39 +47,28 @@ export const createCart = payload =>
};

export const addItemToCart = (payload = {}) => {
const { item, fetchCartId } = payload;
const { addItemMutation, fetchCartId, item, quantity, parentSku } = payload;

const writingImageToCache = writeImageToCache(item);

return async function thunk(dispatch, getState) {
await writingImageToCache;
dispatch(actions.addItem.request(payload));

try {
const { cart, user } = getState();
const { isSignedIn } = user;
let cartEndpoint;

if (!isSignedIn) {
const { cartId } = cart;

if (!cartId) {
const missingCartIdError = new Error(
'Missing required information: cartId'
);
missingCartIdError.noCartId = true;
throw missingCartIdError;
}

cartEndpoint = `/rest/V1/guest-carts/${cartId}/items`;
} else {
cartEndpoint = '/rest/V1/carts/mine/items';
}

const quoteId = getQuoteIdForRest(cart, user);
const cartItem = toRESTCartItem(quoteId, payload);
await request(cartEndpoint, {
method: 'POST',
body: JSON.stringify({ cartItem })
const { cart } = getState();
const { cartId } = cart;

const variables = {
cartId,
parentSku,
product: item,
quantity,
sku: item.sku
};

await addItemMutation({
variables
});

// 2019-02-07 Moved these dispatches to the success clause of
Expand All @@ -100,12 +84,12 @@ export const addItemToCart = (payload = {}) => {
await dispatch(toggleDrawer('cart'));
dispatch(actions.addItem.receive());
} catch (error) {
const { response, noCartId } = error;

dispatch(actions.addItem.receive(error));

// check if the cart has expired
if (noCartId || (response && response.status === 404)) {
const shouldRetry = !error.networkError && isInvalidCart(error);

// Only retry if the cart is invalid or the cartId is missing.
if (shouldRetry) {
// Delete the cached ID from local storage and Redux.
// In contrast to the save, make sure storage deletion is
// complete before dispatching the error--you don't want an
Expand Down Expand Up @@ -209,8 +193,7 @@ export const updateItemInCart = (payload = {}) => {
// Add the updated item to that cart.
await dispatch(
addItemToCart({
...payload,
fetchCartId
...payload
})
);
}
Expand Down Expand Up @@ -332,7 +315,7 @@ export const getCartDetails = (payload = {}) => {
// TODO: If we don't have the image in cache we should probably try
// to find it some other way otherwise we have no image to display
// in the cart and will have to fall back to a placeholder.
if (imageCache && Array.isArray(items) && items.length) {
if (Array.isArray(items) && items.length) {
const validTotals = totals && totals.items;
items.forEach(item => {
item.image = item.image || imageCache[item.sku] || {};
Expand Down
Loading

0 comments on commit 8e675f4

Please sign in to comment.