From f393c193d8cac7da5fbce8918bdf4911c6a321d7 Mon Sep 17 00:00:00 2001 From: Chayoot Kosiwanich <60844731+khunfloat@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:01:51 +0700 Subject: [PATCH 1/2] feat(item): update id type and optional price update id type to string | number and make price optional with default value of 0 --- src/index.tsx | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 58f74188..383e9e6d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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; @@ -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); @@ -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", @@ -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); From ad3c99172e9556dc6180ba22a8f8ab87f86aca89 Mon Sep 17 00:00:00 2001 From: Chayoot Kosiwanich <60844731+khunfloat@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:02:53 +0700 Subject: [PATCH 2/2] feat(item): update id type and optional price testcase --- test/index.test.tsx | 123 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 20 deletions(-) diff --git a/test/index.test.tsx b/test/index.test.tsx index 1ae4892a..de4c165c 100644 --- a/test/index.test.tsx +++ b/test/index.test.tsx @@ -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 { children?: ReactChild; @@ -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", () => { @@ -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 = ({ children }) => ( @@ -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 = ({ children }) => ( (called = true)}> @@ -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 = ({ children }) => ( @@ -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 = ({ children }) => ( (called = true)}> @@ -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 = ({ children }) => ( (called = true)}> @@ -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, @@ -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, @@ -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 = ({ children }) => ( @@ -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 = ({ children }) => ( (called = true)}> @@ -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 = ({ children }) => ( {children} @@ -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 = ({ children }) => ( @@ -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 = ({ children }) => ( {children} @@ -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 = ({ children }) => ( @@ -555,8 +637,8 @@ 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); @@ -564,6 +646,7 @@ describe("setItems", () => { expect.objectContaining(itemToBeReplaced) ); }); + test("trigger onSetItems when setItems is called", () => { let called = false;