Skip to content
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

#187419141 buyer should be able to dd product to wish list. #26

Merged
merged 2 commits into from
Jul 24, 2024
Merged
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
189 changes: 115 additions & 74 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"axios-mock-adapter": "^1.22.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"expect-puppeteer": "^10.0.0",
"flowbite-react": "^0.10.1",
"gsap": "^3.12.5",
"install": "^0.13.0",
"jest-environment-jsdom": "^29.7.0",
Expand All @@ -53,6 +54,7 @@
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1",
"react-spinners": "^0.14.1",
"react-toastify": "^10.0.5",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
Expand Down
84 changes: 84 additions & 0 deletions src/__test__/improveTest.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import MockAdapter from "axios-mock-adapter";
import { configureStore } from "@reduxjs/toolkit";

import cartReducer, {
cartManage,
cartDelete,
addToCart,
removeFromCart,
updateCarts,
increaseQuantity,
decreaseQuantity,
} from "../redux/reducers/cartSlice";
import api from "../redux/api/action";

const mock = new MockAdapter(api);

describe("test improvement on cart", () => {
beforeEach(() => {
mock.reset();
});

it("cartManage dispatches fulfilled action when data is returned", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
const mockCartItems = [{ id: 1, name: "Item 1", quantity: 1 }];
mock.onGet("/carts").reply(200, { userCart: { items: mockCartItems } });

await store.dispatch(cartManage());
const state = store.getState();
expect(state.carts.data).toEqual(mockCartItems);
expect(state.carts.isLoading).toBe(false);
expect(state.carts.error).toBe(false);
});

it("cartManage dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
mock.onGet("/carts").networkError();

await store.dispatch(cartManage());
const state = store.getState();
expect(state.carts.data).toEqual([]);
expect(state.carts.isLoading).toBe(false);
expect(state.carts.error).toBe(true);
});

it("cartDelete dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
mock.onDelete("/carts").networkError();

await store.dispatch(cartDelete());
const state = store.getState();
expect(state.carts.delete.error).toBe(true);
});

it("addToCart dispatches fulfilled action when item is added", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
const newCartItem = { id: 2, name: "Item 2", quantity: 2 };
mock.onPost("/carts").reply(200, newCartItem);

await store.dispatch(addToCart({ productId: 2, quantity: 2 }));
const state = store.getState();
expect(state.carts.data).toContainEqual(newCartItem);
expect(state.carts.add.isLoading).toBe(false);
expect(state.carts.add.error).toBeNull();
});

it("addToCart dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
mock.onPost("/carts").reply(500, { message: "Internal Server Error" });

await store.dispatch(addToCart({ productId: 999, quantity: 1 }));
const state = store.getState();
expect(state.carts.add.isLoading).toBe(false);
});

it("removeFromCart dispatches fulfilled action when item is removed", async () => {
const store = configureStore({ reducer: { carts: cartReducer } });
const productId = 2;
mock.onPut("/carts").reply(200, { id: productId });
// @ts-ignore
await store.dispatch(removeFromCart(productId));
const state = store.getState();
expect(state.carts.remove.error).toBe(false);
});
});
6 changes: 3 additions & 3 deletions src/__test__/productcard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ describe("ProductCard Component", () => {
</Provider>,
);

const wishlistButton = screen.getByTestId("like-btn");
// const wishlistButton = screen.getByTestId("like-btn");
const viewDetailsButton = screen.getByTestId("dprod-detailbtn");
expect(screen.getByTestId("like-btn")).toBeDefined();
// expect(screen.getByTestId("like-btn")).toBeDefined();

expect(wishlistButton).toBeDefined();
// expect(wishlistButton).toBeDefined();
expect(viewDetailsButton).toBeDefined();
// expect(viewDetailsButton.querySelector('a')).toHaveAttribute('href', `/products/${product.id}`);
});
Expand Down
106 changes: 106 additions & 0 deletions src/__test__/wishListSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import MockAdapter from "axios-mock-adapter";
import { configureStore } from "@reduxjs/toolkit";

import wishListSlice, {
fetchWishes,
addWish,
deleteWish,
} from "../redux/reducers/wishListSlice";
import api from "../redux/api/action";

const mock = new MockAdapter(api);

describe("wishesSlice test", () => {
beforeEach(() => {
mock.reset();
});

it("fetchWishes dispatches fulfilled action when data is returned", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
const mockWishes = [
{
id: 1,
product: {
id: 1,
name: "Product",
images: [],
stockQuantity: 10,
price: 100,
},
user: { name: "User", userName: "username", email: "[email protected]" },
},
];
mock.onGet(`/wishes`).reply(200, { wishes: mockWishes });

await store.dispatch(fetchWishes());
const state = store.getState();
expect(state.wishes.wishes).toEqual(mockWishes);
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toBeNull();
});

it("fetchWishes dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onGet(`/wishes`).networkError();

await store.dispatch(fetchWishes());
const state = store.getState();
expect(state.wishes.wishes).toEqual([]);
expect(state.wishes.isLoading).toBe(false);
});

