Skip to content

Commit

Permalink
feat(wishlist): implement buyer wishlist
Browse files Browse the repository at this point in the history
- implement wishlist UI components

[Delivers #96]
  • Loading branch information
jkarenzi committed Jul 18, 2024
1 parent bcb61cb commit 97609ad
Show file tree
Hide file tree
Showing 13 changed files with 508 additions and 18 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1",
"react-slider": "^2.0.6",
"react-simple-maps": "^3.0.0",
"react-slider": "^2.0.6",
"react-spinners": "^0.14.1",
"react-toastify": "^10.0.5",
"redux": "^5.0.1",
Expand All @@ -60,8 +60,8 @@
"@types/node": "^20.14.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.2.22",
"@types/react-slider": "^1.3.6",
"@types/react-simple-maps": "^3.0.4",
"@types/react-slider": "^1.3.6",
"@types/redux-mock-store": "^1.0.6",
"@types/testing-library__react": "^10.2.0",
"@types/webfontloader": "^1.6.38",
Expand Down
4 changes: 2 additions & 2 deletions src/__test__/home/ProductGridFour.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,12 @@ describe('ProductGridFour Component', () => {
mockProducts.slice(0, 4).forEach((product) => {
expect(
screen.getByText(
`${product.name.substring(0, 17)}${product.name.length > 18 ? '...' : ''}`
`${product.name.substring(0, 17)}${product.name.length > 17 ? '...' : ''}`
)
).toBeInTheDocument();
expect(
screen.getAllByText(
`${product.shortDesc.substring(0, 27)}${product.shortDesc.length > 28 ? '...' : ''}`
`${product.shortDesc.substring(0, 27)}${product.shortDesc.length > 27 ? '...' : ''}`
)[0]
).toBeInTheDocument();
expect(screen.getByAltText(product.name)).toBeInTheDocument();
Expand Down
27 changes: 24 additions & 3 deletions src/__test__/home/productCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { describe, it, expect } from 'vitest';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import ProductCard from '@/components/home/ProductCard';
import { Product } from '@/types/Product';
import User from '@/types/User';
import signInReducer from '@/features/Auth/SignInSlice';

// Mock Product Data
const mockProduct: Product = {
Expand Down Expand Up @@ -35,17 +39,34 @@ const mockProduct: Product = {
} as User,
};

const renderWithProviders = (
ui: React.ReactElement,
{
store = configureStore({
reducer: {
signIn: signInReducer,
},
}),
} = {}
) => {
return render(
<Provider store={store}>
<MemoryRouter>{ui}</MemoryRouter>
</Provider>
);
};

describe('ProductCard Component', () => {
it('renders the ProductCard component with product details', () => {
render(<ProductCard product={mockProduct} />);
renderWithProviders(<ProductCard product={mockProduct} />);

const productName = screen.getByText(
`${mockProduct.name.substring(0, 17)}${mockProduct.name.length > 18 ? '...' : ''}`
`${mockProduct.name.substring(0, 17)}${mockProduct.name.length > 17 ? '...' : ''}`
);
expect(productName).toBeInTheDocument();

const productDesc = screen.getByText(
`${mockProduct.shortDesc.substring(0, 27)}${mockProduct.shortDesc.length > 28 ? '...' : ''}`
`${mockProduct.shortDesc.substring(0, 27)}${mockProduct.shortDesc.length > 27 ? '...' : ''}`
);
expect(productDesc).toBeInTheDocument();

Expand Down
4 changes: 2 additions & 2 deletions src/__test__/home/productList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ describe('ProductsList Component', () => {

mockProducts.forEach((product) => {
const productName = screen.getByText(
`${product.name.substring(0, 17)}${product.name.length > 18 ? '...' : ''}`
`${product.name.substring(0, 17)}${product.name.length > 17 ? '...' : ''}`
);
expect(productName).toBeInTheDocument();

const productDesc = screen.getByText(
`${product.shortDesc.substring(0, 27)}${product.shortDesc.length > 28 ? '...' : ''}`
`${product.shortDesc.substring(0, 27)}${product.shortDesc.length > 27 ? '...' : ''}`
);
expect(productDesc).toBeInTheDocument();

Expand Down
186 changes: 186 additions & 0 deletions src/__test__/wishlist.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React from 'react';
import { describe, it, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { MemoryRouter } from 'react-router-dom';
import productReducer, {
fetchWishlistProducts,
addToWishlist,
removeFromWishlist,
} from '@/features/Products/ProductSlice';
import categoryReducer from '@/features/Products/categorySlice';
import signInReducer from '@/features/Auth/SignInSlice';
import WishlistCard from '@/components/WishlistCard';
import Wishlist from '@/pages/Wishlist';
import { AppDispatch, RootState } from '@/app/store';
import { Product } from '@/types/Product';

const mock = new MockAdapter(axios);

const renderWithProviders = (
ui: React.ReactElement,
{
store = configureStore({
reducer: {
products: productReducer,
categories: categoryReducer,
signIn: signInReducer,
},
}),
} = {}
) => {
return render(
<Provider store={store}>
<MemoryRouter>{ui}</MemoryRouter>
</Provider>
);
};

const product = {
id: 1,
name: 'Product name',
shortDesc: 'Short description',
salesPrice: 230,
regularPrice: 280,
averageRating: 4.7,
image: 'path_to_image',
gallery: ['path_to_image', 'path_to_image'],
category: {
id: 1,
name: 'Category name',
description: 'Category description',
},
longDesc: 'Long description',
quantity: 10,
tags: ['tag1', 'tag2'],
type: 'Simple',
isAvailable: true,
isFeatured: true,
reviews: [],
vendor: {
id: 1,
firstName: 'Vendor',
lastName: 'Name',
email: 'example@gmail.com',
password: 'hashed_password',
userType: {
id: 1,
name: 'Vendor',
permissions: ['manage_products', 'view_orders'],
},
orders: [],
googleId: '',
facebookId: '',
picture: 'path_to_picture',
provider: 'local',
isVerified: true,
status: 'active',
},
} as Product;

describe('Wishlist async actions', () => {
let store: ReturnType<typeof configureStore>;

beforeEach(() => {
store = configureStore({
reducer: {
products: productReducer,
},
});
mock.reset();
});

it('fetches wishlist products', async () => {
const products = [
{ id: 1, name: 'Product 1' },
{ id: 2, name: 'Product 2' },
];
mock
.onGet(`${import.meta.env.VITE_BASE_URL}/buyer/getOneWishlist`)
.reply(200, {
data: { product: products },
});

const { dispatch }: { dispatch: AppDispatch } = store;
await dispatch(fetchWishlistProducts('token'));

const state = (store.getState() as RootState).products;
expect(state.wishlistProducts).toEqual(products);
});

it('adds a product to the wishlist', async () => {
const products = [{ id: 1, name: 'Product 1' }];
mock
.onPost(`${import.meta.env.VITE_BASE_URL}/buyer/addItemToWishlist`)
.reply(200, {
data: { product: products },
});

const { dispatch }: { dispatch: AppDispatch } = store;
await dispatch(addToWishlist({ id: 1, token: 'token' }));

const state = (store.getState() as RootState).products;
expect(state.wishlistProducts).toEqual(products);
});

it('removes a product from the wishlist', async () => {
mock
.onDelete(`${import.meta.env.VITE_BASE_URL}/buyer/removeToWishlist`)
.reply(200, {
data: { product: [] },
});

const { dispatch }: { dispatch: AppDispatch } = store;
await dispatch(removeFromWishlist({ id: 1, token: 'token' }));

const state = (store.getState() as RootState).products;
expect(state.wishlistProducts).toEqual([]);
});
});

describe('WishlistCard', () => {
it('renders the wishlist card', () => {
renderWithProviders(<WishlistCard product={product} />);

expect(screen.getByText(/Product name/i)).toBeInTheDocument();
expect(screen.getByText(/In Stock/i)).toBeInTheDocument();
expect(screen.getByText(/\$500/i)).toBeInTheDocument();
expect(screen.getByText(/\$700/i)).toBeInTheDocument();
});
});

describe('Wishlist Page', () => {
it('renders wishlist products', async () => {
const products = [product];

mock
.onGet(`${import.meta.env.VITE_BASE_URL}/buyer/getOneWishlist`)
.reply(200, {
data: { product: products },
});

renderWithProviders(<Wishlist />);

await waitFor(() => {
expect(screen.getByText(/My Wishlist/i)).toBeInTheDocument();
expect(screen.getByText(/Product name/i)).toBeInTheDocument();
});
});

it('shows no products message when wishlist is empty', () => {
mock
.onGet(`${import.meta.env.VITE_BASE_URL}/buyer/getOneWishlist`)
.reply(200, {
data: { product: [] },
});

renderWithProviders(<Wishlist />);

expect(
screen.getByText(/You currently have no products in your wishlist/i)
).toBeInTheDocument();
});
});
8 changes: 7 additions & 1 deletion src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ function Navbar() {
5
</div>
</div>
<FiHeart color="#424856" size="20" title="wishlist" />
<FiHeart
color="#424856"
size="20"
title="wishlist"
className="cursor-pointer"
onClick={() => navigate('/wishlist')}
/>
</div>
{user ? (
<div className="xs:hidden lg:flex items-center gap-2">
Expand Down
Loading

0 comments on commit 97609ad

Please sign in to comment.