Skip to content

feat(item): update id type and optional price #148

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import * as React from "react";
import useLocalStorage from "./useLocalStorage";

export interface Item {
id: string;
price: number;
id: string | number;
price?: number;
quantity?: number;
itemTotal?: number;
[key: string]: any;
Expand Down Expand Up @@ -155,11 +155,11 @@ const generateCartState = (state = initialState, items: Item[]) => {
const calculateItemTotals = (items: Item[]) =>
items.map(item => ({
...item,
itemTotal: item.price * item.quantity!,
itemTotal: item.price! * item.quantity!,
}));

const calculateTotal = (items: Item[]) =>
items.reduce((total, item) => total + item.quantity! * item.price, 0);
items.reduce((total, item) => total + item.quantity! * item.price!, 0);

const calculateTotalItems = (items: Item[]) =>
items.reduce((sum, item) => sum + item.quantity!, 0);
Expand Down Expand Up @@ -221,26 +221,40 @@ export const CartProvider: React.FC<{
onSetItems && onSetItems(items);
};

const addItem = (item: Item, quantity = 1) => {
const addItem = (item: Item) => {
if (!item.id) throw new Error("You must provide an `id` for items");
if (quantity <= 0) return;
if (!item.price) {
item.price = 0;
}
if (!item.quantity) {
item.quantity = 1;
}

const currentItem = state.items.find((i: Item) => i.id === item.id);
if (item.price < 0 || item.quantity < 1) return;

if (!currentItem && !item.hasOwnProperty("price"))
throw new Error("You must pass a `price` for new items");
const currentItem = state.items.find((i: Item) => i.id === item.id);

// if item doesn't in cart, add it
if (!currentItem) {
const payload = { ...item, quantity };
const payload = {
...item,
};

dispatch({ type: "ADD_ITEM", payload });
dispatch({
type: "ADD_ITEM",
payload,
});

onItemAdd && onItemAdd(payload);

return;
}

const payload = { ...item, quantity: currentItem.quantity + quantity };
// if item existed in the cart
const payload = {
...item,
quantity: currentItem.quantity + item.quantity,
};

dispatch({
type: "UPDATE_ITEM",
Expand Down Expand Up @@ -297,7 +311,7 @@ export const CartProvider: React.FC<{
dispatch({ type: "EMPTY_CART" });

onEmptyCart && onEmptyCart();
}
};

const getItem = (id: Item["id"]) =>
state.items.find((i: Item) => i.id === id);
Expand Down
123 changes: 103 additions & 20 deletions test/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { act, renderHook } from "@testing-library/react-hooks";
import React, { FC, HTMLAttributes, ReactChild } from "react";
import {
CartProvider,
createCartIdentifier,
initialState,
useCart,
} from "../src";
import React, { FC, HTMLAttributes, ReactChild } from "react";
import { act, renderHook } from "@testing-library/react-hooks";

export interface Props extends HTMLAttributes<HTMLDivElement> {
children?: ReactChild;
Expand Down Expand Up @@ -95,13 +95,69 @@ describe("addItem", () => {
wrapper: CartProvider,
});

const item = { id: "test", price: 1000 };
const item = { id: 1, price: 1000, quantity: 2 };

act(() => result.current.addItem(item));

expect(result.current.items).toHaveLength(1);
expect(result.current.totalItems).toBe(2);
expect(result.current.totalUniqueItems).toBe(1);
expect(result.current.items).toContainEqual(
expect.objectContaining({ id: 1, price: 1000, quantity: 2 })
);
});

test("adds item to the cart without quantity", () => {
const { result } = renderHook(() => useCart(), {
wrapper: CartProvider,
});

const item = { id: 1, price: 1000 };

act(() => result.current.addItem(item));

expect(result.current.items).toHaveLength(1);
expect(result.current.totalItems).toBe(1);
expect(result.current.totalUniqueItems).toBe(1);
expect(result.current.items).toContainEqual(
expect.objectContaining({ id: 1, price: 1000, quantity: 1 })
);
});

test("adds item to the cart without price", () => {
const { result } = renderHook(() => useCart(), {
wrapper: CartProvider,
});

const item = { id: "test" };

act(() => result.current.addItem(item));

expect(result.current.items).toHaveLength(1);
expect(result.current.totalItems).toBe(1);
expect(result.current.totalUniqueItems).toBe(1);
});

test("adds item to the cart with Int ID", () => {
const { result } = renderHook(() => useCart(), {
wrapper: CartProvider,
});

const item = { id: 1 };

act(() => result.current.addItem(item));

expect(result.current.items).toHaveLength(1);
expect(result.current.totalItems).toBe(1);
expect(result.current.totalUniqueItems).toBe(1);
expect(result.current.items).toContainEqual(
expect.objectContaining({
id: 1,
quantity: 1,
price: 0,
itemTotal: 0,
})
);
});

test("increments existing item quantity in the cart", () => {
Expand Down Expand Up @@ -205,7 +261,7 @@ describe("addItem", () => {

describe("updateItem", () => {
test("updates cart meta state", () => {
const items = [{ id: "test", price: 1000 }];
const items = [{ id: 1, price: 1000 }];
const [item] = items;

const wrapper: FC<Props> = ({ children }) => (
Expand All @@ -231,7 +287,7 @@ describe("updateItem", () => {
test("triggers onItemUpdate when updating existing item", () => {
let called = false;

const item = { id: "test", price: 1000 };
const item = { id: 1, price: 1000 };

const wrapper: FC<Props> = ({ children }) => (
<CartProvider defaultItems={[item]} onItemUpdate={() => (called = true)}>
Expand All @@ -251,7 +307,7 @@ describe("updateItem", () => {

describe("updateItemQuantity", () => {
test("updates cart meta state", () => {
const items = [{ id: "test", price: 1000 }];
const items = [{ id: 1, price: 1000 }];
const [item] = items;

const wrapper: FC<Props> = ({ children }) => (
Expand All @@ -273,7 +329,7 @@ describe("updateItemQuantity", () => {
test("triggers onItemUpdate when setting quantity above 0", () => {
let called = false;

const item = { id: "test", price: 1000 };
const item = { id: 1, price: 1000 };

const wrapper: FC<Props> = ({ children }) => (
<CartProvider defaultItems={[item]} onItemUpdate={() => (called = true)}>
Expand All @@ -294,7 +350,7 @@ describe("updateItemQuantity", () => {
test("triggers onItemRemove when setting quantity to 0", () => {
let called = false;

const item = { id: "test", price: 1000 };
const item = { id: 1, price: 1000 };

const wrapper: FC<Props> = ({ children }) => (
<CartProvider defaultItems={[item]} onItemRemove={() => (called = true)}>
Expand All @@ -313,7 +369,7 @@ describe("updateItemQuantity", () => {
});

test("recalculates itemTotal when incrementing item quantity", () => {
const item = { id: "test", price: 1000 };
const item = { id: 1, price: 1000 };

const { result } = renderHook(() => useCart(), {
wrapper: CartProvider,
Expand All @@ -328,8 +384,24 @@ describe("updateItemQuantity", () => {
);
});

test("recalculates itemTotal when incrementing item quantity but not given price", () => {
const item = { id: 1 };

const { result } = renderHook(() => useCart(), {
wrapper: CartProvider,
});

act(() => result.current.addItem(item));
act(() => result.current.updateItemQuantity(item.id, 2));

expect(result.current.items).toHaveLength(1);
expect(result.current.items).toContainEqual(
expect.objectContaining({ itemTotal: 0, quantity: 2 })
);
});

test("recalculates itemTotal when decrementing item quantity", () => {
const item = { id: "test", price: 1000, quantity: 2 };
const item = { id: 1, price: 1000, quantity: 2 };

const { result } = renderHook(() => useCart(), {
wrapper: CartProvider,
Expand All @@ -347,7 +419,7 @@ describe("updateItemQuantity", () => {

describe("removeItem", () => {
test("updates cart meta state", () => {
const items = [{ id: "test", price: 1000 }];
const items = [{ id: 1, price: 1000 }];
const [item] = items;

const wrapper: FC<Props> = ({ children }) => (
Expand All @@ -369,7 +441,7 @@ describe("removeItem", () => {
test("triggers onItemRemove when removing item", () => {
let called = false;

const item = { id: "test", price: 1000 };
const item = { id: 1, price: 1000 };

const wrapper: FC<Props> = ({ children }) => (
<CartProvider defaultItems={[item]} onItemRemove={() => (called = true)}>
Expand All @@ -389,7 +461,7 @@ describe("removeItem", () => {

describe("emptyCart", () => {
test("updates cart meta state", () => {
const items = [{ id: "test", price: 1000 }];
const items = [{ id: 1, price: 1000 }];

const wrapper: FC<Props> = ({ children }) => (
<CartProvider defaultItems={items}>{children}</CartProvider>
Expand Down Expand Up @@ -509,8 +581,8 @@ describe("updateCartMetadata", () => {
describe("setItems", () => {
test("set cart items state", () => {
const items = [
{ id: "test", price: 1000 },
{ id: "test2", price: 2000 },
{ id: 1, price: 1000 },
{ id: "2", price: 2000 },
];

const wrapper: FC<Props> = ({ children }) => (
Expand All @@ -526,13 +598,16 @@ describe("setItems", () => {
expect(result.current.totalUniqueItems).toBe(2);
expect(result.current.isEmpty).toBe(false);
expect(result.current.items).toContainEqual(
expect.objectContaining({ id: "test2", price: 2000, quantity: 1 })
expect.objectContaining({ id: 1, price: 1000, quantity: 1 })
);
expect(result.current.items).toContainEqual(
expect.objectContaining({ id: "2", price: 2000, quantity: 1 })
);
});
test("add custom quantities with setItems", () => {
const items = [
{ id: "test", price: 1000, quantity: 2 },
{ id: "test2", price: 2000, quantity: 1 },
{ id: 1, price: 1000, quantity: 2 },
{ id: "2", price: 2000, quantity: 1 },
];
const wrapper: FC<Props> = ({ children }) => (
<CartProvider defaultItems={[]}>{children}</CartProvider>
Expand All @@ -545,7 +620,14 @@ describe("setItems", () => {
expect(result.current.items).toHaveLength(2);
expect(result.current.totalItems).toBe(3);
expect(result.current.totalUniqueItems).toBe(2);
expect(result.current.items).toContainEqual(
expect.objectContaining({ id: 1, price: 1000, quantity: 2 })
);
expect(result.current.items).toContainEqual(
expect.objectContaining({ id: "2", price: 2000, quantity: 1 })
);
});

test("current items is replaced when setItems has been called with a new set of items", () => {
const itemToBeReplaced = { id: "test", price: 1000 };
const wrapper: FC<Props> = ({ children }) => (
Expand All @@ -555,15 +637,16 @@ describe("setItems", () => {
wrapper,
});
const items = [
{ id: "test2", price: 2000 },
{ id: "test3", price: 3000 },
{ id: 2, price: 2000 },
{ id: "3", price: 3000 },
];
act(() => result.current.setItems(items));
expect(result.current.items).toHaveLength(2);
expect(result.current.items).not.toContainEqual(
expect.objectContaining(itemToBeReplaced)
);
});

test("trigger onSetItems when setItems is called", () => {
let called = false;

Expand Down