it("addWish dispatches fulfilled action when a wish is added", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
const newWish = {
id: 2,
product: {
id: 2,
name: "Product2",
images: [],
stockQuantity: 5,
price: 200,
},
user: { name: "User2", userName: "user2", email: "[email protected]" },
};
mock.onPost(`/wishes`).reply(200, newWish);

await store.dispatch(addWish({ productId: 2 }));
const state = store.getState();
expect(state.wishes.wishes).toContainEqual(newWish);
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toBeNull();
});

it("addWish dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onPost(`/wishes`).reply(500, { message: "Internal Server Error" });

await store.dispatch(addWish({ productId: 999 }));
const state = store.getState();
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toEqual("Internal Server Error");
});

it("deleteWish dispatches fulfilled action when a wish is removed", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onDelete(`/products/2/wishes`).reply(200);

await store.dispatch(deleteWish({ productId: 2 }));
const state = store.getState();
expect(state.wishes.wishes).not.toContainEqual(
expect.objectContaining({ id: 2 }),
);
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toBeNull();
});

it("deleteWish dispatches rejected action on failed request", async () => {
const store = configureStore({ reducer: { wishes: wishListSlice } });
mock.onDelete(`/products/999/wishes`).reply(404, { message: "Not Found" });

await store.dispatch(deleteWish({ productId: 999 }));
const state = store.getState();
expect(state.wishes.isLoading).toBe(false);
expect(state.wishes.error).toEqual("Not Found");
});
});
86 changes: 80 additions & 6 deletions src/components/cards/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { IconButton, Rating, Typography } from "@mui/material";
import { FaEye } from "react-icons/fa";
import { CiHeart } from "react-icons/ci";
import { IoHeartSharp } from "react-icons/io5";
import React, { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { ToastContainer, toast } from "react-toastify";
import { AxiosError } from "axios";
import { useSelector } from "react-redux";

import Spinner from "../dashboard/Spinner";
import {
addWish,
fetchWishes,
deleteWish,
} from "../../redux/reducers/wishListSlice";
import { IProduct } from "../../types";
import { useAppDispatch } from "../../redux/hooks";
import {
Expand All @@ -26,10 +32,17 @@ interface IProductCardProps {
const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
const [isHovered, setIsHovered] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [loadWishes, setLoadWishes] = useState(false);
const [reviews, setReviews] = useState<Review[]>([]);
const { wishes } = useSelector((state: RootState) => state.wishes);
const dispatch = useAppDispatch();
const navigate = useNavigate();

const loggedInUserToken = localStorage.getItem("accessToken");
let loggedInUser;
if (loggedInUserToken) {
// @ts-ignore
loggedInUser = JSON.parse(atob(loggedInUserToken.split(".")[1]));
}
const formatPrice = (price: number) => {
if (price < 1000) {
return price.toString();
Expand All @@ -56,6 +69,19 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {

fetchReviews();
}, [product.id]);

useEffect(() => {
setLoadWishes(true);
const fetchData = async () => {
try {
await dispatch(fetchWishes());
setLoadWishes(false);
} catch (error) {
console.error(error);
}
};
fetchData();
}, [dispatch]);
const total = reviews
? reviews.reduce((sum, review) => sum + (review.rating, 10), 0)
/ reviews.length
Expand All @@ -81,6 +107,7 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
// @ts-ignore
item.product?.id === product.id,
);
const alreadyWished = wishes?.some((item) => item.product?.id === product.id);

const handleAddToCart = async () => {
if (!localStorage.getItem("accessToken")) {
Expand All @@ -105,6 +132,40 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
}
};

const handleAddWish = async () => {
try {
setLoadWishes(true);
if (product.id) {
const response = await dispatch(addWish({ productId: product.id }));
if (response.payload === "product already exists in your wishlist") {
handleDeleteWish();
setLoadWishes(false);
await dispatch(fetchWishes());
} else {
await dispatch(fetchWishes());
setLoadWishes(false);
}
}
} catch (err) {
const error = err as AxiosError;
toast.error(error.message);
}
};

const handleDeleteWish = async () => {
try {
setLoadWishes(true);
if (product.id) {
dispatch(deleteWish({ productId: product.id }));
await dispatch(fetchWishes());
setLoadWishes(false);
}
} catch (err) {
const error = err as AxiosError;
toast.error(error.message);
}
};

const name = product.name.length > 20
? `${product.name.substring(0, 12)}...`
: product.name;
Expand Down Expand Up @@ -160,10 +221,23 @@ const ProductCard: React.FC<IProductCardProps> = ({ product }) => {
className="bg-white"
sx={{ paddingY: 0.5, paddingX: 0.5 }}
>
<CiHeart
className="text-black bg-white p-2 rounded-full text-[30px]"
data-testid="like-btn"
/>
{!loggedInUserToken || loggedInUser.roleId !== 1 ? (
""
) : loadWishes ? (
<Spinner />
) : alreadyWished ? (
<IoHeartSharp
className="text-[#DB4444] bg-white p-2 rounded-full text-[30px]"
data-testid="like-btn"
onClick={handleAddWish}
/>
) : (
<IoHeartSharp
className="text-black bg-white p-2 rounded-full text-[30px]"
data-testid="like-btn"
onClick={handleAddWish}
/>
)}
</IconButton>
<IconButton
sx={{ paddingY: 0.5, paddingX: 0.5 }}
Expand Down
Loading
Loading