Skip to content
This repository has been archived by the owner on Oct 31, 2024. It is now read-only.

Commit

Permalink
feat: cart errors handling (#1422)
Browse files Browse the repository at this point in the history

Co-authored-by: patzick <13100280+patzick@users.noreply.github.com>
  • Loading branch information
mkucmus and patzick authored Apr 20, 2021
1 parent 402c4f1 commit 90dc4c0
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 38 deletions.
5 changes: 5 additions & 0 deletions api/composables.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CustomerResetPasswordParam } from '@shopware-pwa/shopware-6-client';
import { CustomerUpdateEmailParam } from '@shopware-pwa/shopware-6-client';
import { CustomerUpdatePasswordParam } from '@shopware-pwa/shopware-6-client';
import { CustomerUpdateProfileParam } from '@shopware-pwa/shopware-6-client';
import { EntityError } from '@shopware-pwa/commons/interfaces/models/common/EntityError';
import { EqualsFilter } from '@shopware-pwa/commons/interfaces/search/SearchFilter';
import { GuestOrderParams } from '@shopware-pwa/commons/interfaces/request/GuestOrderParams';
import { Includes } from '@shopware-pwa/commons/interfaces/search/SearchCriteria';
Expand Down Expand Up @@ -184,6 +185,8 @@ export const INTERCEPTOR_KEYS: {
ADD_TO_WISHLIST: string;
ADD_PROMOTION_CODE: string;
ERROR: string;
WARNING: string;
NOTICE: string;
ORDER_PLACE: string;
SESSION_SET_CURRENCY: string;
SESSION_SET_PAYMENT_METHOD: string;
Expand Down Expand Up @@ -221,6 +224,8 @@ export interface IUseCart {
// (undocumented)
cart: ComputedRef<Cart | null>;
// (undocumented)
cartErrors: ComputedRef<EntityError[]>;
// (undocumented)
cartItems: ComputedRef<LineItem[]>;
// (undocumented)
changeProductQuantity: ({ id, quantity, }: {
Expand Down
2 changes: 2 additions & 0 deletions docs/landing/resources/api/composables.interceptor_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ INTERCEPTOR_KEYS: {
ADD_TO_WISHLIST: string;
ADD_PROMOTION_CODE: string;
ERROR: string;
WARNING: string;
NOTICE: string;
ORDER_PLACE: string;
SESSION_SET_CURRENCY: string;
SESSION_SET_PAYMENT_METHOD: string;
Expand Down
14 changes: 14 additions & 0 deletions docs/landing/resources/api/composables.iusecart.carterrors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@shopware-pwa/composables](./composables.md) &gt; [IUseCart](./composables.iusecart.md) &gt; [cartErrors](./composables.iusecart.carterrors.md)

## IUseCart.cartErrors property

> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.
>
<b>Signature:</b>

```typescript
cartErrors: ComputedRef<EntityError[]>;
```
1 change: 1 addition & 0 deletions docs/landing/resources/api/composables.iusecart.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface IUseCart
| [addPromotionCode](./composables.iusecart.addpromotioncode.md) | (promotionCode: string) =&gt; Promise&lt;void&gt; | <b><i>(BETA)</i></b> |
| [appliedPromotionCodes](./composables.iusecart.appliedpromotioncodes.md) | ComputedRef&lt;LineItem\[\]&gt; | <b><i>(BETA)</i></b> |
| [cart](./composables.iusecart.cart.md) | ComputedRef&lt;Cart \| null&gt; | <b><i>(BETA)</i></b> |
| [cartErrors](./composables.iusecart.carterrors.md) | ComputedRef&lt;EntityError\[\]&gt; | <b><i>(BETA)</i></b> |
| [cartItems](./composables.iusecart.cartitems.md) | ComputedRef&lt;LineItem\[\]&gt; | <b><i>(BETA)</i></b> |
| [changeProductQuantity](./composables.iusecart.changeproductquantity.md) | ({ id, quantity, }: { id: string; quantity: number; }) =&gt; void | <b><i>(BETA)</i></b> |
| [count](./composables.iusecart.count.md) | ComputedRef&lt;number&gt; | <b><i>(BETA)</i></b> |
Expand Down
13 changes: 10 additions & 3 deletions packages/commons/interfaces/models/checkout/cart/Cart.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { LineItem } from "./line-item/LineItem";
import { CartPrice } from "./price/CartPrice";
import { Error } from "./error/Error";
import { Delivery } from "../delivery/Delivery";
import { Transaction } from "./transaction/Transaction";
import { EntityError } from "../../common/EntityError";

/**
* @alpha
* @beta
*/
export interface CartErrors {
[key: string]: EntityError;
}

/**
* @beta
*/
export interface Cart {
name: string;
token: string;
price: CartPrice;
lineItems: LineItem[];
errors: Error[];
errors: CartErrors;
deliveries: Delivery[];
transactions: Transaction[];
modified: boolean;
Expand Down
27 changes: 27 additions & 0 deletions packages/commons/interfaces/models/common/EntityError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @beta
*/
export enum ErrorLevel {
NOTICE = 0,
WARNING = 10,
ERROR = 20,
}

/**
* @beta
*/
export interface EntityError {
id: string;
name: string;
quantity: number;
message: string;
code: number;
key: string;
level: ErrorLevel | number;
messageKey:
| "product-stock-reached"
| "product-out-of-stock"
| "product-not-found"
| "purchase-steps-quantity"
| string;
}
76 changes: 76 additions & 0 deletions packages/composables/__tests__/helpers/errorHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { broadcastErrors } from "../../src/internalHelpers/errorHandler";
import * as Composables from "@shopware-pwa/composables";
import { EntityError } from "@shopware-pwa/commons/interfaces/models/common/EntityError";
jest.mock("@shopware-pwa/composables");
const mockedComposables = Composables as jest.Mocked<typeof Composables>;
jest.spyOn(console, "error");
describe("composables errorHandler", () => {
const broadcastMock = jest.fn();

beforeEach(() => {
mockedComposables.getApplicationContext.mockImplementation(() => {
return {} as any;
});
mockedComposables.useIntercept.mockImplementation(() => {
return {
broadcast: broadcastMock,
} as any;
});
});
describe("broadcastErrors", () => {
it("should do nothing if some of arguments does not match the interface", () => {
broadcastErrors(undefined as any, "testMethod", {} as any);
expect(broadcastMock).toBeCalledTimes(0);
});
it("should broadcast errors if any provided", () => {
const errors: EntityError[] = [
{
id: "someId",
name: "product-stock-reached",
quantity: 1,
message: "you reached the available quantity of the product",
code: 10,
key: "product-stock-reached-someId",
level: 0,
messageKey: "product-stock-reached",
},
{
id: "someId",
name: "product-stock-reached",
quantity: 1,
message: "you reached the available quantity of the product",
code: 30,
key: "product-stock-reached-someId",
level: 10,
messageKey: "product-stock-reached",
},
{
id: "someId",
name: "product-stock-reached",
quantity: 1,
message: "you reached the available quantity of the product",
code: 10,
key: "product-stock-reached-someId",
level: 20,
messageKey: "product-stock-reached",
},
];
broadcastErrors(errors, "testMethod", {} as any);
expect(broadcastMock).toBeCalledTimes(3);
expect(broadcastMock).toBeCalledWith("notice", {
inputParams: {},
methodName: "testMethod",
notice: {
code: 10,
id: "someId",
key: "product-stock-reached-someId",
level: 0,
message: "you reached the available quantity of the product",
messageKey: "product-stock-reached",
name: "product-stock-reached",
quantity: 1,
},
});
});
});
});
163 changes: 130 additions & 33 deletions packages/composables/__tests__/useCart.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ const mockedShopwareClient = shopwareClient as jest.Mocked<
typeof shopwareClient
>;
const consoleWarnSpy = jest.spyOn(console, "warn");

const consoleErrorSpy = jest.spyOn(console, "error");
import * as Composables from "@shopware-pwa/composables";
jest.mock("@shopware-pwa/composables");
const mockedComposables = Composables as jest.Mocked<typeof Composables>;

import * as ErrorHandler from "../src/internalHelpers/errorHandler";
const mockedErrorHandler = ErrorHandler as jest.Mocked<typeof ErrorHandler>;
jest.mock("@shopware-pwa/composables/src/internalHelpers/errorHandler");

import { useCart } from "../src/hooks/useCart";

describe("Composables - useCart", () => {
Expand All @@ -28,6 +32,7 @@ describe("Composables - useCart", () => {
jest.resetAllMocks();
stateCart.value = null;
consoleWarnSpy.mockImplementationOnce(() => {});
consoleErrorSpy.mockImplementationOnce(() => {});

mockedComposables.getApplicationContext.mockImplementation(() => {
return {
Expand All @@ -41,6 +46,7 @@ describe("Composables - useCart", () => {
} as any;
});

mockedErrorHandler.broadcastErrors.mockImplementation(() => jest.fn());
mockedComposables.useIntercept.mockImplementation(() => {
return {
broadcast: broadcastMock,
Expand All @@ -49,44 +55,72 @@ describe("Composables - useCart", () => {
});
});
describe("computed", () => {
describe("shippingTotal", () => {
it("should return default value 0 (zero) if cart is empty", () => {
describe("cartErrors", () => {
it("should return default value [] (empty array) if cart is empty", () => {
stateCart.value = undefined as any;
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(0);
});
it("should return default value 0 (zero) if there is no delivery in cart", () => {
stateCart.value = {
deliveries: undefined,
};
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(0);
const { cartErrors } = useCart(rootContextMock);
expect(cartErrors.value).toStrictEqual([]);
});
it("should return default value 0 (zero) if shipping costs are empty", () => {
it("should return array of {EntityError} typed Objects", () => {
stateCart.value = {
deliveries: [
{
shippingCosts: undefined,
errors: {
"error-1": {
key: "error-1",
},
],
};
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(0);
});
it("should return total price from shipping cost of the first delivery from cart", () => {
stateCart.value = {
deliveries: [
{
shippingCosts: {
totalPrice: 199.5,
},
"error-2": {
key: "error-2",
},
],
};
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(199.5);
},
} as any;
const { cartErrors } = useCart(rootContextMock);
expect(cartErrors.value).toStrictEqual([
{
key: "error-1",
},
{
key: "error-2",
},
]);
});
}),
describe("shippingTotal", () => {
it("should return default value 0 (zero) if cart is empty", () => {
stateCart.value = undefined as any;
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(0);
});
it("should return default value 0 (zero) if there is no delivery in cart", () => {
stateCart.value = {
deliveries: undefined,
};
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(0);
});
it("should return default value 0 (zero) if shipping costs are empty", () => {
stateCart.value = {
deliveries: [
{
shippingCosts: undefined,
},
],
};
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(0);
});
it("should return total price from shipping cost of the first delivery from cart", () => {
stateCart.value = {
deliveries: [
{
shippingCosts: {
totalPrice: 199.5,
},
},
],
};
const { shippingTotal } = useCart(rootContextMock);
expect(shippingTotal.value).toBe(199.5);
});
});
});
describe("cart", () => {
it("should be null on not loaded cart", () => {
stateCart.value = null;
Expand Down Expand Up @@ -378,5 +412,68 @@ describe("Composables - useCart", () => {
);
});
});
describe("broadcastUpcomingErrors", () => {
it("should catch the exception while the errors are trying to be broadcasted", async () => {
const { addProduct } = useCart(rootContextMock);
mockedShopwareClient.addProductToCart.mockResolvedValueOnce({
errors: {
"error-1": {
key: "some-error-key",
},
},
} as any);
mockedErrorHandler.broadcastErrors.mockImplementation(() => {
throw new Error("An error occured");
});
await addProduct({ id: "someId", quantity: 1 });
expect(consoleErrorSpy).toBeCalledWith(
"[useCart][broadcastUpcomingErrors]",
expect.any(Object)
);
});
it("should not invoke broadcastErrors helper when there is no new cart result - changeCartItemQuantity", async () => {
const { changeProductQuantity } = useCart(rootContextMock);
mockedShopwareClient.changeCartItemQuantity.mockResolvedValueOnce(
undefined as any
);
await changeProductQuantity({ id: "qwerty", quantity: 6 });
expect(mockedErrorHandler.broadcastErrors).toBeCalledTimes(0);
});
it("should not invoke broadcastErrors helper when there is no new cart result - addProduct", async () => {
const { addProduct } = useCart(rootContextMock);
mockedShopwareClient.addProductToCart.mockResolvedValueOnce(
undefined as any
);
await addProduct({ id: "qwerty", quantity: 6 });
expect(mockedErrorHandler.broadcastErrors).toBeCalledTimes(0);
});
it("should not invoke broadcastErrors helper when there is no new cart result - removeItem", async () => {
const { removeItem } = useCart(rootContextMock);
mockedShopwareClient.removeCartItem.mockResolvedValueOnce(
undefined as any
);
await removeItem({ referencedId: "qwerty" } as any);
expect(mockedErrorHandler.broadcastErrors).toBeCalledTimes(0);
});
it("should invoke broadcastErrors helper once there is a new error in cart response", async () => {
const { removeItem } = useCart(rootContextMock);
mockedShopwareClient.removeCartItem.mockResolvedValueOnce({
errors: {
"error-id": {
id: "error-id",
name: "product stock reached",
quantity: 55,
message: "too many products added to cart",
code: 0,
key: "product-stock-reached-productId",
level: 10,
messageKey: "product-stock-reached",
},
},
} as any);
await removeItem({ referencedId: "qwerty" } as any);
expect(mockedErrorHandler.broadcastErrors).toBeCalledTimes(1);
});
});
});
});
Loading

1 comment on commit 90dc4c0

@vercel
Copy link

@vercel vercel bot commented on 90dc4c0 Apr 20, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.