initial commit
This commit is contained in:
@ -0,0 +1,16 @@
|
||||
export const ACTION_TYPES = {
|
||||
RECEIVE_CART: 'RECEIVE_CART',
|
||||
RECEIVE_ERROR: 'RECEIVE_ERROR',
|
||||
REPLACE_ERRORS: 'REPLACE_ERRORS',
|
||||
APPLYING_COUPON: 'APPLYING_COUPON',
|
||||
REMOVING_COUPON: 'REMOVING_COUPON',
|
||||
RECEIVE_CART_ITEM: 'RECEIVE_CART_ITEM',
|
||||
ITEM_PENDING_QUANTITY: 'ITEM_PENDING_QUANTITY',
|
||||
SET_IS_CART_DATA_STALE: 'SET_IS_CART_DATA_STALE',
|
||||
RECEIVE_REMOVED_ITEM: 'RECEIVE_REMOVED_ITEM',
|
||||
UPDATING_CUSTOMER_DATA: 'UPDATING_CUSTOMER_DATA',
|
||||
UPDATING_SELECTED_SHIPPING_RATE: 'UPDATING_SELECTED_SHIPPING_RATE',
|
||||
UPDATE_LEGACY_CART_FRAGMENTS: 'UPDATE_LEGACY_CART_FRAGMENTS',
|
||||
TRIGGER_ADDING_TO_CART_EVENT: 'TRIGGER_ADDING_TO_CART_EVENT',
|
||||
TRIGGER_ADDED_TO_CART_EVENT: 'TRIGGER_ADDED_TO_CART_EVENT',
|
||||
} as const;
|
525
packages/woocommerce-blocks/assets/js/data/cart/actions.ts
Normal file
525
packages/woocommerce-blocks/assets/js/data/cart/actions.ts
Normal file
@ -0,0 +1,525 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { select } from '@wordpress/data-controls';
|
||||
import type {
|
||||
Cart,
|
||||
CartResponse,
|
||||
CartResponseItem,
|
||||
CartBillingAddress,
|
||||
CartShippingAddress,
|
||||
ExtensionCartUpdateArgs,
|
||||
} from '@woocommerce/types';
|
||||
import { ReturnOrGeneratorYieldUnion } from '@automattic/data-stores';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
import type { AddToCartEventDetail } from '@woocommerce/type-defs/events';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES as types } from './action-types';
|
||||
import { STORE_KEY as CART_STORE_KEY } from './constants';
|
||||
import { apiFetchWithHeaders } from '../shared-controls';
|
||||
import type { ResponseError } from '../types';
|
||||
|
||||
/**
|
||||
* Returns an action object used in updating the store with the provided items
|
||||
* retrieved from a request using the given querystring.
|
||||
*
|
||||
* This is a generic response action.
|
||||
*
|
||||
* @param {CartResponse} response
|
||||
*/
|
||||
export const receiveCart = (
|
||||
response: CartResponse
|
||||
): { type: string; response: Cart } => {
|
||||
const cart = ( mapKeys( response, ( _, key ) =>
|
||||
camelCase( key )
|
||||
) as unknown ) as Cart;
|
||||
return {
|
||||
type: types.RECEIVE_CART,
|
||||
response: cart,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an action object used for receiving customer facing errors from the API.
|
||||
*
|
||||
* @param {ResponseError|null} [error=null] An error object containing the error
|
||||
* message and response code.
|
||||
* @param {boolean} [replace=true] Should existing errors be replaced,
|
||||
* or should the error be appended.
|
||||
*/
|
||||
export const receiveError = (
|
||||
error: ResponseError | null = null,
|
||||
replace = true
|
||||
) =>
|
||||
( {
|
||||
type: replace ? types.REPLACE_ERRORS : types.RECEIVE_ERROR,
|
||||
error,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object used to track when a coupon is applying.
|
||||
*
|
||||
* @param {string} [couponCode] Coupon being added.
|
||||
*/
|
||||
export const receiveApplyingCoupon = ( couponCode: string ) =>
|
||||
( {
|
||||
type: types.APPLYING_COUPON,
|
||||
couponCode,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object used to track when a coupon is removing.
|
||||
*
|
||||
* @param {string} [couponCode] Coupon being removed..
|
||||
*/
|
||||
export const receiveRemovingCoupon = ( couponCode: string ) =>
|
||||
( {
|
||||
type: types.REMOVING_COUPON,
|
||||
couponCode,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object for updating a single cart item in the store.
|
||||
*
|
||||
* @param {CartResponseItem} [response=null] A cart item API response.
|
||||
*/
|
||||
export const receiveCartItem = ( response: CartResponseItem | null = null ) =>
|
||||
( {
|
||||
type: types.RECEIVE_CART_ITEM,
|
||||
cartItem: response,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object to indicate if the specified cart item quantity is
|
||||
* being updated.
|
||||
*
|
||||
* @param {string} cartItemKey Cart item being updated.
|
||||
* @param {boolean} [isPendingQuantity=true] Flag for update state; true if API
|
||||
* request is pending.
|
||||
*/
|
||||
export const itemIsPendingQuantity = (
|
||||
cartItemKey: string,
|
||||
isPendingQuantity = true
|
||||
) =>
|
||||
( {
|
||||
type: types.ITEM_PENDING_QUANTITY,
|
||||
cartItemKey,
|
||||
isPendingQuantity,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object to remove a cart item from the store.
|
||||
*
|
||||
* @param {string} cartItemKey Cart item to remove.
|
||||
* @param {boolean} [isPendingDelete=true] Flag for update state; true if API
|
||||
* request is pending.
|
||||
*/
|
||||
export const itemIsPendingDelete = (
|
||||
cartItemKey: string,
|
||||
isPendingDelete = true
|
||||
) =>
|
||||
( {
|
||||
type: types.RECEIVE_REMOVED_ITEM,
|
||||
cartItemKey,
|
||||
isPendingDelete,
|
||||
} as const );
|
||||
/**
|
||||
* Returns an action object to mark the cart data in the store as stale.
|
||||
*
|
||||
* @param {boolean} [isCartDataStale=true] Flag to mark cart data as stale; true if
|
||||
* lastCartUpdate timestamp is newer than the
|
||||
* one in wcSettings.
|
||||
*/
|
||||
export const setIsCartDataStale = ( isCartDataStale = true ) =>
|
||||
( {
|
||||
type: types.SET_IS_CART_DATA_STALE,
|
||||
isCartDataStale,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object used to track when customer data is being updated
|
||||
* (billing and/or shipping).
|
||||
*/
|
||||
export const updatingCustomerData = ( isResolving: boolean ) =>
|
||||
( {
|
||||
type: types.UPDATING_CUSTOMER_DATA,
|
||||
isResolving,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object used to track whether the shipping rate is being
|
||||
* selected or not.
|
||||
*
|
||||
* @param {boolean} isResolving True if shipping rate is being selected.
|
||||
*/
|
||||
export const shippingRatesBeingSelected = ( isResolving: boolean ) =>
|
||||
( {
|
||||
type: types.UPDATING_SELECTED_SHIPPING_RATE,
|
||||
isResolving,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object for updating legacy cart fragments.
|
||||
*/
|
||||
export const updateCartFragments = () =>
|
||||
( {
|
||||
type: types.UPDATE_LEGACY_CART_FRAGMENTS,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Triggers an adding to cart event so other blocks can update accordingly.
|
||||
*/
|
||||
export const triggerAddingToCartEvent = () =>
|
||||
( {
|
||||
type: types.TRIGGER_ADDING_TO_CART_EVENT,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Triggers an added to cart event so other blocks can update accordingly.
|
||||
*/
|
||||
export const triggerAddedToCartEvent = ( {
|
||||
preserveCartData,
|
||||
}: AddToCartEventDetail ) =>
|
||||
( {
|
||||
type: types.TRIGGER_ADDED_TO_CART_EVENT,
|
||||
preserveCartData,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* POSTs to the /cart/extensions endpoint with the data supplied by the extension.
|
||||
*
|
||||
* @param {Object} args The data to be posted to the endpoint
|
||||
*/
|
||||
export function* applyExtensionCartUpdate(
|
||||
args: ExtensionCartUpdateArgs
|
||||
): Generator< unknown, CartResponse, { response: CartResponse } > {
|
||||
try {
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: '/wc/store/cart/extensions',
|
||||
method: 'POST',
|
||||
data: { namespace: args.namespace, data: args.data },
|
||||
cache: 'no-store',
|
||||
} );
|
||||
yield receiveCart( response );
|
||||
yield updateCartFragments();
|
||||
return response;
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a coupon code and either invalidates caches, or receives an error if
|
||||
* the coupon cannot be applied.
|
||||
*
|
||||
* @param {string} couponCode The coupon code to apply to the cart.
|
||||
* @throws Will throw an error if there is an API problem.
|
||||
*/
|
||||
export function* applyCoupon(
|
||||
couponCode: string
|
||||
): Generator< unknown, boolean, { response: CartResponse } > {
|
||||
yield receiveApplyingCoupon( couponCode );
|
||||
|
||||
try {
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: '/wc/store/cart/apply-coupon',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: couponCode,
|
||||
},
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
yield receiveApplyingCoupon( '' );
|
||||
yield updateCartFragments();
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
yield receiveApplyingCoupon( '' );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a coupon code and either invalidates caches, or receives an error if
|
||||
* the coupon cannot be removed.
|
||||
*
|
||||
* @param {string} couponCode The coupon code to remove from the cart.
|
||||
* @throws Will throw an error if there is an API problem.
|
||||
*/
|
||||
export function* removeCoupon(
|
||||
couponCode: string
|
||||
): Generator< unknown, boolean, { response: CartResponse } > {
|
||||
yield receiveRemovingCoupon( couponCode );
|
||||
|
||||
try {
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: '/wc/store/cart/remove-coupon',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: couponCode,
|
||||
},
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
yield receiveRemovingCoupon( '' );
|
||||
yield updateCartFragments();
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
yield receiveRemovingCoupon( '' );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the cart:
|
||||
* - Calls API to add item.
|
||||
* - If successful, yields action to add item from store.
|
||||
* - If error, yields action to store error.
|
||||
*
|
||||
* @param {number} productId Product ID to add to cart.
|
||||
* @param {number} [quantity=1] Number of product ID being added to cart.
|
||||
* @throws Will throw an error if there is an API problem.
|
||||
*/
|
||||
export function* addItemToCart(
|
||||
productId: number,
|
||||
quantity = 1
|
||||
): Generator< unknown, void, { response: CartResponse } > {
|
||||
try {
|
||||
yield triggerAddingToCartEvent();
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: `/wc/store/cart/add-item`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
id: productId,
|
||||
quantity,
|
||||
},
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
yield triggerAddedToCartEvent( { preserveCartData: true } );
|
||||
yield updateCartFragments();
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes specified item from the cart:
|
||||
* - Calls API to remove item.
|
||||
* - If successful, yields action to remove item from store.
|
||||
* - If error, yields action to store error.
|
||||
* - Sets cart item as pending while API request is in progress.
|
||||
*
|
||||
* @param {string} cartItemKey Cart item being updated.
|
||||
*/
|
||||
export function* removeItemFromCart(
|
||||
cartItemKey: string
|
||||
): Generator< unknown, void, { response: CartResponse } > {
|
||||
yield itemIsPendingDelete( cartItemKey );
|
||||
|
||||
try {
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: `/wc/store/cart/remove-item`,
|
||||
data: {
|
||||
key: cartItemKey,
|
||||
},
|
||||
method: 'POST',
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
yield updateCartFragments();
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
}
|
||||
yield itemIsPendingDelete( cartItemKey, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a quantity change the for specified cart item:
|
||||
* - Calls API to set quantity.
|
||||
* - If successful, yields action to update store.
|
||||
* - If error, yields action to store error.
|
||||
*
|
||||
* @param {string} cartItemKey Cart item being updated.
|
||||
* @param {number} quantity Specified (new) quantity.
|
||||
*/
|
||||
export function* changeCartItemQuantity(
|
||||
cartItemKey: string,
|
||||
quantity: number
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unclear how to represent multiple different yields as type
|
||||
): Generator< unknown, void, any > {
|
||||
const cartItem = yield select( CART_STORE_KEY, 'getCartItem', cartItemKey );
|
||||
yield itemIsPendingQuantity( cartItemKey );
|
||||
|
||||
if ( cartItem?.quantity === quantity ) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: '/wc/store/cart/update-item',
|
||||
method: 'POST',
|
||||
data: {
|
||||
key: cartItemKey,
|
||||
quantity,
|
||||
},
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
yield updateCartFragments();
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
}
|
||||
yield itemIsPendingQuantity( cartItemKey, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a shipping rate.
|
||||
*
|
||||
* @param {string} rateId The id of the rate being selected.
|
||||
* @param {number | string} [packageId] The key of the packages that we will
|
||||
* select within.
|
||||
*/
|
||||
export function* selectShippingRate(
|
||||
rateId: string,
|
||||
packageId = 0
|
||||
): Generator< unknown, boolean, { response: CartResponse } > {
|
||||
try {
|
||||
yield shippingRatesBeingSelected( true );
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: `/wc/store/cart/select-shipping-rate`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
package_id: packageId,
|
||||
rate_id: rateId,
|
||||
},
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
yield shippingRatesBeingSelected( false );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
yield shippingRatesBeingSelected( false );
|
||||
return true;
|
||||
}
|
||||
|
||||
type BillingAddressShippingAddress = {
|
||||
billing_address: CartBillingAddress;
|
||||
shipping_address: CartShippingAddress;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the shipping and/or billing address for the customer and returns an
|
||||
* updated cart.
|
||||
*
|
||||
* @param {BillingAddressShippingAddress} customerData Address data to be updated; can contain both
|
||||
* billing_address and shipping_address.
|
||||
*/
|
||||
export function* updateCustomerData(
|
||||
customerData: BillingAddressShippingAddress
|
||||
): Generator< unknown, boolean, { response: CartResponse } > {
|
||||
yield updatingCustomerData( true );
|
||||
|
||||
try {
|
||||
const { response } = yield apiFetchWithHeaders( {
|
||||
path: '/wc/store/cart/update-customer',
|
||||
method: 'POST',
|
||||
data: customerData,
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
yield updatingCustomerData( false );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
yield receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// rethrow error.
|
||||
throw error;
|
||||
}
|
||||
|
||||
yield updatingCustomerData( false );
|
||||
return true;
|
||||
}
|
||||
|
||||
export type CartAction = ReturnOrGeneratorYieldUnion<
|
||||
| typeof receiveCart
|
||||
| typeof receiveError
|
||||
| typeof receiveApplyingCoupon
|
||||
| typeof receiveRemovingCoupon
|
||||
| typeof receiveCartItem
|
||||
| typeof itemIsPendingQuantity
|
||||
| typeof itemIsPendingDelete
|
||||
| typeof updatingCustomerData
|
||||
| typeof shippingRatesBeingSelected
|
||||
| typeof setIsCartDataStale
|
||||
| typeof updateCustomerData
|
||||
| typeof removeItemFromCart
|
||||
| typeof changeCartItemQuantity
|
||||
| typeof addItemToCart
|
||||
| typeof updateCartFragments
|
||||
>;
|
17
packages/woocommerce-blocks/assets/js/data/cart/constants.ts
Normal file
17
packages/woocommerce-blocks/assets/js/data/cart/constants.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const STORE_KEY = 'wc/store/cart';
|
||||
export const CART_API_ERROR = {
|
||||
code: 'cart_api_error',
|
||||
message: __(
|
||||
'Unable to get cart data from the API.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
data: {
|
||||
status: 500,
|
||||
},
|
||||
};
|
||||
export const LAST_CART_UPDATE_TIMESTAMP_KEY = 'wc-blocks_cart_update_timestamp';
|
25
packages/woocommerce-blocks/assets/js/data/cart/controls.js
vendored
Normal file
25
packages/woocommerce-blocks/assets/js/data/cart/controls.js
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
triggerFragmentRefresh,
|
||||
triggerAddedToCartEvent,
|
||||
triggerAddingToCartEvent,
|
||||
} from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Default export for registering the controls with the store.
|
||||
*
|
||||
* @return {Object} An object with the controls to register with the store on the controls property of the registration object.
|
||||
*/
|
||||
export const controls = {
|
||||
UPDATE_LEGACY_CART_FRAGMENTS() {
|
||||
triggerFragmentRefresh();
|
||||
},
|
||||
TRIGGER_ADDING_TO_CART_EVENT() {
|
||||
triggerAddingToCartEvent();
|
||||
},
|
||||
TRIGGER_ADDED_TO_CART_EVENT( preserveCartData ) {
|
||||
triggerAddedToCartEvent( preserveCartData );
|
||||
},
|
||||
};
|
36
packages/woocommerce-blocks/assets/js/data/cart/index.ts
Normal file
36
packages/woocommerce-blocks/assets/js/data/cart/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerStore } from '@wordpress/data';
|
||||
import { controls as dataControls } from '@wordpress/data-controls';
|
||||
import type { SelectFromMap, DispatchFromMap } from '@automattic/data-stores';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from './constants';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducers';
|
||||
import { controls as sharedControls } from '../shared-controls';
|
||||
import { controls } from './controls';
|
||||
|
||||
registerStore< State >( STORE_KEY, {
|
||||
reducer,
|
||||
actions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
controls: { ...dataControls, ...sharedControls, ...controls } as any,
|
||||
selectors,
|
||||
resolvers,
|
||||
} );
|
||||
|
||||
export const CART_STORE_KEY = STORE_KEY;
|
||||
|
||||
declare module '@wordpress/data' {
|
||||
function dispatch(
|
||||
key: typeof CART_STORE_KEY
|
||||
): DispatchFromMap< typeof actions >;
|
||||
function select(
|
||||
key: typeof CART_STORE_KEY
|
||||
): SelectFromMap< typeof selectors >;
|
||||
}
|
169
packages/woocommerce-blocks/assets/js/data/cart/reducers.ts
Normal file
169
packages/woocommerce-blocks/assets/js/data/cart/reducers.ts
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { CartItem } from '@woocommerce/types';
|
||||
import type { Reducer } from 'redux';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES as types } from './action-types';
|
||||
import { defaultCartState, CartState } from '../default-states';
|
||||
import { EMPTY_CART_ERRORS } from '../constants';
|
||||
import type { CartAction } from './actions';
|
||||
|
||||
/**
|
||||
* Sub-reducer for cart items array.
|
||||
*
|
||||
* @param {Array<CartItem>} state cartData.items state slice.
|
||||
* @param {CartAction} action Action object.
|
||||
*/
|
||||
const cartItemsReducer = (
|
||||
state: Array< CartItem > = [],
|
||||
action: Partial< CartAction >
|
||||
) => {
|
||||
switch ( action.type ) {
|
||||
case types.RECEIVE_CART_ITEM:
|
||||
// Replace specified cart element with the new data from server.
|
||||
return state.map( ( cartItem ) => {
|
||||
if ( cartItem.key === action.cartItem?.key ) {
|
||||
return action.cartItem;
|
||||
}
|
||||
return cartItem;
|
||||
} );
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for receiving items related to the cart.
|
||||
*
|
||||
* @param {CartState} state The current state in the store.
|
||||
* @param {CartAction} action Action object.
|
||||
*
|
||||
* @return {CartState} New or existing state.
|
||||
*/
|
||||
const reducer: Reducer< CartState > = (
|
||||
state = defaultCartState,
|
||||
action: Partial< CartAction >
|
||||
) => {
|
||||
switch ( action.type ) {
|
||||
case types.RECEIVE_ERROR:
|
||||
if ( action.error ) {
|
||||
state = {
|
||||
...state,
|
||||
errors: state.errors.concat( action.error ),
|
||||
};
|
||||
}
|
||||
break;
|
||||
case types.REPLACE_ERRORS:
|
||||
if ( action.error ) {
|
||||
state = {
|
||||
...state,
|
||||
errors: [ action.error ],
|
||||
};
|
||||
}
|
||||
break;
|
||||
case types.RECEIVE_CART:
|
||||
if ( action.response ) {
|
||||
state = {
|
||||
...state,
|
||||
errors: EMPTY_CART_ERRORS,
|
||||
cartData: action.response,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case types.APPLYING_COUPON:
|
||||
if ( action.couponCode || action.couponCode === '' ) {
|
||||
state = {
|
||||
...state,
|
||||
metaData: {
|
||||
...state.metaData,
|
||||
applyingCoupon: action.couponCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
case types.REMOVING_COUPON:
|
||||
if ( action.couponCode || action.couponCode === '' ) {
|
||||
state = {
|
||||
...state,
|
||||
metaData: {
|
||||
...state.metaData,
|
||||
removingCoupon: action.couponCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case types.ITEM_PENDING_QUANTITY:
|
||||
// Remove key by default - handles isQuantityPending==false
|
||||
// and prevents duplicates when isQuantityPending===true.
|
||||
const keysPendingQuantity = state.cartItemsPendingQuantity.filter(
|
||||
( key ) => key !== action.cartItemKey
|
||||
);
|
||||
if ( action.isPendingQuantity && action.cartItemKey ) {
|
||||
keysPendingQuantity.push( action.cartItemKey );
|
||||
}
|
||||
state = {
|
||||
...state,
|
||||
cartItemsPendingQuantity: keysPendingQuantity,
|
||||
};
|
||||
break;
|
||||
case types.RECEIVE_REMOVED_ITEM:
|
||||
const keysPendingDelete = state.cartItemsPendingDelete.filter(
|
||||
( key ) => key !== action.cartItemKey
|
||||
);
|
||||
if ( action.isPendingDelete && action.cartItemKey ) {
|
||||
keysPendingDelete.push( action.cartItemKey );
|
||||
}
|
||||
state = {
|
||||
...state,
|
||||
cartItemsPendingDelete: keysPendingDelete,
|
||||
};
|
||||
break;
|
||||
// Delegate to cartItemsReducer.
|
||||
case types.RECEIVE_CART_ITEM:
|
||||
state = {
|
||||
...state,
|
||||
errors: EMPTY_CART_ERRORS,
|
||||
cartData: {
|
||||
...state.cartData,
|
||||
items: cartItemsReducer( state.cartData.items, action ),
|
||||
},
|
||||
};
|
||||
break;
|
||||
case types.UPDATING_CUSTOMER_DATA:
|
||||
state = {
|
||||
...state,
|
||||
metaData: {
|
||||
...state.metaData,
|
||||
updatingCustomerData: !! action.isResolving,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case types.UPDATING_SELECTED_SHIPPING_RATE:
|
||||
state = {
|
||||
...state,
|
||||
metaData: {
|
||||
...state.metaData,
|
||||
updatingSelectedRate: !! action.isResolving,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case types.SET_IS_CART_DATA_STALE:
|
||||
state = {
|
||||
...state,
|
||||
metaData: {
|
||||
...state.metaData,
|
||||
isCartDataStale: action.isCartDataStale,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export type State = ReturnType< typeof reducer >;
|
||||
|
||||
export default reducer;
|
36
packages/woocommerce-blocks/assets/js/data/cart/resolvers.ts
Normal file
36
packages/woocommerce-blocks/assets/js/data/cart/resolvers.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { select, apiFetch } from '@wordpress/data-controls';
|
||||
import { CartResponse, Cart } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { receiveCart, receiveError } from './actions';
|
||||
import { STORE_KEY, CART_API_ERROR } from './constants';
|
||||
|
||||
/**
|
||||
* Resolver for retrieving all cart data.
|
||||
*/
|
||||
export function* getCartData(): Generator< unknown, void, CartResponse > {
|
||||
const cartData = yield apiFetch( {
|
||||
path: '/wc/store/cart',
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
if ( ! cartData ) {
|
||||
yield receiveError( CART_API_ERROR );
|
||||
return;
|
||||
}
|
||||
|
||||
yield receiveCart( cartData );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolver for retrieving cart totals.
|
||||
*/
|
||||
export function* getCartTotals(): Generator< unknown, void, Cart > {
|
||||
yield select( STORE_KEY, 'getCartData' );
|
||||
}
|
165
packages/woocommerce-blocks/assets/js/data/cart/selectors.ts
Normal file
165
packages/woocommerce-blocks/assets/js/data/cart/selectors.ts
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Cart, CartTotals, CartMeta, CartItem } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CartState, defaultCartState } from '../default-states';
|
||||
import type { ResponseError } from '../types';
|
||||
|
||||
/**
|
||||
* Retrieves cart data from state.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {Cart} The data to return.
|
||||
*/
|
||||
export const getCartData = ( state: CartState ): Cart => {
|
||||
return state.cartData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves cart totals from state.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {CartTotals} The data to return.
|
||||
*/
|
||||
export const getCartTotals = ( state: CartState ): CartTotals => {
|
||||
return state.cartData.totals || defaultCartState.cartData.totals;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves cart meta from state.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {CartMeta} The data to return.
|
||||
*/
|
||||
export const getCartMeta = ( state: CartState ): CartMeta => {
|
||||
return state.metaData || defaultCartState.metaData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves cart errors from state.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {Array<ResponseError>} Array of errors.
|
||||
*/
|
||||
export const getCartErrors = ( state: CartState ): Array< ResponseError > => {
|
||||
return state.errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if any coupon is being applied.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {boolean} True if a coupon is being applied.
|
||||
*/
|
||||
export const isApplyingCoupon = ( state: CartState ): boolean => {
|
||||
return !! state.metaData.applyingCoupon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if cart is stale, false if it is not.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {boolean} True if the cart data is stale.
|
||||
*/
|
||||
export const isCartDataStale = ( state: CartState ): boolean => {
|
||||
return state.metaData.isCartDataStale;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the coupon code currently being applied.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {string} The data to return.
|
||||
*/
|
||||
export const getCouponBeingApplied = ( state: CartState ): string => {
|
||||
return state.metaData.applyingCoupon || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if any coupon is being removed.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {boolean} True if a coupon is being removed.
|
||||
*/
|
||||
export const isRemovingCoupon = ( state: CartState ): boolean => {
|
||||
return !! state.metaData.removingCoupon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the coupon code currently being removed.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {string} The data to return.
|
||||
*/
|
||||
export const getCouponBeingRemoved = ( state: CartState ): string => {
|
||||
return state.metaData.removingCoupon || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns cart item matching specified key.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @param {string} cartItemKey Key for a cart item.
|
||||
* @return {CartItem | void} Cart item object, or undefined if not found.
|
||||
*/
|
||||
export const getCartItem = (
|
||||
state: CartState,
|
||||
cartItemKey: string
|
||||
): CartItem | void => {
|
||||
return state.cartData.items.find(
|
||||
( cartItem ) => cartItem.key === cartItemKey
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the specified cart item quantity is being updated.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @param {string} cartItemKey Key for a cart item.
|
||||
* @return {boolean} True if a item has a pending request to be updated.
|
||||
*/
|
||||
export const isItemPendingQuantity = (
|
||||
state: CartState,
|
||||
cartItemKey: string
|
||||
): boolean => {
|
||||
return state.cartItemsPendingQuantity.includes( cartItemKey );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the specified cart item quantity is being updated.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @param {string} cartItemKey Key for a cart item.
|
||||
* @return {boolean} True if a item has a pending request to be updated.
|
||||
*/
|
||||
export const isItemPendingDelete = (
|
||||
state: CartState,
|
||||
cartItemKey: string
|
||||
): boolean => {
|
||||
return state.cartItemsPendingDelete.includes( cartItemKey );
|
||||
};
|
||||
/**
|
||||
* Retrieves if the address is being applied for shipping.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
* @return {boolean} are shipping rates loading.
|
||||
*/
|
||||
export const isCustomerDataUpdating = ( state: CartState ): boolean => {
|
||||
return !! state.metaData.updatingCustomerData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves if the shipping rate selection is being persisted.
|
||||
*
|
||||
* @param {CartState} state The current state.
|
||||
*
|
||||
* @return {boolean} True if the shipping rate selection is being persisted to
|
||||
* the server.
|
||||
*/
|
||||
export const isShippingRateBeingSelected = ( state: CartState ): boolean => {
|
||||
return !! state.metaData.updatingSelectedRate;
|
||||
};
|
118
packages/woocommerce-blocks/assets/js/data/cart/test/reducers.js
Normal file
118
packages/woocommerce-blocks/assets/js/data/cart/test/reducers.js
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import deepFreeze from 'deep-freeze';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import cartReducer from '../reducers';
|
||||
import { ACTION_TYPES as types } from '../action-types';
|
||||
|
||||
describe( 'cartReducer', () => {
|
||||
const originalState = deepFreeze( {
|
||||
cartData: {
|
||||
coupons: [],
|
||||
items: [],
|
||||
fees: [],
|
||||
itemsCount: 0,
|
||||
itemsWeight: 0,
|
||||
needsShipping: true,
|
||||
totals: {},
|
||||
},
|
||||
metaData: {},
|
||||
errors: [
|
||||
{
|
||||
code: '100',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
} );
|
||||
it( 'sets expected state when a cart is received', () => {
|
||||
const testAction = {
|
||||
type: types.RECEIVE_CART,
|
||||
response: {
|
||||
coupons: [],
|
||||
items: [],
|
||||
fees: [],
|
||||
itemsCount: 0,
|
||||
itemsWeight: 0,
|
||||
needsShipping: true,
|
||||
totals: {},
|
||||
},
|
||||
};
|
||||
const newState = cartReducer( originalState, testAction );
|
||||
expect( newState ).not.toBe( originalState );
|
||||
expect( newState.cartData ).toEqual( {
|
||||
coupons: [],
|
||||
items: [],
|
||||
fees: [],
|
||||
itemsCount: 0,
|
||||
itemsWeight: 0,
|
||||
needsShipping: true,
|
||||
totals: {},
|
||||
} );
|
||||
} );
|
||||
it( 'sets expected state when errors are replaced', () => {
|
||||
const testAction = {
|
||||
type: types.REPLACE_ERRORS,
|
||||
error: {
|
||||
code: '101',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
const newState = cartReducer( originalState, testAction );
|
||||
expect( newState ).not.toBe( originalState );
|
||||
expect( newState.errors ).toEqual( [
|
||||
{
|
||||
code: '101',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
] );
|
||||
} );
|
||||
it( 'sets expected state when an error is added', () => {
|
||||
const testAction = {
|
||||
type: types.RECEIVE_ERROR,
|
||||
error: {
|
||||
code: '101',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
const newState = cartReducer( originalState, testAction );
|
||||
expect( newState ).not.toBe( originalState );
|
||||
expect( newState.errors ).toEqual( [
|
||||
{
|
||||
code: '100',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
code: '101',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
] );
|
||||
} );
|
||||
it( 'sets expected state when a coupon is applied', () => {
|
||||
const testAction = {
|
||||
type: types.APPLYING_COUPON,
|
||||
couponCode: 'APPLYME',
|
||||
};
|
||||
const newState = cartReducer( originalState, testAction );
|
||||
expect( newState ).not.toBe( originalState );
|
||||
expect( newState.metaData.applyingCoupon ).toEqual( 'APPLYME' );
|
||||
} );
|
||||
it( 'sets expected state when a coupon is removed', () => {
|
||||
const testAction = {
|
||||
type: types.REMOVING_COUPON,
|
||||
couponCode: 'REMOVEME',
|
||||
};
|
||||
const newState = cartReducer( originalState, testAction );
|
||||
expect( newState ).not.toBe( originalState );
|
||||
expect( newState.metaData.removingCoupon ).toEqual( 'REMOVEME' );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getCartData } from '../resolvers';
|
||||
import { receiveCart, receiveError } from '../actions';
|
||||
import { CART_API_ERROR } from '../constants';
|
||||
|
||||
jest.mock( '@wordpress/data-controls' );
|
||||
|
||||
describe( 'getCartData', () => {
|
||||
describe( 'yields with expected responses', () => {
|
||||
let fulfillment;
|
||||
const rewind = () => ( fulfillment = getCartData() );
|
||||
test(
|
||||
'when apiFetch returns a valid response, yields expected ' +
|
||||
'action',
|
||||
() => {
|
||||
rewind();
|
||||
fulfillment.next( 'https://example.org' );
|
||||
const { value } = fulfillment.next( {
|
||||
coupons: [],
|
||||
items: [],
|
||||
fees: [],
|
||||
itemsCount: 0,
|
||||
itemsWeight: 0,
|
||||
needsShipping: true,
|
||||
totals: {},
|
||||
} );
|
||||
expect( value ).toEqual(
|
||||
receiveCart( {
|
||||
coupons: [],
|
||||
items: [],
|
||||
fees: [],
|
||||
itemsCount: 0,
|
||||
itemsWeight: 0,
|
||||
needsShipping: true,
|
||||
totals: {},
|
||||
} )
|
||||
);
|
||||
const { done } = fulfillment.next();
|
||||
expect( done ).toBe( true );
|
||||
}
|
||||
);
|
||||
} );
|
||||
describe( 'yields with expected response when there is an error', () => {
|
||||
let fulfillment;
|
||||
const rewind = () => ( fulfillment = getCartData() );
|
||||
test(
|
||||
'when apiFetch returns a valid response, yields expected ' +
|
||||
'action',
|
||||
() => {
|
||||
rewind();
|
||||
fulfillment.next( 'https://example.org' );
|
||||
const { value } = fulfillment.next( undefined );
|
||||
expect( value ).toEqual( receiveError( CART_API_ERROR ) );
|
||||
const { done } = fulfillment.next();
|
||||
expect( done ).toBe( true );
|
||||
}
|
||||
);
|
||||
} );
|
||||
} );
|
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
getCartData,
|
||||
getCartTotals,
|
||||
getCartMeta,
|
||||
getCartErrors,
|
||||
isApplyingCoupon,
|
||||
getCouponBeingApplied,
|
||||
isRemovingCoupon,
|
||||
getCouponBeingRemoved,
|
||||
} from '../selectors';
|
||||
|
||||
const state = {
|
||||
cartData: {
|
||||
coupons: [
|
||||
{
|
||||
code: 'test',
|
||||
totals: {
|
||||
currency_code: 'GBP',
|
||||
currency_symbol: '£',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '£',
|
||||
currency_suffix: '',
|
||||
total_discount: '583',
|
||||
total_discount_tax: '117',
|
||||
},
|
||||
},
|
||||
],
|
||||
items: [
|
||||
{
|
||||
key: '1f0e3dad99908345f7439f8ffabdffc4',
|
||||
id: 19,
|
||||
quantity: 1,
|
||||
name: 'Album',
|
||||
short_description: '<p>This is a simple, virtual product.</p>',
|
||||
description:
|
||||
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.</p>',
|
||||
sku: 'woo-album',
|
||||
low_stock_remaining: null,
|
||||
permalink: 'http://local.wordpress.test/product/album/',
|
||||
images: [
|
||||
{
|
||||
id: 48,
|
||||
src:
|
||||
'http://local.wordpress.test/wp-content/uploads/2019/12/album-1.jpg',
|
||||
thumbnail:
|
||||
'http://local.wordpress.test/wp-content/uploads/2019/12/album-1-324x324.jpg',
|
||||
srcset:
|
||||
'http://local.wordpress.test/wp-content/uploads/2019/12/album-1.jpg 800w, http://local.wordpress.test/wp-content/uploads/2019/12/album-1-324x324.jpg 324w, http://local.wordpress.test/wp-content/uploads/2019/12/album-1-100x100.jpg 100w, http://local.wordpress.test/wp-content/uploads/2019/12/album-1-416x416.jpg 416w, http://local.wordpress.test/wp-content/uploads/2019/12/album-1-300x300.jpg 300w, http://local.wordpress.test/wp-content/uploads/2019/12/album-1-150x150.jpg 150w, http://local.wordpress.test/wp-content/uploads/2019/12/album-1-768x768.jpg 768w',
|
||||
sizes: '(max-width: 800px) 100vw, 800px',
|
||||
name: 'album-1.jpg',
|
||||
alt: '',
|
||||
},
|
||||
],
|
||||
variation: [],
|
||||
totals: {
|
||||
currency_code: 'GBP',
|
||||
currency_symbol: '£',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '£',
|
||||
currency_suffix: '',
|
||||
line_subtotal: '1250',
|
||||
line_subtotal_tax: '250',
|
||||
line_total: '1000',
|
||||
line_total_tax: '200',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '6512bd43d9caa6e02c990b0a82652dca',
|
||||
id: 11,
|
||||
quantity: 1,
|
||||
name: 'Beanie',
|
||||
short_description: '<p>This is a simple product.</p>',
|
||||
description:
|
||||
'<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>',
|
||||
sku: 'woo-beanie',
|
||||
low_stock_remaining: null,
|
||||
permalink: 'http://local.wordpress.test/product/beanie/',
|
||||
images: [
|
||||
{
|
||||
id: 40,
|
||||
src:
|
||||
'http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2.jpg',
|
||||
thumbnail:
|
||||
'http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2-324x324.jpg',
|
||||
srcset:
|
||||
'http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2.jpg 801w, http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2-324x324.jpg 324w, http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2-100x100.jpg 100w, http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2-416x416.jpg 416w, http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2-300x300.jpg 300w, http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2-150x150.jpg 150w, http://local.wordpress.test/wp-content/uploads/2019/12/beanie-2-768x768.jpg 768w',
|
||||
sizes: '(max-width: 801px) 100vw, 801px',
|
||||
name: 'beanie-2.jpg',
|
||||
alt: '',
|
||||
},
|
||||
],
|
||||
variation: [],
|
||||
totals: {
|
||||
currency_code: 'GBP',
|
||||
currency_symbol: '£',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '£',
|
||||
currency_suffix: '',
|
||||
line_subtotal: '1667',
|
||||
line_subtotal_tax: '333',
|
||||
line_total: '1333',
|
||||
line_total_tax: '267',
|
||||
},
|
||||
},
|
||||
],
|
||||
items_count: 2,
|
||||
items_weight: 0,
|
||||
needs_payment: true,
|
||||
needs_shipping: true,
|
||||
totals: {
|
||||
currency_code: 'GBP',
|
||||
currency_symbol: '£',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '£',
|
||||
currency_suffix: '',
|
||||
total_items: '2917',
|
||||
total_items_tax: '583',
|
||||
total_fees: '0',
|
||||
total_fees_tax: '0',
|
||||
total_discount: '583',
|
||||
total_discount_tax: '117',
|
||||
total_shipping: '2000',
|
||||
total_shipping_tax: '400',
|
||||
total_price: '5200',
|
||||
total_tax: '867',
|
||||
tax_lines: [
|
||||
{
|
||||
name: 'Tax',
|
||||
price: '867',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
metaData: {
|
||||
applyingCoupon: 'test-coupon',
|
||||
removingCoupon: 'test-coupon2',
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
code: '100',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe( 'getCartData', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( getCartData( state ) ).toEqual( state.cartData );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getCartTotals', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( getCartTotals( state ) ).toEqual( state.cartData.totals );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getCartMeta', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( getCartMeta( state ) ).toEqual( state.metaData );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getCartErrors', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( getCartErrors( state ) ).toEqual( state.errors );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isApplyingCoupon', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( isApplyingCoupon( state ) ).toEqual( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getCouponBeingApplied', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( getCouponBeingApplied( state ) ).toEqual(
|
||||
state.metaData.applyingCoupon
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isRemovingCoupon', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( isRemovingCoupon( state ) ).toEqual( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getCouponBeingRemoved', () => {
|
||||
it( 'returns expected values for items existing in state', () => {
|
||||
expect( getCouponBeingRemoved( state ) ).toEqual(
|
||||
state.metaData.removingCoupon
|
||||
);
|
||||
} );
|
||||
} );
|
Reference in New Issue
Block a user