From 5ad63a749e24919e06282cead8ae78a8e2fb17fb Mon Sep 17 00:00:00 2001
From: Maryna <49429739+MarynaVozniuk@users.noreply.github.com>
Date: Tue, 23 Apr 2019 01:44:26 +0300
Subject: [PATCH 1/4] Header logo width changed on as other header elements
have (#1129)
---
packages/venia-concept/src/components/Header/header.css | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/venia-concept/src/components/Header/header.css b/packages/venia-concept/src/components/Header/header.css
index 3554a6bcd9..80e3f558d7 100755
--- a/packages/venia-concept/src/components/Header/header.css
+++ b/packages/venia-concept/src/components/Header/header.css
@@ -41,6 +41,7 @@
.logo {
grid-area: title;
+ width: 3rem;
}
.primaryActions {
From 3c0e7ed3378cf870cdc54537fcd1e84461484533 Mon Sep 17 00:00:00 2001
From: Andy Terranova <13182778+supernova-at@users.noreply.github.com>
Date: Tue, 23 Apr 2019 12:35:14 -0500
Subject: [PATCH 2/4] Authenticated Users (#968)
* Changes guestCartId to cartId. Adds endpoints for signed in users.
* Updates receipt to hide create account prompt for signed in users
* Fixes bug where cartId is expected to be a String, not a number
* Fixes for missing test and missed refactor naming
* Add casting back
* Adds reset action to cart
* Removes direct call to assignGuestCartToCustomer
* Completes the authorized customer checkout flow
* Fixes cart bug after customer signs out
* Adds unit tests for signIn and signOut user action
* Adds unit tests for removeCart function
* Updates documentation.
* Renames createGuestCart to createCart in documentation and comments
* Adds and uses clearToken function for consistency
* Adds workaround for price zero bug
---
.../src/RestApi/Magento2/MulticastCache.js | 2 +-
.../src/RootComponents/Product/Product.js | 10 +-
.../actions/cart/__tests__/actions.spec.js | 28 +-
.../cart/__tests__/asyncActions.spec.js | 1538 ++++++++---------
.../venia-concept/src/actions/cart/actions.js | 4 +-
.../src/actions/cart/asyncActions.js | 408 ++---
.../checkout/__tests__/asyncActions.spec.js | 53 +-
.../src/actions/checkout/asyncActions.js | 150 +-
.../user/__tests__/asyncActions.spec.js | 158 +-
.../src/actions/user/asyncActions.js | 63 +-
.../Receipt/__tests__/receipt.spec.js | 14 +-
.../components/Checkout/Receipt/receipt.js | 38 +-
.../Checkout/__tests__/form.spec.js | 2 +-
.../src/components/Checkout/flow.js | 16 +-
.../src/components/Checkout/form.js | 4 +-
.../src/components/Checkout/wrapper.js | 24 +-
.../src/components/MiniCart/miniCart.js | 2 +-
.../src/components/SignIn/container.js | 4 +-
.../src/components/SignIn/signIn.js | 5 +-
packages/venia-concept/src/index.js | 3 +-
.../src/reducers/__tests__/cart.spec.js | 16 +-
packages/venia-concept/src/reducers/cart.js | 15 +-
.../features/checkout/index.md | 7 +-
23 files changed, 1294 insertions(+), 1270 deletions(-)
diff --git a/packages/peregrine/src/RestApi/Magento2/MulticastCache.js b/packages/peregrine/src/RestApi/Magento2/MulticastCache.js
index b9d6e4d33a..2bebfe4c62 100644
--- a/packages/peregrine/src/RestApi/Magento2/MulticastCache.js
+++ b/packages/peregrine/src/RestApi/Magento2/MulticastCache.js
@@ -4,7 +4,7 @@
* Resource matching is determined by a string composite [method, path, body].
*
* (M2ApiRequests know not to use this cache for create operations, except for
- * singleton create operations like createGuestCart, which have no body.)
+ * singleton create operations like createCart, which have no body.)
* @module MulticastCache
*/
diff --git a/packages/venia-concept/src/RootComponents/Product/Product.js b/packages/venia-concept/src/RootComponents/Product/Product.js
index ee98c08ed4..e62ca6a447 100644
--- a/packages/venia-concept/src/RootComponents/Product/Product.js
+++ b/packages/venia-concept/src/RootComponents/Product/Product.js
@@ -1,4 +1,5 @@
import React, { Component } from 'react';
+import { string, func } from 'prop-types';
import { connect, Query } from 'src/drivers';
import { addItemToCart } from 'src/actions/cart';
@@ -15,9 +16,14 @@ import productQuery from 'src/queries/getProductDetail.graphql';
* TODO: Replace with a single product query when possible.
*/
class Product extends Component {
+ static propTypes = {
+ addItemToCart: func.isRequired,
+ cartId: string
+ };
+
addToCart = async (item, quantity) => {
- const { guestCartId } = this.props;
- await this.props.addItemToCart({ guestCartId, item, quantity });
+ const { addItemToCart, cartId } = this.props;
+ await addItemToCart({ cartId, item, quantity });
};
componentDidMount() {
diff --git a/packages/venia-concept/src/actions/cart/__tests__/actions.spec.js b/packages/venia-concept/src/actions/cart/__tests__/actions.spec.js
index 1f4e595c21..0175d5fd21 100644
--- a/packages/venia-concept/src/actions/cart/__tests__/actions.spec.js
+++ b/packages/venia-concept/src/actions/cart/__tests__/actions.spec.js
@@ -61,32 +61,28 @@ test('removeItem.receive() returns a proper action object', () => {
});
});
-test('getGuestCart.request.toString() returns the proper action type', () => {
- expect(actions.getGuestCart.request.toString()).toBe(
- 'CART/GET_GUEST_CART/REQUEST'
- );
+test('getCart.request.toString() returns the proper action type', () => {
+ expect(actions.getCart.request.toString()).toBe('CART/GET_CART/REQUEST');
});
-test('getGuestCart.request() returns a proper action object', () => {
- expect(actions.getGuestCart.request(payload)).toEqual({
- type: 'CART/GET_GUEST_CART/REQUEST',
+test('getCart.request() returns a proper action object', () => {
+ expect(actions.getCart.request(payload)).toEqual({
+ type: 'CART/GET_CART/REQUEST',
payload
});
});
-test('getGuestCart.receive.toString() returns the proper action type', () => {
- expect(actions.getGuestCart.receive.toString()).toBe(
- 'CART/GET_GUEST_CART/RECEIVE'
- );
+test('getCart.receive.toString() returns the proper action type', () => {
+ expect(actions.getCart.receive.toString()).toBe('CART/GET_CART/RECEIVE');
});
-test('getGuestCart.receive() returns a proper action object', () => {
- expect(actions.getGuestCart.receive(payload)).toEqual({
- type: 'CART/GET_GUEST_CART/RECEIVE',
+test('getCart.receive() returns a proper action object', () => {
+ expect(actions.getCart.receive(payload)).toEqual({
+ type: 'CART/GET_CART/RECEIVE',
payload
});
- expect(actions.getGuestCart.receive(error)).toEqual({
- type: 'CART/GET_GUEST_CART/RECEIVE',
+ expect(actions.getCart.receive(error)).toEqual({
+ type: 'CART/GET_CART/RECEIVE',
payload: error,
error: true
});
diff --git a/packages/venia-concept/src/actions/cart/__tests__/asyncActions.spec.js b/packages/venia-concept/src/actions/cart/__tests__/asyncActions.spec.js
index 985c5c5341..bffcae5f59 100644
--- a/packages/venia-concept/src/actions/cart/__tests__/asyncActions.spec.js
+++ b/packages/venia-concept/src/actions/cart/__tests__/asyncActions.spec.js
@@ -12,9 +12,11 @@ import {
addItemToCart,
updateItemInCart,
removeItemFromCart,
- createGuestCart,
+ createCart,
getCartDetails,
- toggleCart
+ removeCart,
+ toggleCart,
+ writeImageToCache
} from '../asyncActions';
jest.mock('src/store');
@@ -25,7 +27,7 @@ const { request } = RestApi.Magento2;
beforeAll(() => {
getState.mockImplementation(() => ({
app: { drawer: null },
- cart: { guestCartId: 'GUEST_CART_ID' },
+ cart: { cartId: 'CART_ID' },
user: { isSignedIn: false }
}));
});
@@ -43,896 +45,836 @@ afterAll(() => {
getState.mockRestore();
});
-test('createGuestCart() returns a thunk', () => {
- expect(createGuestCart()).toBeInstanceOf(Function);
-});
+describe('createCart', () => {
+ test('it returns a thunk', () => {
+ expect(createCart()).toBeInstanceOf(Function);
+ });
-test('createGuestCart thunk returns undefined', async () => {
- const result = await createGuestCart()(...thunkArgs);
+ test('its thunk returns undefined', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: {},
+ user: { isSignedIn: false }
+ }));
- expect(result).toBeUndefined();
-});
+ const result = await createCart()(...thunkArgs);
-test('createGuestCart thunk does nothing if a guest cart exists in state', async () => {
- await createGuestCart()(...thunkArgs);
+ expect(result).toBeUndefined();
+ });
- expect(dispatch).not.toHaveBeenCalled();
- expect(request).not.toHaveBeenCalled();
-});
+ test('its thunk earlies out if a cartId already exists in state', async () => {
+ await createCart()(...thunkArgs);
-test('createGuestCart thunk uses the guest cart from storage', async () => {
- const storedGuestCartId = 'STORED_GUEST_CART_ID';
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
- mockGetItem.mockImplementationOnce(() => storedGuestCartId);
-
- await createGuestCart()(...thunkArgs);
-
- expect(mockGetItem).toHaveBeenCalledWith('guestCartId');
- expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.getGuestCart.receive(storedGuestCartId)
- );
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(request).not.toHaveBeenCalled();
-});
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(request).not.toHaveBeenCalled();
+ });
-test('createGuestCart thunk dispatches actions on success', async () => {
- const response = 'NEW_GUEST_CART_ID';
+ test('its thunk uses the cart from storage', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: {},
+ user: { isSignedIn: false }
+ }));
+ const storedCartId = 'STORED_CART_ID';
+ mockGetItem.mockImplementationOnce(() => storedCartId);
+
+ await createCart()(...thunkArgs);
+
+ expect(mockGetItem).toHaveBeenCalledWith('cartId');
+ expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.getCart.receive(storedCartId)
+ );
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ expect(request).not.toHaveBeenCalled();
+ });
- request.mockResolvedValueOnce(response);
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
+ test('its thunk dispatches actions on success', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: {},
+ user: { isSignedIn: false }
+ }));
+ const response = 'NEW_CART_ID';
+ request.mockResolvedValueOnce(response);
+
+ await createCart()(...thunkArgs);
+
+ expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
+ expect(dispatch).toHaveBeenNthCalledWith(2, actions.getCart.request());
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 3,
+ actions.getCart.receive(response)
+ );
+ expect(dispatch).toHaveBeenCalledTimes(3);
+ expect(mockSetItem).toHaveBeenCalledWith('cartId', response);
+ });
- await createGuestCart()(...thunkArgs);
+ test('its thunk calls the appropriate endpoints when user is signed in', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: {},
+ user: { isSignedIn: true }
+ }));
+ const response = 'NEW_CART_ID';
+ request.mockResolvedValueOnce(response);
- expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
- expect(dispatch).toHaveBeenNthCalledWith(2, actions.getGuestCart.request());
- expect(dispatch).toHaveBeenNthCalledWith(
- 3,
- actions.getGuestCart.receive(response)
- );
- expect(dispatch).toHaveBeenCalledTimes(3);
- expect(mockSetItem).toHaveBeenCalled();
-});
+ await createCart()(...thunkArgs);
-test('createGuestCart thunk dispatches actions on failure', async () => {
- const error = new Error('ERROR');
+ expect(request).toHaveBeenCalledTimes(2);
- request.mockRejectedValueOnce(error);
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
- mockGetItem.mockImplementationOnce(() => {});
+ const authedEndpoint = '/rest/V1/carts/mine';
+ expect(request).toHaveBeenNthCalledWith(1, authedEndpoint, {
+ method: 'POST'
+ });
- await createGuestCart()(...thunkArgs);
+ const billingEndpoint = '/rest/V1/carts/mine/billing-address';
+ expect(request).toHaveBeenNthCalledWith(2, billingEndpoint, {
+ method: 'POST',
+ body: JSON.stringify({
+ address: {},
+ cartId: response
+ })
+ });
+ });
- expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
- expect(dispatch).toHaveBeenNthCalledWith(2, actions.getGuestCart.request());
- expect(dispatch).toHaveBeenNthCalledWith(
- 3,
- actions.getGuestCart.receive(error)
- );
- expect(dispatch).toHaveBeenCalledTimes(3);
+ test('its thunk dispatches actions on failure', async () => {
+ mockGetItem.mockImplementationOnce(() => {});
+ getState.mockImplementationOnce(() => ({
+ cart: {},
+ user: { isSignedIn: false }
+ }));
+ const error = new Error('ERROR');
+ request.mockRejectedValueOnce(error);
+
+ await createCart()(...thunkArgs);
+
+ expect(dispatch).toHaveBeenNthCalledWith(1, checkoutActions.reset());
+ expect(dispatch).toHaveBeenNthCalledWith(2, actions.getCart.request());
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 3,
+ actions.getCart.receive(error)
+ );
+ expect(dispatch).toHaveBeenCalledTimes(3);
+ });
});
-test('addItemToCart() returns a thunk', () => {
- expect(addItemToCart()).toBeInstanceOf(Function);
-});
+describe('addItemToCart', () => {
+ const payload = { item: 'ITEM', quantity: 1 };
-test('addItemToCart thunk returns undefined', async () => {
- const result = await addItemToCart()(...thunkArgs);
+ test('it returns a thunk', () => {
+ expect(addItemToCart()).toBeInstanceOf(Function);
+ });
- expect(result).toBeUndefined();
-});
+ test('its thunk returns undefined', async () => {
+ const result = await addItemToCart()(...thunkArgs);
-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);
-});
+ expect(result).toBeUndefined();
+ });
-test('addItemToCart thunk skips image cache if no sku or image', async () => {
- const noSku = {
- quantity: 1,
- item: {
- media_gallery_entries: [
- {
- position: 1,
- url: 'http://example.com'
- }
- ]
- }
- };
- await addItemToCart(noSku)(...thunkArgs);
- expect(mockGetItem).not.toHaveBeenCalled;
- const noImages = {
- quantity: 1,
- item: {
- sku: 'INVISIBLE'
- }
- };
- await addItemToCart(noImages)(...thunkArgs);
- expect(mockGetItem).not.toHaveBeenCalled;
- const emptyImages = {
- quantity: 1,
- item: {
- sku: 'INVISIBLE',
- media_gallery_entries: []
- }
- };
- await addItemToCart(emptyImages)(...thunkArgs);
- expect(mockGetItem).not.toHaveBeenCalled;
-});
+ // 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);
+
+ // Make assertions.
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.addItem.request(payload)
+ );
+ // getCartDetails
+ expect(dispatch).toHaveBeenNthCalledWith(2, expect.any(Function));
+ // toggleDrawer
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 4,
+ actions.addItem.receive({
+ cartItem,
+ item: payload.item,
+ quantity: payload.quantity
+ })
+ );
+ expect(dispatch).toHaveBeenCalledTimes(4);
+ });
-test('addItemToCart stores product images in local cache for use in cart', async () => {
- const itemWithImages = {
- quantity: 1,
- item: {
- sku: 'HELLO',
- media_gallery_entries: [
- {
- position: 2,
- url: 'http://example.com/second'
- },
- {
- position: 1,
- url: 'http://example.com/first'
- }
- ]
- }
- };
- await addItemToCart(itemWithImages)(...thunkArgs);
- expect(mockGetItem).toHaveBeenCalledWith('imagesBySku');
- expect(mockSetItem).toHaveBeenCalledWith(
- 'imagesBySku',
- expect.objectContaining({
- HELLO: { position: 1, url: 'http://example.com/first' }
- })
- );
-
- const itemWithUnpositionedImages = {
- quantity: 1,
- item: {
- sku: 'GOODBYE',
- media_gallery_entries: [
- {
- url: 'http://example.com'
- }
- ]
- }
- };
- await addItemToCart(itemWithUnpositionedImages)(...thunkArgs);
- expect(mockGetItem).toHaveBeenCalledTimes(2);
- expect(mockSetItem).toHaveBeenCalledWith(
- 'imagesBySku',
- expect.objectContaining({
- GOODBYE: { url: 'http://example.com' }
- })
- );
-});
+ // test('it calls writeImageToCache', async () => {
+ // writeImageToCache.mockImplementationOnce(() => {});
-test('addItemToCart reuses product images from cache', async () => {
- const sameItem = {
- sku: 'SAME_ITEM',
- media_gallery_entries: [{ url: 'http://example.com/same/item' }]
- };
- const fakeImageCache = {};
- mockGetItem.mockReturnValueOnce(fakeImageCache);
- await addItemToCart({ quantity: 1, item: sameItem })(...thunkArgs);
- mockGetItem.mockReturnValueOnce(fakeImageCache);
- expect(mockSetItem).toHaveBeenCalledTimes(1);
- await addItemToCart({ quantity: 4, item: sameItem })(...thunkArgs);
- expect(mockSetItem).toHaveBeenCalledTimes(1);
-});
+ // await updateItemInCart(payload)(...thunkArgs);
-test('addItemToCart thunk dispatches special failure if guestCartId is not present', async () => {
- const payload = {
- item: { sku: 'ITEM_SKU', name: 'ITEM_NAME' },
- quantity: 1
- };
- const error = new Error('Missing required information: guestCartId');
- error.noGuestCartId = true;
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
- await addItemToCart(payload)(...thunkArgs);
- expect(dispatch).toHaveBeenNthCalledWith(
- 1,
- actions.addItem.request(payload)
- );
- expect(dispatch).toHaveBeenNthCalledWith(2, actions.addItem.receive(error));
- // and now, the the createGuestCart thunk
- expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
-});
+ // expect(writeImageToCache).toHaveBeenCalled();
+ // });
-test('addItemToCart tries to recreate a guest cart on 404 failure', async () => {
- getState
- .mockImplementationOnce(() => ({
- cart: { guestCartId: 'OLD_AND_BUSTED' },
- user: { isSignedIn: false }
- }))
- .mockImplementationOnce(() => ({
- cart: { guestCartId: 'CACHED_CART' },
- user: { isSignedIn: false }
- }))
- .mockImplementationOnce(() => ({
- cart: {},
+ test('its thunk dispatches special failure if cartId is not present', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: {
+ /* Purposefully no cartId here */
+ },
user: { isSignedIn: false }
}));
- const payload = { item: 'ITEM', quantity: 1 };
- const error = new Error('ERROR');
- error.response = {
- status: 404
- };
- // image cache
- mockGetItem.mockResolvedValueOnce('CACHED_CART');
- request.mockRejectedValueOnce(error);
+ 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));
+ });
- await addItemToCart(payload)(...thunkArgs);
+ test('its thunk tries to recreate a cart on 404 failure', async () => {
+ const error = new Error('ERROR');
+ error.response = {
+ status: 404
+ };
+ request.mockRejectedValueOnce(error);
+
+ 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));
+
+ // And then the thunk is called again.
+
+ expect(request).toHaveBeenCalledTimes(2);
+ });
- expect(dispatch.mock.calls).toMatchObject([
- [
- {
- payload: {
- item: 'ITEM',
- quantity: 1
- },
- type: 'CART/ADD_ITEM/REQUEST'
- }
- ],
- [
- {
- error: true,
- payload: expect.any(Error),
- type: 'CART/ADD_ITEM/RECEIVE'
- }
- ],
- [expect.any(Function)],
- [
- {
- payload: {
- item: 'ITEM',
- quantity: 1
- },
- type: 'CART/ADD_ITEM/REQUEST'
- }
- ],
- [expect.any(Function)],
- [expect.any(Function)],
- [
- {
- payload: {
- cartItem: undefined,
- item: 'ITEM',
- quantity: 1
- },
- type: 'CART/ADD_ITEM/RECEIVE'
- }
- ]
- ]);
-});
+ test('its thunk uses the appropriate endpoint when user is signed in', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: { cartId: 'SOME_CART_ID' },
+ user: { isSignedIn: true }
+ }));
-test('addItemToCart opens drawer and gets cart details on success', async () => {
- const payload = { item: 'ITEM', quantity: 1 };
- const fakeCart = {
- cart: { guestCartId: 'NEW_GUEST_CART_ID' },
- user: { isSignedIn: false }
- };
- const cartItem = 'CART_ITEM';
-
- getState.mockReturnValueOnce(fakeCart).mockReturnValueOnce(fakeCart);
- const fakeDispatch = fn =>
- typeof fn === 'function' && fn(dispatch, getState);
- dispatch
- .mockImplementationOnce(fakeDispatch)
- .mockImplementationOnce(fakeDispatch)
- .mockImplementationOnce(fakeDispatch)
- .mockImplementationOnce(fakeDispatch)
- .mockImplementationOnce(fakeDispatch)
- .mockImplementationOnce(fakeDispatch);
-
- request.mockResolvedValueOnce(cartItem).mockResolvedValueOnce(cartItem);
- await addItemToCart(payload)(...thunkArgs);
-
- expect(getState).toHaveBeenCalledTimes(3);
- expect(dispatch).toHaveBeenCalledTimes(7);
- expect(request).toHaveBeenCalledTimes(4);
-});
+ await addItemToCart(payload)(...thunkArgs);
-test('removeItemFromCart() returns a thunk', () => {
- expect(removeItemFromCart({})).toBeInstanceOf(Function);
+ const authedEndpoint = '/rest/V1/carts/mine/items';
+ expect(request).toHaveBeenCalledWith(authedEndpoint, {
+ method: 'POST',
+ body: expect.any(String)
+ });
+ });
});
-test('removeItemFromCart thunk returns undefined', async () => {
- const result = await removeItemFromCart({})(...thunkArgs);
+describe('removeItemFromCart', () => {
+ const payload = { item: { item_id: 1 } };
- expect(result).toBeUndefined();
-});
+ test('it returns a thunk', () => {
+ expect(removeItemFromCart(payload)).toBeInstanceOf(Function);
+ });
-test('updateItemInCart() returns a thunk', () => {
- expect(updateItemInCart()).toBeInstanceOf(Function);
-});
+ test('its thunk returns undefined', async () => {
+ const result = await removeItemFromCart(payload)(...thunkArgs);
-test('updateItemInCart thunk returns undefined', async () => {
- const result = await updateItemInCart()(...thunkArgs);
+ expect(result).toBeUndefined();
+ });
- expect(result).toBeUndefined();
-});
+ test('its thunk dispatches actions on success', async () => {
+ const response = 1;
+ request.mockResolvedValueOnce(response);
+
+ await removeItemFromCart(payload)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(3);
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.removeItem.request(payload)
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.removeItem.receive({
+ cartItem: response,
+ item: payload.item,
+ cartItemCount: 0
+ })
+ );
+ // getCartDetails
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+ });
-test('updateItemInCart thunk dispatches actions on success', async () => {
- const payload = { item: 'ITEM', quantity: 1 };
- const cartItem = 'CART_ITEM';
-
- request.mockResolvedValueOnce(cartItem);
- await updateItemInCart(payload)(...thunkArgs);
-
- expect(dispatch).toHaveBeenNthCalledWith(
- 1,
- actions.updateItem.request(payload)
- );
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.updateItem.receive({ cartItem, ...payload })
- );
- expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
- expect(dispatch).toHaveBeenNthCalledWith(4, expect.any(Function));
- // Additional dispatch occurs to close the options drawer
- expect(dispatch).toHaveBeenCalledTimes(5);
-});
+ test('its thunk dispatches special failure if cartId is not present', async () => {
+ getState.mockImplementationOnce(() => ({ cart: {} }));
-test('updateItemInCart thunk skips image cache if no sku or image', async () => {
- const noSku = {
- quantity: 1,
- item: {
- media_gallery_entries: [
- {
- position: 1,
- url: 'http://example.com'
- }
- ]
- }
- };
- await updateItemInCart(noSku)(...thunkArgs);
- expect(mockGetItem).not.toHaveBeenCalled;
+ const error = new Error('Missing required information: cartId');
+ error.noCartId = true;
- const noImages = {
- quantity: 1,
- item: {
- sku: 'INVISIBLE'
- }
- };
- await updateItemInCart(noImages)(...thunkArgs);
- expect(mockGetItem).not.toHaveBeenCalled;
+ await removeItemFromCart(payload)(...thunkArgs);
- const emptyImages = {
- quantity: 1,
- item: {
- sku: 'INVISIBLE',
- media_gallery_entries: []
- }
- };
- await updateItemInCart(emptyImages)(...thunkArgs);
- expect(mockGetItem).not.toHaveBeenCalled;
-});
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.removeItem.request(payload)
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.removeItem.receive(error)
+ );
+ // createCart
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
-test('updateItemInCart stores product images in local cache for use in cart', async () => {
- const itemWithImages = {
- quantity: 1,
- item: {
- sku: 'HELLO',
- media_gallery_entries: [
- {
- position: 2,
- url: 'http://example.com/second'
- },
- {
- position: 1,
- url: 'http://example.com/first'
- }
- ]
- }
- };
- await updateItemInCart(itemWithImages)(...thunkArgs);
- expect(mockGetItem).toHaveBeenCalledWith('imagesBySku');
- expect(mockSetItem).toHaveBeenCalledWith(
- 'imagesBySku',
- expect.objectContaining({
- HELLO: { position: 1, url: 'http://example.com/first' }
- })
- );
-
- const itemWithUnpositionedImages = {
- quantity: 1,
- item: {
- sku: 'GOODBYE',
- media_gallery_entries: [
- {
- url: 'http://example.com'
- }
- ]
- }
- };
- await updateItemInCart(itemWithUnpositionedImages)(...thunkArgs);
- expect(mockGetItem).toHaveBeenCalledTimes(2);
- expect(mockSetItem).toHaveBeenCalledWith(
- 'imagesBySku',
- expect.objectContaining({
- GOODBYE: { url: 'http://example.com' }
- })
- );
-});
+ expect(mockRemoveItem).toHaveBeenCalledWith('cartId');
+ });
-test('updateItemInCart reuses product images from cache', async () => {
- const sameItem = {
- sku: 'SAME_ITEM',
- media_gallery_entries: [{ url: 'http://example.com/same/item' }]
- };
- const fakeImageCache = {};
+ test('its thunk tries to recreate a cart on 404 failure', async () => {
+ const error = new Error('ERROR');
+ error.response = {
+ status: 404
+ };
+ request.mockRejectedValueOnce(error);
+
+ await removeItemFromCart(payload)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.removeItem.request(payload)
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.removeItem.receive(error)
+ );
+ // createCart
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+
+ // And then the thunk is called again.
+
+ expect(request).toHaveBeenCalledTimes(2);
+ });
- mockGetItem.mockReturnValueOnce(fakeImageCache);
- await updateItemInCart({ quantity: 1, item: sameItem })(...thunkArgs);
- mockGetItem.mockReturnValueOnce(fakeImageCache);
- expect(mockSetItem).toHaveBeenCalledTimes(1);
+ test('its thunk clears the cartId when removing the last item in the cart', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: { cartId: 'CART', details: { items_count: 1 } },
+ user: { isSignedIn: false }
+ }));
- await updateItemInCart({ quantity: 4, item: sameItem })(...thunkArgs);
- expect(mockSetItem).toHaveBeenCalledTimes(1);
+ await removeItemFromCart(payload)(...thunkArgs);
+
+ expect(mockRemoveItem).toHaveBeenCalledWith('cartId');
+ });
+
+ test('its thunk uses the proper endpoint when user is signed in', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: { cartId: 'UNIT_TEST' },
+ user: { isSignedIn: true }
+ }));
+
+ await removeItemFromCart(payload)(...thunkArgs);
+
+ const authedEndpoint = `/rest/V1/carts/mine/items/${
+ payload.item.item_id
+ }`;
+ expect(request).toHaveBeenCalledWith(authedEndpoint, {
+ method: 'DELETE'
+ });
+ });
});
-test('updateItemInCart thunk dispatches special failure if guestCartId is not present', async () => {
+describe('updateItemInCart', () => {
const payload = {
- item: { sku: 'ITEM_SKU', name: 'ITEM_NAME' },
+ item: { item_id: 1 },
quantity: 1
};
- const error = new Error('Missing required information: guestCartId');
- error.noGuestCartId = true;
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
- await updateItemInCart(payload)(...thunkArgs);
- expect(dispatch).toHaveBeenNthCalledWith(
- 1,
- actions.updateItem.request(payload)
- );
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.updateItem.receive(error)
- );
- // and now, the createGuestCart thunk
- expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
-});
+ const targetItemId = 2;
-test('updateItemInCart tries to recreate a guest cart on 404 failure', async () => {
- getState
- .mockImplementationOnce(() => ({
- cart: { guestCartId: 'OLD_AND_BUSTED' },
- user: { isSignedIn: false }
- }))
- .mockImplementationOnce(() => ({
- cart: { guestCartId: 'CACHED_CART' },
- user: { isSignedIn: false }
- }))
- .mockImplementationOnce(() => ({
- cart: {},
+ test('it returns a thunk', () => {
+ expect(updateItemInCart(payload, targetItemId)).toBeInstanceOf(
+ Function
+ );
+ });
+
+ test('its thunk returns undefined', async () => {
+ const result = await updateItemInCart(payload, targetItemId)(
+ ...thunkArgs
+ );
+
+ expect(result).toBeUndefined();
+ });
+
+ test('its thunk dispatches actions on success', async () => {
+ const response = 7;
+ request.mockResolvedValueOnce(response);
+
+ await updateItemInCart(payload, targetItemId)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(4);
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.updateItem.request(payload)
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.updateItem.receive({
+ cartItem: response,
+ item: payload.item,
+ quantity: payload.quantity
+ })
+ );
+ // getCartDetails
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+ // closeOptionsDrawer
+ expect(dispatch).toHaveBeenNthCalledWith(4, expect.any(Function));
+ });
+
+ // test('it calls writeImageToCache', async () => {
+ // writeImageToCache.mockImplementationOnce(() => {});
+
+ // await updateItemInCart(payload, targetItemId)(...thunkArgs);
+
+ // expect(writeImageToCache).toHaveBeenCalled();
+ // });
+
+ test('its thunk dispatches special failure if cartId is not present', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: {
+ /* cartId is purposefully not present */
+ },
user: { isSignedIn: false }
}));
- const payload = { item: 'ITEM', quantity: 1 };
- const error = new Error('ERROR');
- error.response = {
- status: 404
- };
- // image cache
- mockGetItem.mockResolvedValueOnce('CACHED_CART');
- request.mockRejectedValueOnce(error);
+ const error = new Error('Missing required information: cartId');
+ error.noCartId = true;
+
+ await updateItemInCart(payload, targetItemId)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.updateItem.request(payload)
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.updateItem.receive(error)
+ );
+ // createCart
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+ });
- await updateItemInCart(payload)(...thunkArgs);
+ test('its thunk tries to recreate a cart on 404 failure', async () => {
+ const error = new Error('ERROR');
+ error.response = {
+ status: 404
+ };
+ request.mockRejectedValueOnce(error);
+
+ await updateItemInCart(payload, targetItemId)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.updateItem.request(payload)
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.updateItem.receive(error)
+ );
+ // createCart
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+
+ // And then the thunk is called again.
+
+ expect(request).toHaveBeenCalledTimes(2);
+ });
- expect(dispatch.mock.calls).toMatchObject([
- [
- {
- payload: {
- item: 'ITEM',
- quantity: 1
- },
- type: 'CART/UPDATE_ITEM/REQUEST'
- }
- ],
- [
- {
- error: true,
- payload: expect.any(Error),
- type: 'CART/UPDATE_ITEM/RECEIVE'
- }
- ],
- [expect.any(Function)],
- [
- {
- payload: {
- item: 'ITEM',
- quantity: 1
- },
- type: 'CART/UPDATE_ITEM/REQUEST'
- }
- ],
- [
- {
- payload: {
- cartItem: undefined,
- item: 'ITEM',
- quantity: 1
- },
- type: 'CART/UPDATE_ITEM/RECEIVE'
- }
- ],
- [expect.any(Function)],
- [expect.any(Function)],
- [expect.any(Function)]
- ]);
-});
+ test('its thunk uses the proper endpoint when the user is signed in', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: { cartId: 'UNIT_TEST' },
+ user: { isSignedIn: true }
+ }));
-test('removeItemFromCart thunk dispatches actions on success', async () => {
- const payload = { item: 'ITEM' };
- const cartItem = 'CART_ITEM';
-
- request.mockResolvedValueOnce(cartItem);
- await removeItemFromCart(payload)(...thunkArgs);
-
- expect(dispatch).toHaveBeenNthCalledWith(
- 1,
- actions.removeItem.request(payload)
- );
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.removeItem.receive({ cartItem, cartItemCount: 0, ...payload })
- );
- expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
- expect(dispatch).toHaveBeenCalledTimes(3);
-});
+ await updateItemInCart(payload, targetItemId)(...thunkArgs);
-test('removeItemFromCart thunk dispatches special failure if guestCartId is not present', async () => {
- const payload = { item: 'ITEM' };
- const error = new Error('Missing required information: guestCartId');
- error.noGuestCartId = true;
- getState.mockImplementationOnce(() => ({ cart: {} }));
- await removeItemFromCart(payload)(...thunkArgs);
- expect(mockRemoveItem).toHaveBeenCalledWith('guestCartId');
- expect(dispatch).toHaveBeenNthCalledWith(
- 1,
- actions.removeItem.request(payload)
- );
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.removeItem.receive(error)
- );
- expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+ const authedEndpoint = `/rest/V1/carts/mine/items/${targetItemId}`;
+ expect(request).toHaveBeenCalledWith(authedEndpoint, {
+ method: 'PUT',
+ body: expect.any(String)
+ });
+ });
});
-test('removeItemFromCart tries to recreate a guest cart on 404 failure', async () => {
- getState.mockImplementationOnce(() => ({
- cart: { guestCartId: 'OLD_AND_BUSTED' }
- }));
- const payload = { item: 'ITEM' };
- const error = new Error('ERROR');
- error.response = {
- status: 404
- };
+describe('getCartDetails', () => {
+ const payload = { forceRefresh: true };
- request.mockRejectedValueOnce(error);
+ test('it returns a thunk', () => {
+ expect(getCartDetails(payload)).toBeInstanceOf(Function);
+ });
- await removeItemFromCart(payload)(...thunkArgs);
+ test('its thunk returns undefined', async () => {
+ const result = await getCartDetails(payload)(...thunkArgs);
- expect(request).toHaveBeenCalledTimes(2);
-});
+ expect(result).toBeUndefined();
+ });
-test('removeItemFromCart resets the guest cart when removing the last item in the cart', async () => {
- getState.mockImplementationOnce(() => ({
- cart: { guestCartId: 'CART', details: { items_count: 1 } }
- }));
- const payload = { item: 'ITEM' };
+ test('its thunk creates a cart if no id is found', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: {
+ /* cartId purposefully not present */
+ },
+ user: { isSignedIn: false }
+ }));
- // removeItemFromCart() calls storage.removeItem() to clear the guestCartId
- // but only if there's 1 item left in the cart
- mockRemoveItem.mockImplementationOnce(() => {});
+ await getCartDetails(payload)(...thunkArgs);
- await removeItemFromCart(payload)(...thunkArgs);
+ // createCart
+ expect(dispatch).toHaveBeenNthCalledWith(1, expect.any(Function));
+ });
- expect(mockRemoveItem).toHaveBeenCalled();
-});
+ test('its thunk dispatches actions on success', async () => {
+ const mockDetails = { items: [] };
+ request
+ // fetchCartPart (details)
+ .mockResolvedValueOnce(mockDetails)
+ // fetchCartPart (payment methods)
+ .mockResolvedValueOnce(2)
+ // fetchCartPart (totals)
+ .mockResolvedValueOnce(3);
+
+ await getCartDetails(payload)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.getDetails.request('CART_ID')
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.getDetails.receive({
+ details: mockDetails,
+ paymentMethods: 2,
+ totals: 3
+ })
+ );
+ });
-test('getCartDetails() returns a thunk', () => {
- expect(getCartDetails()).toBeInstanceOf(Function);
-});
+ test('its thunk dispatches actions on failure', async () => {
+ const error = new Error('ERROR');
+ request.mockRejectedValueOnce(error);
+
+ await getCartDetails(payload)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.getDetails.request('CART_ID')
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.getDetails.receive(error)
+ );
+ });
-test('getCartDetails thunk returns undefined', async () => {
- getState.mockImplementationOnce(() => ({
- user: { isSignedIn: false },
- cart: { guestCartId: 'NEW_GUEST_CART_ID' }
- }));
+ test('its thunk tries to recreate a cart on 404 failure', async () => {
+ const error = new Error('ERROR');
+ error.response = {
+ status: 404
+ };
+ request.mockRejectedValueOnce(error);
+
+ await getCartDetails(payload)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.getDetails.request('CART_ID')
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.getDetails.receive(error)
+ );
+ // createCart
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+
+ // And then the thunk is called again.
+
+ // three (3) fetchCartParts x two (2) thunk calls (initial, then the retry) = 6.
+ expect(request).toHaveBeenCalledTimes(6);
+ });
+
+ test('its thunk merges cached item images into details', async () => {
+ // Mock getting the image cache from storage.
+ const cache = { SKU_1: 'IMAGE_1' };
+ mockGetItem.mockResolvedValueOnce(cache);
+
+ const items = [
+ { image: 'IMAGE_0', sku: 'SKU_0' },
+ { sku: 'SKU_1' },
+ { sku: 'SKU_2' }
+ ];
+ const expected = [
+ items[0],
+ { ...items[1], image: cache.SKU_1, options: [] },
+ { ...items[2], image: {}, options: [] }
+ ];
+ const mockDetails = { items };
+ request
+ // fetchCartPart (details)
+ .mockResolvedValueOnce(mockDetails)
+ // fetchCartPart (payment methods)
+ .mockResolvedValueOnce(2)
+ // fetchCartPart (totals)
+ .mockResolvedValueOnce(3);
+
+ await getCartDetails(payload)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.getDetails.request('CART_ID')
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.getDetails.receive({
+ details: { items: expected },
+ paymentMethods: 2,
+ totals: 3
+ })
+ );
+ });
- const result = await getCartDetails()(...thunkArgs);
+ test('its thunk uses the proper endpoint when the user is signed in', async () => {
+ getState.mockImplementationOnce(() => ({
+ cart: { cartId: 'UNIT_TEST' },
+ user: { isSignedIn: true }
+ }));
- expect(result).toBeUndefined();
+ await getCartDetails(payload)(...thunkArgs);
+
+ const authedEndpoints = {
+ details: '/rest/V1/carts/mine/',
+ paymentMethods: '/rest/V1/carts/mine/payment-methods',
+ totals: '/rest/V1/carts/mine/totals'
+ };
+ const cacheArg = expect.any(Object);
+ expect(request).toHaveBeenNthCalledWith(
+ 1,
+ authedEndpoints.details,
+ cacheArg
+ );
+ expect(request).toHaveBeenNthCalledWith(
+ 2,
+ authedEndpoints.paymentMethods,
+ cacheArg
+ );
+ expect(request).toHaveBeenNthCalledWith(
+ 3,
+ authedEndpoints.totals,
+ cacheArg
+ );
+ });
});
-test('getCartDetails thunk creates a guest cart if no ID is found', async () => {
- getState.mockClear();
- getState
- // for the getCartDetails state check
- .mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }))
- // for the createGuestCart check
- .mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }))
- // for the subsequent getCartDetails re-check
- .mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
- dispatch
- // for not dispatching the sync notifier action
- .mockImplementationOnce(() => {})
- // for actually dispatching the createGuestCart action
- .mockImplementationOnce(fn => fn(...thunkArgs));
- mockGetItem.mockImplementationOnce(() => {});
- request
- // for createGuestCart
- .mockResolvedValueOnce('GUEST_CART_ID')
- // for getCartDetails
- .mockResolvedValueOnce({
- id: 1,
- guestCartId: 'GUEST_CART_ID',
- items: []
- });
+describe('removeCart', () => {
+ test('it returns a thunk', () => {
+ expect(removeCart()).toBeInstanceOf(Function);
+ });
+
+ test('its thunk returns undefined', async () => {
+ const result = await removeCart()(...thunkArgs);
+
+ expect(result).toBeUndefined();
+ });
- await getCartDetails()(...thunkArgs);
+ test('it clears the cartId from local storage', async () => {
+ await removeCart()(...thunkArgs);
- expect(getState).toHaveBeenCalledTimes(4);
- expect(mockGetItem).toHaveBeenCalled();
- const createCallArgs = request.mock.calls[0];
- const retrieveCallArgs = request.mock.calls[1];
- expect(createCallArgs[0]).toBe('/rest/V1/guest-carts');
- expect(createCallArgs[1]).toHaveProperty('method', 'POST');
- expect(retrieveCallArgs[0]).toBe('/rest/V1/guest-carts/GUEST_CART_ID/');
+ expect(mockRemoveItem).toHaveBeenCalledWith('cartId');
+ });
+
+ test('it clears the cart from the redux store', async () => {
+ await removeCart()(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ expect(dispatch).toHaveBeenCalledWith(actions.reset());
+ });
});
-test('getCartDetails thunk deletes an old cart id and recreates a guest cart if cart ID is expired', async () => {
- const tempStorage = {};
- mockSetItem.mockImplementation((key, value) => {
- tempStorage[key] = value;
+describe('toggleCart', () => {
+ test('it returns a thunk', () => {
+ expect(toggleCart()).toBeInstanceOf(Function);
});
- mockGetItem.mockImplementation(key => {
- return tempStorage[key];
+
+ test('its thunk returns undefined', async () => {
+ const result = await toggleCart()(...thunkArgs);
+
+ expect(result).toBeUndefined();
});
- getState
- // for the getCartDetails state check
- .mockImplementationOnce(() => ({
- cart: { guestCartId: 'EXPIRED_CART_ID' },
- user: { isSignedIn: false }
- }))
- // for the createGuestCart check
- .mockImplementationOnce(() => ({
- cart: { guestCartId: tempStorage.guestCartId },
- user: { isSignedIn: false }
- }))
- // for the subsequent createGuestCart check
- .mockImplementationOnce(() => ({
- cart: { guestCartId: null },
- user: { isSignedIn: false }
- }))
- // for the subsequent getCartDetails re-check
- .mockImplementationOnce(() => ({
- cart: { guestCartId: 'BRAND_NEW_CART' },
- user: { isSignedIn: false }
+
+ test('its thunk exits if app state is not present', async () => {
+ getState.mockImplementationOnce(() => ({
+ /* app is purposefully not present */
+ cart: {}
}));
- dispatch
- // for not dispatching the sync notifier action
- .mockImplementationOnce(() => {})
- // for not dispatching the create notifier action
- .mockImplementationOnce(() => {})
- // for actually dispatching the createGuestCart action
- .mockImplementationOnce(fn => fn(...thunkArgs));
- request
- // for for getting expired cart
- .mockRejectedValueOnce({ response: { status: 404 } })
- // for getting expired cart payment methods
- .mockRejectedValueOnce({ response: { status: 404 } })
- // for for getting expired cart totals
- .mockRejectedValueOnce({ response: { status: 404 } })
- // for createNewCart
- .mockResolvedValueOnce('BRAND_NEW_CART')
- // for getCartDetails
- .mockResolvedValueOnce({
- id: 1,
- guestCartId: 'BRAND_NEW_CART',
- items: [{ sku: 'SKU', name: 'NAME', image: 'IMAGE' }]
- });
- await getCartDetails()(...thunkArgs);
-
- expect(getState).toHaveBeenCalledTimes(4);
- expect(mockGetItem).toHaveBeenCalledWith('imagesBySku');
- expect(mockRemoveItem).toHaveBeenCalledWith('guestCartId');
- expect(mockSetItem).toHaveBeenCalledWith('guestCartId', 'BRAND_NEW_CART');
- const [
- retrieveExpiredCallArgs,
- retrieveExpiredPaymentMethodsArgs,
- retrieveExpiredTotalsArgs,
- createCallArgs,
- retrieveCallArgs
- ] = request.mock.calls;
- expect(retrieveExpiredCallArgs[0]).toBe(
- '/rest/V1/guest-carts/EXPIRED_CART_ID/'
- );
- expect(retrieveExpiredPaymentMethodsArgs[0]).toBe(
- '/rest/V1/guest-carts/EXPIRED_CART_ID/payment-methods'
- );
- expect(retrieveExpiredTotalsArgs[0]).toBe(
- '/rest/V1/guest-carts/EXPIRED_CART_ID/totals'
- );
- expect(createCallArgs[0]).toBe('/rest/V1/guest-carts');
- expect(createCallArgs[1]).toHaveProperty('method', 'POST');
- expect(retrieveCallArgs[0]).toBe('/rest/V1/guest-carts/BRAND_NEW_CART/');
-});
+ await toggleCart()(...thunkArgs);
-test('getCartDetails thunk dispatches actions on success', async () => {
- getState.mockImplementationOnce(() => ({
- cart: { guestCartId: 'GUEST_CART_ID' },
- user: { isSignedIn: false }
- }));
- // For getting details.
- request.mockResolvedValueOnce(1);
- // For getting payment methods.
- request.mockResolvedValueOnce(2);
- // For getting totals.
- request.mockResolvedValueOnce(3);
-
- await getCartDetails()(...thunkArgs);
-
- expect(dispatch).toHaveBeenNthCalledWith(
- 1,
- actions.getDetails.request('GUEST_CART_ID')
- );
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.getDetails.receive({ details: 1, paymentMethods: 2, totals: 3 })
- );
- expect(dispatch).toHaveBeenCalledTimes(2);
-});
+ expect(dispatch).not.toHaveBeenCalled();
+ });
-test('getCartDetails thunk dispatches actions on failure', async () => {
- getState.mockImplementationOnce(() => ({
- cart: { guestCartId: 'GUEST_CART_ID' },
- user: { isSignedIn: false }
- }));
- const error = new Error('ERROR');
- request.mockRejectedValueOnce(error);
-
- await getCartDetails()(...thunkArgs);
-
- expect(dispatch).toHaveBeenNthCalledWith(
- 1,
- actions.getDetails.request('GUEST_CART_ID')
- );
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.getDetails.receive(error)
- );
- expect(dispatch).toHaveBeenCalledTimes(2);
-});
+ test('its thunk exits if cart state is not present', async () => {
+ getState.mockImplementationOnce(() => ({
+ app: {}
+ /* cart is purposefully not present */
+ }));
-test('getCartDetails thunk merges cached item images into details', async () => {
- const cache = { SKU_1: 'IMAGE_1' };
- const items = [
- { image: 'IMAGE_0', sku: 'SKU_0' },
- { sku: 'SKU_1' },
- { sku: 'SKU_2' }
- ];
- const expected = [
- items[0],
- { ...items[1], image: cache.SKU_1, options: [] },
- { ...items[2], image: {}, options: [] }
- ];
-
- mockGetItem.mockResolvedValueOnce(cache);
- // For getting details.
- request.mockResolvedValueOnce({ items });
- // For getting payment methods.
- request.mockResolvedValueOnce(2);
- // For getting totals.
- request.mockResolvedValueOnce(3);
-
- await getCartDetails()(...thunkArgs);
-
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.getDetails.receive({
- details: { items: expected },
- paymentMethods: 2,
- totals: 3
- })
- );
-});
+ await toggleCart()(...thunkArgs);
-test('toggleCart() returns a thunk', () => {
- expect(toggleCart()).toBeInstanceOf(Function);
-});
+ expect(dispatch).not.toHaveBeenCalled();
+ });
-test('toggleCart thunk returns undefined', async () => {
- const result = await toggleCart()(...thunkArgs);
+ test('its thunk closes the drawer if it is open', async () => {
+ getState.mockImplementationOnce(() => ({
+ app: { drawer: 'cart' },
+ cart: {}
+ }));
- expect(result).toBeUndefined();
-});
+ await toggleCart()(...thunkArgs);
-test('toggleCart thunk exits if app state is not present', async () => {
- getState.mockImplementationOnce(() => ({
- cart: {},
- user: { isSignedIn: false }
- }));
+ const closeDrawer = expect.any(Function);
+ expect(dispatch).toHaveBeenNthCalledWith(1, closeDrawer);
+ });
- await toggleCart()(...thunkArgs);
+ test('its thunk opens the drawer and refreshes the cart', async () => {
+ await toggleCart()(...thunkArgs);
- expect(dispatch).not.toHaveBeenCalled();
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ const toggleDrawer = expect.any(Function);
+ expect(dispatch).toHaveBeenNthCalledWith(1, toggleDrawer);
+ const getCartDetails = expect.any(Function);
+ expect(dispatch).toHaveBeenNthCalledWith(2, getCartDetails);
+ });
});
-test('toggleCart thunk exits if cart state is not present', async () => {
- getState.mockImplementationOnce(() => ({
- app: {},
- user: { isSignedIn: false }
- }));
+describe('writeImageToCache', () => {
+ test('it does nothing when the item has no sku', async () => {
+ const noSku = {
+ media_gallery_entries: [
+ {
+ position: 1,
+ url: 'http://example.com'
+ }
+ ]
+ };
- await toggleCart()(...thunkArgs);
+ await writeImageToCache(noSku);
- expect(dispatch).not.toHaveBeenCalled();
-});
+ expect(mockGetItem).not.toHaveBeenCalled();
+ });
+
+ test('it does nothing when the item is missing entries', async () => {
+ const noImages = {
+ sku: 'INVISIBLE'
+ /* media_gallery_entries is purposefully omitted */
+ };
-test('toggleCart thunk opens the drawer and refreshes the cart', async () => {
- await toggleCart()(...thunkArgs);
+ await writeImageToCache(noImages);
- expect(dispatch).toHaveBeenNthCalledWith(1, expect.any(Function));
- expect(dispatch).toHaveBeenNthCalledWith(2, expect.any(Function));
- expect(dispatch).toHaveBeenCalledTimes(2);
-});
+ expect(mockGetItem).not.toHaveBeenCalled();
+ });
+
+ test('it does nothing when the item has zero entries', async () => {
+ const emptyImages = {
+ sku: 'INVISIBLE',
+ media_gallery_entries: []
+ };
+
+ await addItemToCart(emptyImages);
-test('toggleCart thunk closes the drawer', async () => {
- getState.mockReturnValueOnce({ app: { drawer: 'cart' }, cart: {} });
- await toggleCart()(...thunkArgs);
+ expect(mockGetItem).not.toHaveBeenCalled();
+ });
- expect(dispatch).toHaveBeenNthCalledWith(1, expect.any(Function));
- expect(dispatch).toHaveBeenCalledTimes(1);
+ test('it stores product images in local cache when they have positions', async () => {
+ const item = {
+ sku: 'HELLO',
+ media_gallery_entries: [
+ {
+ position: 2,
+ url: 'http://example.com/second'
+ },
+ {
+ position: 1,
+ url: 'http://example.com/first'
+ }
+ ]
+ };
+
+ await writeImageToCache(item);
+
+ expect(mockGetItem).toHaveBeenCalledWith('imagesBySku');
+ expect(mockSetItem).toHaveBeenCalledWith(
+ 'imagesBySku',
+ expect.objectContaining({
+ HELLO: { position: 1, url: 'http://example.com/first' }
+ })
+ );
+ });
+
+ test('it stores product images when they do not have positions', async () => {
+ // With unpositioned images.
+ const itemWithUnpositionedImages = {
+ sku: 'GOODBYE',
+ media_gallery_entries: [
+ {
+ url: 'http://example.com'
+ }
+ ]
+ };
+
+ await writeImageToCache(itemWithUnpositionedImages);
+
+ expect(mockGetItem).toHaveBeenCalledWith('imagesBySku');
+ expect(mockSetItem).toHaveBeenCalledWith(
+ 'imagesBySku',
+ expect.objectContaining({
+ GOODBYE: { url: 'http://example.com' }
+ })
+ );
+ });
+
+ test('it reuses product images from cache', async () => {
+ const sameItem = {
+ sku: 'SAME_ITEM',
+ media_gallery_entries: [{ url: 'http://example.com/same/item' }]
+ };
+
+ const fakeImageCache = {};
+ mockGetItem
+ .mockReturnValueOnce(fakeImageCache)
+ .mockReturnValueOnce(fakeImageCache);
+
+ await writeImageToCache(sameItem);
+ expect(mockSetItem).toHaveBeenCalledTimes(1);
+
+ await writeImageToCache(sameItem);
+ // mockSetItem should still have only been called once.
+ expect(mockSetItem).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/packages/venia-concept/src/actions/cart/actions.js b/packages/venia-concept/src/actions/cart/actions.js
index 8a9be86009..a62660dcc8 100644
--- a/packages/venia-concept/src/actions/cart/actions.js
+++ b/packages/venia-concept/src/actions/cart/actions.js
@@ -7,7 +7,7 @@ const actionMap = {
REQUEST: null,
RECEIVE: null
},
- GET_GUEST_CART: {
+ GET_CART: {
REQUEST: null,
RECEIVE: null
},
@@ -25,6 +25,6 @@ const actionMap = {
}
};
-const actionTypes = ['OPEN_OPTIONS_DRAWER', 'CLOSE_OPTIONS_DRAWER'];
+const actionTypes = ['CLOSE_OPTIONS_DRAWER', 'OPEN_OPTIONS_DRAWER', 'RESET'];
export default createActions(actionMap, ...actionTypes, { prefix });
diff --git a/packages/venia-concept/src/actions/cart/asyncActions.js b/packages/venia-concept/src/actions/cart/asyncActions.js
index f33a1bc72f..ecad990dc2 100644
--- a/packages/venia-concept/src/actions/cart/asyncActions.js
+++ b/packages/venia-concept/src/actions/cart/asyncActions.js
@@ -8,12 +8,12 @@ const { request } = RestApi.Magento2;
const { BrowserPersistence } = Util;
const storage = new BrowserPersistence();
-export const createGuestCart = () =>
+export const createCart = () =>
async function thunk(dispatch, getState) {
- const { cart } = getState();
+ const { cart, user } = getState();
- // if a guest cart already exists, exit
- if (cart.guestCartId) {
+ // if a cart already exists in the store, exit
+ if (cart.cartId) {
return;
}
@@ -21,89 +21,84 @@ export const createGuestCart = () =>
// in case the user has already completed an order this session
dispatch(checkoutActions.reset());
- const guestCartId = await retrieveGuestCartId();
-
- // if a guest cart exists in storage, act like we just received it
- if (guestCartId) {
- dispatch(actions.getGuestCart.receive(guestCartId));
+ // if a cart exists in storage, act like we just received it
+ const cartId = await retrieveCartId();
+ if (cartId) {
+ dispatch(actions.getCart.receive(cartId));
return;
}
- // otherwise, request a new guest cart
- dispatch(actions.getGuestCart.request());
+ // otherwise, request a new cart
+ dispatch(actions.getCart.request());
try {
- const id = await request('/rest/V1/guest-carts', {
+ const guestCartEndpoint = '/rest/V1/guest-carts';
+ const signedInCartEndpoint = '/rest/V1/carts/mine';
+ const cartEndpoint = user.isSignedIn
+ ? signedInCartEndpoint
+ : guestCartEndpoint;
+
+ const cartId = await request(cartEndpoint, {
method: 'POST'
});
// write to storage in the background
- saveGuestCartId(id);
- dispatch(actions.getGuestCart.receive(id));
+ saveCartId(cartId);
+
+ // There is currently an issue in Magento 2
+ // where the first item added to an empty cart for an
+ // authenticated customer gets added with a price of zero.
+ // @see https://github.com/magento/magento2/issues/2991
+ // This workaround is in place until that issue is resolved.
+ if (user.isSignedIn) {
+ await request('/rest/V1/carts/mine/billing-address', {
+ method: 'POST',
+ body: JSON.stringify({
+ address: {},
+ cartId
+ })
+ });
+ }
+
+ dispatch(actions.getCart.receive(cartId));
} catch (error) {
- dispatch(actions.getGuestCart.receive(error));
+ dispatch(actions.getCart.receive(error));
}
};
export const addItemToCart = (payload = {}) => {
- const { item, options, parentSku, productType, quantity } = payload;
+ const { item, quantity } = payload;
const writingImageToCache = writeImageToCache(item);
return async function thunk(dispatch, getState) {
await writingImageToCache;
dispatch(actions.addItem.request(payload));
- const { user } = getState();
- if (user.isSignedIn) {
- // TODO: handle authed carts
- // if a user creates an account,
- // then the guest cart will be transferred to their account
- // causing `/guest-carts` to 400
- return;
- }
-
try {
- const { cart } = getState();
- const { guestCartId } = cart;
+ const { cart, user } = getState();
+ const { cartId } = cart;
- if (!guestCartId) {
- const missingGuestCartError = new Error(
- 'Missing required information: guestCartId'
+ if (!cartId) {
+ const missingCartIdError = new Error(
+ 'Missing required information: cartId'
);
- missingGuestCartError.noGuestCartId = true;
- throw missingGuestCartError;
+ missingCartIdError.noCartId = true;
+ throw missingCartIdError;
}
- // TODO: change to GraphQL mutation
- // for now, manually transform the payload for REST
- const itemPayload = {
- qty: quantity,
- sku: item.sku,
- name: item.name,
- quote_id: guestCartId
- };
-
- if (productType === 'ConfigurableProduct') {
- Object.assign(itemPayload, {
- sku: parentSku,
- product_type: 'configurable',
- product_option: {
- extension_attributes: {
- configurable_item_options: options
- }
- }
- });
- }
+ const cartItem = toRESTCartItem(cartId, payload);
- const cartItem = await request(
- `/rest/V1/guest-carts/${guestCartId}/items`,
- {
- method: 'POST',
- body: JSON.stringify({
- cartItem: itemPayload
- })
- }
- );
+ const { isSignedIn } = user;
+ const guestCartEndpoint = `/rest/V1/guest-carts/${cartId}/items`;
+ const signedInCartEndpoint = '/rest/V1/carts/mine/items';
+ const cartEndpoint = isSignedIn
+ ? signedInCartEndpoint
+ : guestCartEndpoint;
+
+ const response = await request(cartEndpoint, {
+ method: 'POST',
+ body: JSON.stringify({ cartItem })
+ });
// 2019-02-07 Moved these dispatches to the success clause of
// addItemToCart. The cart should only open on success.
@@ -111,21 +106,23 @@ export const addItemToCart = (payload = {}) => {
// so a successful retry will wind up here anyway.
await dispatch(getCartDetails({ forceRefresh: true }));
await dispatch(toggleDrawer('cart'));
- dispatch(actions.addItem.receive({ cartItem, item, quantity }));
+ dispatch(
+ actions.addItem.receive({ cartItem: response, item, quantity })
+ );
} catch (error) {
- const { response, noGuestCartId } = error;
+ const { response, noCartId } = error;
dispatch(actions.addItem.receive(error));
// check if the guest cart has expired
- if (noGuestCartId || (response && response.status === 404)) {
+ if (noCartId || (response && response.status === 404)) {
// if so, then delete the cached ID...
// in contrast to the save, make sure storage deletion is
// complete before dispatching the error--you don't want an
// upstream action to try and reuse the known-bad ID.
- await clearGuestCartId();
+ await clearCartId();
// then create a new one
- await dispatch(createGuestCart());
+ await dispatch(createCart());
// then retry this operation
return thunk(...arguments);
}
@@ -134,92 +131,73 @@ export const addItemToCart = (payload = {}) => {
};
export const updateItemInCart = (payload = {}, targetItemId) => {
- const { item, options, parentSku, productType, quantity } = payload;
+ const { item, quantity } = payload;
const writingImageToCache = writeImageToCache(item);
return async function thunk(dispatch, getState) {
await writingImageToCache;
dispatch(actions.updateItem.request(payload));
- const { user } = getState();
- if (user.isSignedIn) {
- // TODO: handle authed carts
- // if a user creates an account,
- // then the guest cart will be transferred to their account
- // causing `/guest-carts` to 400
- return;
- }
-
try {
- const { cart } = getState();
- const { guestCartId } = cart;
+ const { cart, user } = getState();
+ const { cartId } = cart;
- if (!guestCartId) {
- const missingGuestCartError = new Error(
- 'Missing required information: guestCartId'
+ if (!cartId) {
+ const missingCartIdError = new Error(
+ 'Missing required information: cartId'
);
- missingGuestCartError.noGuestCartId = true;
- throw missingGuestCartError;
+ missingCartIdError.noCartId = true;
+ throw missingCartIdError;
}
- // TODO: change to GraphQL mutation
- // for now, manually transform the payload for REST
- const itemPayload = {
- qty: quantity,
- sku: item.sku,
- name: item.name,
- quote_id: guestCartId
- };
-
- if (productType === 'ConfigurableProduct') {
- Object.assign(itemPayload, {
- sku: parentSku,
- product_type: 'configurable',
- product_option: {
- extension_attributes: {
- configurable_item_options: options
- }
- }
- });
- }
+ const cartItem = toRESTCartItem(cartId, payload);
- const cartItem = await request(
- `/rest/V1/guest-carts/${guestCartId}/items/${targetItemId}`,
- {
- method: 'PUT',
- body: JSON.stringify({
- cartItem: itemPayload
- })
- }
- );
+ const { isSignedIn } = user;
+ const guestCartEndpoint = `/rest/V1/guest-carts/${cartId}/items/${targetItemId}`;
+ const signedInCartEndpoint = `/rest/V1/carts/mine/items/${targetItemId}`;
+ const cartEndpoint = isSignedIn
+ ? signedInCartEndpoint
+ : guestCartEndpoint;
+
+ const response = await request(cartEndpoint, {
+ method: 'PUT',
+ body: JSON.stringify({ cartItem })
+ });
- dispatch(actions.updateItem.receive({ cartItem, item, quantity }));
+ dispatch(
+ actions.updateItem.receive({
+ cartItem: response,
+ item,
+ quantity
+ })
+ );
} catch (error) {
- const { response, noGuestCartId } = error;
+ const { response, noCartId } = error;
dispatch(actions.updateItem.receive(error));
// check if the guest cart has expired
- if (noGuestCartId || (response && response.status === 404)) {
+ if (noCartId || (response && response.status === 404)) {
// if so, then delete the cached ID...
// in contrast to the save, make sure storage deletion is
// complete before dispatching the error--you don't want an
// upstream action to try and reuse the known-bad ID.
- await clearGuestCartId();
+ await clearCartId();
// then create a new one
- await dispatch(createGuestCart());
+ await dispatch(createCart());
// then retry this operation
return thunk(...arguments);
}
}
- await Promise.all([
- dispatch(toggleDrawer('cart')),
- dispatch(getCartDetails({ forceRefresh: true }))
- ]);
- // This is done here as a dispatch instead of as part of
- // updateItem.receive() so that the cart will close the options
- // drawer only after it's finished updating
+ // await Promise.all([
+ // dispatch(getCartDetails({ forceRefresh: true })),
+ // dispatch(toggleDrawer('cart'))
+ // ]);
+
+ await dispatch(getCartDetails({ forceRefresh: true }));
+
+ // Close the options drawer only after the cart is finished updating.
dispatch(closeOptionsDrawer());
};
};
@@ -231,48 +209,61 @@ export const removeItemFromCart = payload => {
dispatch(actions.removeItem.request(payload));
try {
- const { cart } = getState();
- const { guestCartId } = cart;
- const cartItemCount = cart.details ? cart.details.items_count : 0;
+ const { cart, user } = getState();
+ const { cartId } = cart;
- if (!guestCartId) {
- const missingGuestCartError = new Error(
- 'Missing required information: guestCartId'
+ if (!cartId) {
+ const missingCartIdError = new Error(
+ 'Missing required information: cartId'
);
- missingGuestCartError.noGuestCartId = true;
- throw missingGuestCartError;
+ missingCartIdError.noCartId = true;
+ throw missingCartIdError;
}
- const cartItem = await request(
- `/rest/V1/guest-carts/${guestCartId}/items/${item.item_id}`,
- {
- method: 'DELETE'
- }
- );
+ const { isSignedIn } = user;
+ const guestCartEndpoint = `/rest/V1/guest-carts/${cartId}/items/${
+ item.item_id
+ }`;
+ const signedInCartEndpoint = `/rest/V1/carts/mine/items/${
+ item.item_id
+ }`;
+ const cartEndpoint = isSignedIn
+ ? signedInCartEndpoint
+ : guestCartEndpoint;
+
+ const response = await request(cartEndpoint, {
+ method: 'DELETE'
+ });
+
// When removing the last item in the cart, perform a reset
// to prevent a bug where the next item added to the cart has
// a price of 0
- if (cartItemCount == 1) {
- await clearGuestCartId();
+ const cartItemCount = cart.details ? cart.details.items_count : 0;
+ if (cartItemCount === 1) {
+ await clearCartId();
}
dispatch(
- actions.removeItem.receive({ cartItem, item, cartItemCount })
+ actions.removeItem.receive({
+ cartItem: response,
+ item,
+ cartItemCount
+ })
);
} catch (error) {
- const { response, noGuestCartId } = error;
+ const { response, noCartId } = error;
dispatch(actions.removeItem.receive(error));
// check if the guest cart has expired
- if (noGuestCartId || (response && response.status === 404)) {
+ if (noCartId || (response && response.status === 404)) {
// if so, then delete the cached ID...
// in contrast to the save, make sure storage deletion is
// complete before dispatching the error--you don't want an
// upstream action to try and reuse the known-bad ID.
- await clearGuestCartId();
+ await clearCartId();
// then create a new one
- await dispatch(createGuestCart());
+ await dispatch(createCart());
// then retry this operation
return thunk(...arguments);
}
@@ -297,26 +288,19 @@ export const getCartDetails = (payload = {}) => {
return async function thunk(dispatch, getState) {
const { cart, user } = getState();
- const { guestCartId } = cart;
+ const { cartId } = cart;
+ const { isSignedIn } = user;
- if (user.isSignedIn) {
- // TODO: handle authed carts
- // if a user creates an account,
- // then the guest cart will be transferred to their account
- // causing `/guest-carts` to 400
- return;
- }
-
- // if there isn't a guest cart, create one
+ // if there isn't a cart, create one
// then retry this operation
- if (!guestCartId) {
- await dispatch(createGuestCart());
+ if (!cartId) {
+ await dispatch(createCart());
return thunk(...arguments);
}
// Once we have the cart id indicate that we are starting to make
// async requests for the details.
- dispatch(actions.getDetails.request(guestCartId));
+ dispatch(actions.getDetails.request(cartId));
try {
const [
@@ -327,17 +311,20 @@ export const getCartDetails = (payload = {}) => {
] = await Promise.all([
retrieveImageCache(),
fetchCartPart({
- guestCartId,
- forceRefresh
+ cartId,
+ forceRefresh,
+ isSignedIn
}),
fetchCartPart({
- guestCartId,
+ cartId,
forceRefresh,
+ isSignedIn,
subResource: 'payment-methods'
}),
fetchCartPart({
- guestCartId,
+ cartId,
forceRefresh,
+ isSignedIn,
subResource: 'totals'
})
]);
@@ -379,9 +366,9 @@ export const getCartDetails = (payload = {}) => {
// in contrast to the save, make sure storage deletion is
// complete before dispatching the error--you don't want an
// upstream action to try and reuse the known-bad ID.
- await clearGuestCartId();
+ await clearCartId();
// then create a new one
- await dispatch(createGuestCart());
+ await dispatch(createCart());
// then retry this operation
return thunk(...arguments);
}
@@ -410,53 +397,59 @@ export const toggleCart = () =>
]);
};
-export const removeGuestCart = () =>
- async function thunk(...args) {
- const [dispatch, getState] = args;
- const { cart } = getState();
- // ensure state slices are present
- if (!cart) {
- return;
- }
- if (cart['guestCartId']) {
- dispatch({
- type: 'REMOVE_GUEST_CART'
- });
- }
+export const removeCart = () =>
+ async function thunk(dispatch) {
+ // Clear the cartId from local storage.
+ await clearCartId();
+
+ // Clear the cart info from the redux store.
+ await dispatch(actions.reset());
};
/* helpers */
-async function fetchCartPart({ guestCartId, forceRefresh, subResource = '' }) {
- return request(`/rest/V1/guest-carts/${guestCartId}/${subResource}`, {
- cache: forceRefresh ? 'reload' : 'default'
- });
+async function fetchCartPart({
+ cartId,
+ forceRefresh,
+ isSignedIn,
+ subResource = ''
+}) {
+ const signedInEndpoint = `/rest/V1/carts/mine/${subResource}`;
+ const guestEndpoint = `/rest/V1/guest-carts/${cartId}/${subResource}`;
+ const endpoint = isSignedIn ? signedInEndpoint : guestEndpoint;
+
+ const cache = forceRefresh ? 'reload' : 'default';
+
+ return request(endpoint, { cache });
}
-export async function getGuestCartId(dispatch, getState) {
+export async function getCartId(dispatch, getState) {
const { cart } = getState();
+
// reducers may be added asynchronously
if (!cart) {
return null;
}
- // create a guest cart if one hasn't been created yet
- if (!cart.guestCartId) {
- await dispatch(createGuestCart());
+
+ // create a cart if one hasn't been created yet
+ if (!cart.cartId) {
+ await dispatch(createCart());
}
+
// retrieve app state again
- return getState().cart.guestCartId;
+ return getState().cart.cartId;
}
-export async function retrieveGuestCartId() {
- return storage.getItem('guestCartId');
+export async function retrieveCartId() {
+ return storage.getItem('cartId');
}
-export async function saveGuestCartId(id) {
- return storage.setItem('guestCartId', id);
+export async function saveCartId(id) {
+ return storage.setItem('cartId', id);
}
-export async function clearGuestCartId() {
- return storage.removeItem('guestCartId');
+export async function clearCartId() {
+ return storage.removeItem('cartId');
}
async function retrieveImageCache() {
@@ -467,7 +460,36 @@ async function saveImageCache(cache) {
return storage.setItem('imagesBySku', cache);
}
-async function writeImageToCache(item = {}) {
+/**
+ * Transforms an item payload to a shape that the REST endpoints expect.
+ * When GraphQL comes online we can drop this.
+ */
+function toRESTCartItem(cartId, payload) {
+ const { item, productType, quantity } = payload;
+
+ const cartItem = {
+ qty: quantity,
+ sku: item.sku,
+ name: item.name,
+ quote_id: cartId
+ };
+
+ if (productType === 'ConfigurableProduct') {
+ const { options, parentSku } = payload;
+
+ cartItem.sku = parentSku;
+ cartItem.product_type = 'configurable';
+ cartItem.product_option = {
+ extension_attributes: {
+ configurable_item_options: options
+ }
+ };
+ }
+
+ return cartItem;
+}
+
+export async function writeImageToCache(item = {}) {
const { media_gallery_entries: media, sku } = item;
if (sku) {
diff --git a/packages/venia-concept/src/actions/checkout/__tests__/asyncActions.spec.js b/packages/venia-concept/src/actions/checkout/__tests__/asyncActions.spec.js
index 89ccb5a7b2..afdfd88c91 100644
--- a/packages/venia-concept/src/actions/checkout/__tests__/asyncActions.spec.js
+++ b/packages/venia-concept/src/actions/checkout/__tests__/asyncActions.spec.js
@@ -53,8 +53,9 @@ const paymentMethod = {
beforeAll(() => {
getState.mockImplementation(() => ({
- cart: { guestCartId: 'GUEST_CART_ID' },
- directory: { countries }
+ cart: { cartId: 'CART_ID' },
+ directory: { countries },
+ user: { isSignedIn: false }
}));
});
@@ -153,7 +154,7 @@ describe('getShippingMethods', () => {
expect(dispatch).toHaveBeenNthCalledWith(
1,
- actions.getShippingMethods.request('GUEST_CART_ID')
+ actions.getShippingMethods.request('CART_ID')
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
@@ -171,7 +172,7 @@ describe('getShippingMethods', () => {
expect(dispatch).toHaveBeenNthCalledWith(
1,
- actions.getShippingMethods.request('GUEST_CART_ID')
+ actions.getShippingMethods.request('CART_ID')
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
@@ -179,6 +180,10 @@ describe('getShippingMethods', () => {
);
expect(dispatch).toHaveBeenCalledTimes(2);
});
+
+ test('its thunk uses the proper endpoint when the user is signed in', async () => {
+ // TODO
+ });
});
describe('submitPaymentMethodAndBillingAddress', () => {
@@ -279,14 +284,14 @@ describe('submitBillingAddress', () => {
});
});
- test('submitBillingAddress thunk throws if there is no guest cart', async () => {
+ test('submitBillingAddress thunk throws if there is no cart', async () => {
getState.mockImplementationOnce(() => ({
cart: {},
directory: { countries }
}));
await expect(
submitBillingAddress(sameAsShippingPayload)(...thunkArgs)
- ).rejects.toThrow('guestCartId');
+ ).rejects.toThrow('cartId');
});
});
@@ -323,14 +328,14 @@ describe('submitShippingAddress', () => {
expect(mockSetItem).toHaveBeenCalledWith('shipping_address', address);
});
- test('submitShippingAddress thunk throws if there is no guest cart', async () => {
+ test('submitShippingAddress thunk throws if there is no cart', async () => {
getState.mockImplementationOnce(() => ({
cart: {},
directory: { countries }
}));
await expect(
submitShippingAddress(payload)(...thunkArgs)
- ).rejects.toThrow('guestCartId');
+ ).rejects.toThrow('cartId');
});
});
@@ -370,14 +375,14 @@ describe('submitPaymentMethod', () => {
);
});
- test('submitPaymentMethod thunk throws if there is no guest cart', async () => {
+ test('submitPaymentMethod thunk throws if there is no cart', async () => {
getState.mockImplementationOnce(() => ({
cart: {}
}));
await expect(
submitPaymentMethod(payload)(...thunkArgs)
- ).rejects.toThrow('guestCartId');
+ ).rejects.toThrow('cartId');
});
});
@@ -426,14 +431,14 @@ describe('submitShippingMethod', () => {
expect(mockSetItem).toHaveBeenCalledTimes(1);
});
- test('submitShippingMethod thunk throws if there is no guest cart', async () => {
+ test('submitShippingMethod thunk throws if there is no cart', async () => {
getState.mockImplementationOnce(() => ({
cart: {}
}));
await expect(
submitShippingMethod(payload)(...thunkArgs)
- ).rejects.toThrow('guestCartId');
+ ).rejects.toThrow('cartId');
});
});
@@ -487,9 +492,10 @@ describe('submitOrder', () => {
details: {
billing_address: address
},
- guestCartId: 'GUEST_CART_ID'
+ cartId: 'CART_ID'
},
- directory: { countries }
+ directory: { countries },
+ user: { isSignedIn: false }
};
getState
@@ -522,7 +528,7 @@ describe('submitOrder', () => {
expect(dispatch).toHaveBeenCalledTimes(3);
expect(mockRemoveItem).toHaveBeenNthCalledWith(1, 'billing_address');
- expect(mockRemoveItem).toHaveBeenNthCalledWith(2, 'guestCartId');
+ expect(mockRemoveItem).toHaveBeenNthCalledWith(2, 'cartId');
expect(mockRemoveItem).toHaveBeenNthCalledWith(3, 'paymentMethod');
expect(mockRemoveItem).toHaveBeenNthCalledWith(4, 'shipping_address');
expect(mockRemoveItem).toHaveBeenNthCalledWith(5, 'shippingMethod');
@@ -535,9 +541,10 @@ describe('submitOrder', () => {
details: {
billing_address: address
},
- guestCartId: 'GUEST_CART_ID'
+ cartId: 'CART_ID'
},
- directory: { countries }
+ directory: { countries },
+ user: { isSignedIn: false }
};
getState
@@ -572,7 +579,7 @@ describe('submitOrder', () => {
expect(dispatch).toHaveBeenCalledTimes(3);
expect(mockRemoveItem).toHaveBeenNthCalledWith(1, 'billing_address');
- expect(mockRemoveItem).toHaveBeenNthCalledWith(2, 'guestCartId');
+ expect(mockRemoveItem).toHaveBeenNthCalledWith(2, 'cartId');
expect(mockRemoveItem).toHaveBeenNthCalledWith(3, 'paymentMethod');
expect(mockRemoveItem).toHaveBeenNthCalledWith(4, 'shipping_address');
expect(mockRemoveItem).toHaveBeenNthCalledWith(5, 'shippingMethod');
@@ -599,14 +606,16 @@ describe('submitOrder', () => {
expect(dispatch).toHaveBeenCalledTimes(2);
});
- test('submitOrder thunk throws if there is no guest cart', async () => {
+ test('submitOrder thunk throws if there is no cart', async () => {
getState.mockImplementationOnce(() => ({
cart: {}
}));
- await expect(submitOrder()(...thunkArgs)).rejects.toThrow(
- 'guestCartId'
- );
+ await expect(submitOrder()(...thunkArgs)).rejects.toThrow('cartId');
+ });
+
+ test('its thunk uses the proper endpoints when the user is signed in', async () => {
+ // TODO
});
});
diff --git a/packages/venia-concept/src/actions/checkout/asyncActions.js b/packages/venia-concept/src/actions/checkout/asyncActions.js
index b8ce04aff2..be064fdd9d 100644
--- a/packages/venia-concept/src/actions/checkout/asyncActions.js
+++ b/packages/venia-concept/src/actions/checkout/asyncActions.js
@@ -1,7 +1,7 @@
import { RestApi, Util } from '@magento/peregrine';
import { closeDrawer } from 'src/actions/app';
-import { clearGuestCartId, createGuestCart } from 'src/actions/cart';
+import { clearCartId, createCart } from 'src/actions/cart';
import { getCountries } from 'src/actions/directory';
import { getOrderInformation } from 'src/selectors/cart';
import { getAccountInformation } from 'src/selectors/checkoutReceipt';
@@ -27,7 +27,7 @@ export const cancelCheckout = () =>
export const resetCheckout = () =>
async function thunk(dispatch) {
await dispatch(closeDrawer());
- await dispatch(createGuestCart());
+ await dispatch(createCart());
dispatch(actions.reset());
};
@@ -38,31 +38,33 @@ export const editOrder = section =>
export const getShippingMethods = () => {
return async function thunk(dispatch, getState) {
- const { cart } = getState();
- const { guestCartId } = cart;
+ const { cart, user } = getState();
+ const { cartId } = cart;
try {
// if there isn't a guest cart, create one
// then retry this operation
- if (!guestCartId) {
- await dispatch(createGuestCart());
+ if (!cartId) {
+ await dispatch(createCart());
return thunk(...arguments);
}
- dispatch(actions.getShippingMethods.request(guestCartId));
-
- const response = await request(
- `/rest/V1/guest-carts/${guestCartId}/estimate-shipping-methods`,
- {
- method: 'POST',
- body: JSON.stringify({
- address: {
- country_id: 'US',
- postcode: null
- }
- })
- }
- );
+ dispatch(actions.getShippingMethods.request(cartId));
+
+ const guestEndpoint = `/rest/V1/guest-carts/${cartId}/estimate-shipping-methods`;
+ const authedEndpoint =
+ '/rest/V1/carts/mine/estimate-shipping-methods';
+ const endpoint = user.isSignedIn ? authedEndpoint : guestEndpoint;
+
+ const response = await request(endpoint, {
+ method: 'POST',
+ body: JSON.stringify({
+ address: {
+ country_id: 'US',
+ postcode: null
+ }
+ })
+ });
dispatch(actions.getShippingMethods.receive(response));
} catch (error) {
@@ -73,8 +75,8 @@ export const getShippingMethods = () => {
// check if the guest cart has expired
if (response && response.status === 404) {
// if so, clear it out, get a new one, and retry.
- await clearGuestCartId();
- await dispatch(createGuestCart());
+ await clearCartId();
+ await dispatch(createCart());
return thunk(...arguments);
}
}
@@ -99,9 +101,9 @@ export const submitBillingAddress = payload =>
const { cart, directory } = getState();
- const { guestCartId } = cart;
- if (!guestCartId) {
- throw new Error('Missing required information: guestCartId');
+ const { cartId } = cart;
+ if (!cartId) {
+ throw new Error('Missing required information: cartId');
}
let desiredBillingAddress = payload;
@@ -125,9 +127,9 @@ export const submitPaymentMethod = payload =>
const { cart } = getState();
- const { guestCartId } = cart;
- if (!guestCartId) {
- throw new Error('Missing required information: guestCartId');
+ const { cartId } = cart;
+ if (!cartId) {
+ throw new Error('Missing required information: cartId');
}
await savePaymentMethod(payload);
@@ -140,9 +142,9 @@ export const submitShippingAddress = payload =>
const { cart, directory } = getState();
- const { guestCartId } = cart;
- if (!guestCartId) {
- throw new Error('Missing required information: guestCartId');
+ const { cartId } = cart;
+ if (!cartId) {
+ throw new Error('Missing required information: cartId');
}
const { countries } = directory;
@@ -167,9 +169,9 @@ export const submitShippingMethod = payload =>
dispatch(actions.shippingMethod.submit(payload));
const { cart } = getState();
- const { guestCartId } = cart;
- if (!guestCartId) {
- throw new Error('Missing required information: guestCartId');
+ const { cartId } = cart;
+ if (!cartId) {
+ throw new Error('Missing required information: cartId');
}
const desiredShippingMethod = payload.formValues.shippingMethod;
@@ -181,10 +183,10 @@ export const submitOrder = () =>
async function thunk(dispatch, getState) {
dispatch(actions.order.submit());
- const { cart } = getState();
- const { guestCartId } = cart;
- if (!guestCartId) {
- throw new Error('Missing required information: guestCartId');
+ const { cart, user } = getState();
+ const { cartId } = cart;
+ if (!cartId) {
+ throw new Error('Missing required information: cartId');
}
let billing_address = await retrieveBillingAddress();
@@ -208,40 +210,48 @@ export const submitOrder = () =>
try {
// POST to shipping-information to submit the shipping address and shipping method.
- await request(
- `/rest/V1/guest-carts/${guestCartId}/shipping-information`,
- {
- method: 'POST',
- body: JSON.stringify({
- addressInformation: {
- billing_address,
- shipping_address,
- shipping_carrier_code: shipping_method.carrier_code,
- shipping_method_code: shipping_method.method_code
- }
- })
- }
- );
+ const guestShippingEndpoint = `/rest/V1/guest-carts/${cartId}/shipping-information`;
+ const authedShippingEndpoint =
+ '/rest/V1/carts/mine/shipping-information';
+ const shippingEndpoint = user.isSignedIn
+ ? authedShippingEndpoint
+ : guestShippingEndpoint;
+
+ await request(shippingEndpoint, {
+ method: 'POST',
+ body: JSON.stringify({
+ addressInformation: {
+ billing_address,
+ shipping_address,
+ shipping_carrier_code: shipping_method.carrier_code,
+ shipping_method_code: shipping_method.method_code
+ }
+ })
+ });
// POST to payment-information to submit the payment details and billing address,
// Note: this endpoint also actually submits the order.
- const response = await request(
- `/rest/V1/guest-carts/${guestCartId}/payment-information`,
- {
- method: 'POST',
- body: JSON.stringify({
- billingAddress: billing_address,
- cartId: guestCartId,
- email: shipping_address.email,
- paymentMethod: {
- additional_data: {
- payment_method_nonce: paymentMethod.data.nonce
- },
- method: paymentMethod.code
- }
- })
- }
- );
+ const guestPaymentEndpoint = `/rest/V1/guest-carts/${cartId}/payment-information`;
+ const authedPaymentEndpoint =
+ '/rest/V1/carts/mine/payment-information';
+ const paymentEndpoint = user.isSignedIn
+ ? authedPaymentEndpoint
+ : guestPaymentEndpoint;
+
+ const response = await request(paymentEndpoint, {
+ method: 'POST',
+ body: JSON.stringify({
+ billingAddress: billing_address,
+ cartId: cartId,
+ email: shipping_address.email,
+ paymentMethod: {
+ additional_data: {
+ payment_method_nonce: paymentMethod.data.nonce
+ },
+ method: paymentMethod.code
+ }
+ })
+ });
dispatch(
checkoutReceiptActions.setOrderInformation(
@@ -251,7 +261,7 @@ export const submitOrder = () =>
// Clear out everything we've saved about this cart from local storage.
await clearBillingAddress();
- await clearGuestCartId();
+ await clearCartId();
await clearPaymentMethod();
await clearShippingAddress();
await clearShippingMethod();
diff --git a/packages/venia-concept/src/actions/user/__tests__/asyncActions.spec.js b/packages/venia-concept/src/actions/user/__tests__/asyncActions.spec.js
index 2d20c1b018..a0bda8f0a5 100644
--- a/packages/venia-concept/src/actions/user/__tests__/asyncActions.spec.js
+++ b/packages/venia-concept/src/actions/user/__tests__/asyncActions.spec.js
@@ -9,11 +9,11 @@ import {
import actions from '../actions';
import {
signIn,
+ signOut,
getUserDetails,
completePasswordReset,
createAccount,
createNewUserRequest,
- assignGuestCartToCustomer,
resetPassword
} from '../asyncActions';
@@ -53,10 +53,6 @@ afterAll(() => {
getState.mockRestore();
});
-test('signIn() returns a thunk', () => {
- expect(signIn()).toBeInstanceOf(Function);
-});
-
test('getUserDetails() returns a thunk', () => {
expect(getUserDetails()).toBeInstanceOf(Function);
});
@@ -69,54 +65,6 @@ test('createNewUserRequest() returns a thunk', () => {
expect(createNewUserRequest()).toBeInstanceOf(Function);
});
-test('assignGuestCartToCustomer() returns a thunk', () => {
- expect(assignGuestCartToCustomer()).toBeInstanceOf(Function);
-});
-
-test('signIn thunk dispatches resetSignInError', async () => {
- await signIn(credentials)(...thunkArgs);
-
- expect(dispatch).toHaveBeenCalledWith(actions.resetSignInError.request());
-});
-
-test('signIn thunk dispatches signIn', async () => {
- await signIn(credentials)(...thunkArgs);
-
- expect(dispatch).toHaveBeenCalledWith(actions.signIn.receive());
-});
-
-test('signIn thunk dispatches signInError on failed request', async () => {
- const error = new Error('ERROR');
- request.mockRejectedValueOnce(error);
- await signIn(credentials)(...thunkArgs);
- expect(dispatch).toHaveBeenNthCalledWith(
- 2,
- actions.signInError.receive(error)
- );
-});
-
-test('signIn thunk makes request to get customer token with credentials', async () => {
- await signIn(credentials)(...thunkArgs);
-
- const firstRequest = request.mock.calls[0];
-
- expect(firstRequest[0]).toBe('/rest/V1/integration/customer/token');
- expect(firstRequest[1]).toHaveProperty('method', 'POST');
- expect(JSON.parse(firstRequest[1].body)).toEqual({
- username: `${credentials.username}`,
- password: `${credentials.password}`
- });
-});
-
-test('signIn thunk makes request to get customer details after sign in', async () => {
- await signIn(credentials)(...thunkArgs);
-
- const secondRequest = request.mock.calls[1];
-
- expect(secondRequest[0]).toBe('/rest/V1/customers/me');
- expect(secondRequest[1]).toHaveProperty('method', 'GET');
-});
-
test('getUserDetails thunk makes request to get customer details if user is signed in', async () => {
getState.mockImplementationOnce(() => ({
user: { isSignedIn: false }
@@ -175,12 +123,6 @@ test('createNewUserRequest thunk dispatches signIn', async () => {
expect(dispatch).toHaveBeenNthCalledWith(2, expect.any(Function));
});
-test('createNewUserRequest thunk dispatches assignGuestCartToCustomer', async () => {
- await createNewUserRequest(accountInfo)(...thunkArgs);
-
- expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
-});
-
test('createNewUserRequest thunk dispatches createAccountError on invalid account info', async () => {
const error = new TypeError('ERROR');
request.mockRejectedValueOnce(error);
@@ -195,25 +137,95 @@ test('createNewUserRequest thunk dispatches createAccountError on invalid accoun
);
});
-test('assignGuestCartToCustomer thunk retrieves guest cart with guestCartId', async () => {
- getState.mockImplementationOnce(() => ({
- user: { isSignedIn: false, id: 'ID', storeId: 'STORE_ID' }
- }));
+describe('signIn', () => {
+ test('it returns a thunk', () => {
+ expect(signIn()).toBeInstanceOf(Function);
+ });
+
+ test('its thunk dispatches actions on success', async () => {
+ await signIn(credentials)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(4);
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.resetSignInError.request()
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(2, actions.signIn.receive());
+ // removeCart
+ expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
+ // getCartDetails
+ expect(dispatch).toHaveBeenNthCalledWith(4, expect.any(Function));
+ });
- const storedGuestCartId = 'STORED_GUEST_CART_ID';
- mockGetItem.mockImplementationOnce(() => storedGuestCartId);
+ test('its thunk dispatches actions on error', async () => {
+ // Test setup.
+ const error = new Error('ERROR');
+ request.mockRejectedValueOnce(error);
- await assignGuestCartToCustomer({})(...thunkArgs);
+ // Execute the function.
+ await signIn(credentials)(...thunkArgs);
- const firstRequest = request.mock.calls[0];
- expect(mockGetItem).toHaveBeenCalledWith('guestCartId');
- expect(firstRequest[0]).toBe(`/rest/V1/guest-carts/STORED_GUEST_CART_ID`);
- expect(firstRequest[1]).toHaveProperty('method', 'PUT');
+ // Make assertions.
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 1,
+ actions.resetSignInError.request()
+ );
+ expect(dispatch).toHaveBeenNthCalledWith(
+ 2,
+ actions.signInError.receive(error)
+ );
+ });
+
+ test('its thunk makes requests on success', async () => {
+ await signIn(credentials)(...thunkArgs);
+
+ expect(request).toHaveBeenCalledTimes(2);
+
+ const tokenRequest = request.mock.calls[0];
+ const tokenEndpoint = tokenRequest[0];
+ const tokenParams = tokenRequest[1];
+
+ expect(tokenEndpoint).toBe('/rest/V1/integration/customer/token');
+ expect(tokenParams).toHaveProperty('method', 'POST');
+ expect(tokenParams).toHaveProperty('body');
+
+ const tokenBody = JSON.parse(tokenParams.body);
+ expect(tokenBody).toEqual({
+ username: `${credentials.username}`,
+ password: `${credentials.password}`
+ });
+
+ const detailsRequest = request.mock.calls[1];
+ const detailsEndpoint = detailsRequest[0];
+ const detailsParams = detailsRequest[1];
+
+ expect(detailsEndpoint).toBe('/rest/V1/customers/me');
+ expect(detailsParams).toHaveProperty('method', 'GET');
+ });
});
-test('assignGuestCartToCustomer thunk dispatches removeGuestCart()', async () => {
- await assignGuestCartToCustomer({})(...thunkArgs);
- expect(dispatch).toHaveBeenNthCalledWith(1, expect.any(Function));
+describe('signOut', () => {
+ const mockParam = {
+ history: {
+ go: jest.fn()
+ }
+ };
+
+ test('it returns a thunk', () => {
+ expect(signOut(mockParam)).toBeInstanceOf(Function);
+ });
+
+ test('its thunk dispatches actions on success', async () => {
+ await signOut(mockParam)(...thunkArgs);
+
+ expect(dispatch).toHaveBeenCalledTimes(3);
+ expect(dispatch).toHaveBeenNthCalledWith(1, actions.signIn.reset());
+ const removeCart = expect.any(Function);
+ expect(dispatch).toHaveBeenNthCalledWith(2, removeCart);
+ const getCartDetails = expect.any(Function);
+ expect(dispatch).toHaveBeenNthCalledWith(3, getCartDetails);
+ });
});
describe('resetPassword', () => {
diff --git a/packages/venia-concept/src/actions/user/asyncActions.js b/packages/venia-concept/src/actions/user/asyncActions.js
index 631759877d..ef221b3fe4 100755
--- a/packages/venia-concept/src/actions/user/asyncActions.js
+++ b/packages/venia-concept/src/actions/user/asyncActions.js
@@ -1,12 +1,13 @@
import { RestApi } from '@magento/peregrine';
import { Util } from '@magento/peregrine';
-import { removeGuestCart } from 'src/actions/cart';
import { refresh } from 'src/util/router-helpers';
+import { getCartDetails, removeCart } from 'src/actions/cart';
+
+import actions from './actions';
const { request } = RestApi.Magento2;
const { BrowserPersistence } = Util;
-
-import actions from './actions';
+const storage = new BrowserPersistence();
export const signIn = credentials =>
async function thunk(...args) {
@@ -16,8 +17,6 @@ export const signIn = credentials =>
try {
const body = {
- // username: 'roni_cost@example.com',
- // password: 'roni_cost3@example.com'
username: credentials.username,
password: credentials.password
};
@@ -36,17 +35,28 @@ export const signIn = credentials =>
method: 'GET'
});
- dispatch(actions.signIn.receive(userDetails));
+ await dispatch(actions.signIn.receive(userDetails));
+
+ // Now that we're signed in, forget the old (guest) cart
+ // and fetch this customer's cart.
+ await dispatch(removeCart());
+ dispatch(getCartDetails({ forceRefresh: true }));
} catch (error) {
dispatch(actions.signInError.receive(error));
}
};
-export const signOut = ({ history }) => dispatch => {
- setToken(null);
+export const signOut = ({ history }) => async dispatch => {
+ // Sign the user out in local storage and Redux.
+ await clearToken();
+ await dispatch(actions.signIn.reset());
- dispatch(actions.signIn.reset());
+ // Now that we're signed out, forget the old (customer) cart
+ // and fetch a new guest cart.
+ await dispatch(removeCart());
+ dispatch(getCartDetails({ forceRefresh: true }));
+ // Finally, go back to the first page of the browser history.
refresh({ history });
};
@@ -54,8 +64,10 @@ export const getUserDetails = () =>
async function thunk(...args) {
const [dispatch, getState] = args;
const { user } = getState();
+
if (user.isSignedIn) {
dispatch(actions.resetSignInError.request());
+
try {
const userDetails = await request('/rest/V1/customers/me', {
method: 'GET'
@@ -78,13 +90,13 @@ export const createNewUserRequest = accountInfo =>
method: 'POST',
body: JSON.stringify(accountInfo)
});
+
await dispatch(
signIn({
username: accountInfo.customer.email,
password: accountInfo.password
})
);
- dispatch(assignGuestCartToCustomer());
} catch (error) {
dispatch(actions.createAccountError.receive(error));
@@ -106,30 +118,6 @@ export const createAccount = accountInfo => async dispatch => {
} catch (e) {}
};
-export const assignGuestCartToCustomer = () =>
- async function thunk(...args) {
- const [dispatch, getState] = args;
- const { user } = getState();
-
- try {
- const storage = new BrowserPersistence();
- const guestCartId = storage.getItem('guestCartId');
- const payload = {
- customerId: user.id,
- storeId: user.store_id
- };
- // TODO: Check if guestCartId exists
- await request(`/rest/V1/guest-carts/${guestCartId}`, {
- method: 'PUT',
- body: JSON.stringify(payload)
- });
- dispatch(removeGuestCart());
- } catch (error) {
- // TODO: Handle error
- console.log(error);
- }
- };
-
export const resetPassword = ({ email }) =>
async function thunk(...args) {
const [dispatch] = args;
@@ -147,7 +135,10 @@ export const completePasswordReset = email => async dispatch =>
dispatch(actions.completePasswordReset(email));
async function setToken(token) {
- const storage = new BrowserPersistence();
// TODO: Get correct token expire time from API
- storage.setItem('signin_token', token, 3600);
+ return storage.setItem('signin_token', token, 3600);
+}
+
+async function clearToken() {
+ return storage.removeItem('signin_token');
}
diff --git a/packages/venia-concept/src/components/Checkout/Receipt/__tests__/receipt.spec.js b/packages/venia-concept/src/components/Checkout/Receipt/__tests__/receipt.spec.js
index b03cc19cf9..dcfda7d171 100644
--- a/packages/venia-concept/src/components/Checkout/Receipt/__tests__/receipt.spec.js
+++ b/packages/venia-concept/src/components/Checkout/Receipt/__tests__/receipt.spec.js
@@ -13,12 +13,15 @@ const classes = {
jest.mock('src/classify');
+const userProp = { isSignedIn: false };
+
test('renders a Receipt component correctly', () => {
const props = {
continueShopping: jest.fn(),
order: { id: '123' },
createAccount: jest.fn(),
- reset: jest.fn()
+ reset: jest.fn(),
+ user: userProp
};
const component = testRenderer.create(