From 7a962928d57fde31cac9f7b5c273245ccfe79066 Mon Sep 17 00:00:00 2001 From: 13XAVI <101405347+13XAVI@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:13:05 +0200 Subject: [PATCH 1/2] Added Category Dashboard (#136) --- package-lock.json | 6 + src/__test__/dashBoard/Categories.test.tsx | 148 ++++++ src/__test__/dashBoard/Seller.test.tsx | 46 +- src/components/dashBoard/Category.tsx | 454 ++++++++++++++++++ src/components/dashBoard/DashboardSideNav.tsx | 17 +- src/pages/Seller.tsx | 289 +++++------ src/routes/AppRoutes.tsx | 2 + 7 files changed, 744 insertions(+), 218 deletions(-) create mode 100644 src/__test__/dashBoard/Categories.test.tsx create mode 100644 src/components/dashBoard/Category.tsx diff --git a/package-lock.json b/package-lock.json index 150255d3..e5ed72f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11049,6 +11049,12 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/synchronous-promise": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", + "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", + "dev": true + }, "node_modules/synckit": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", diff --git a/src/__test__/dashBoard/Categories.test.tsx b/src/__test__/dashBoard/Categories.test.tsx new file mode 100644 index 00000000..a60f2fa1 --- /dev/null +++ b/src/__test__/dashBoard/Categories.test.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { MemoryRouter } from 'react-router-dom'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import Category from '@/components/dashBoard/Category'; +import categoriesReducer from '@/features/Products/categorySlice'; + +const renderWithProviders = (ui: React.ReactElement) => { + const store = configureStore({ reducer: { categories: categoriesReducer } }); + return render( + + {ui} + + ); +}; + +describe('Category Component', () => { + let mock: MockAdapter; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('should render the component with initial state', () => { + renderWithProviders(); + expect(screen.getByText(/Categories/i)).toBeInTheDocument(); + }); + + it('renders Categories component with category icons', async () => { + mock.onGet('/api/categories').reply(200, [ + { + id: 1, + name: 'Category 1', + description: 'Description 1', + icon: 'https://example.com/icon1.png', + }, + { + id: 2, + name: 'Category 2', + description: 'Description 2', + icon: 'https://example.com/icon2.png', + }, + ]); + + renderWithProviders(); + + const icons = await screen.findAllByRole('img'); + icons.forEach((icon) => { + expect(icon).toHaveAttribute('alt'); + }); + }); + + it('paginates categories', async () => { + mock.onGet('/api/categories').reply( + 200, + Array.from({ length: 20 }, (_, i) => ({ + id: i + 1, + name: `Category ${i + 1}`, + description: `Description ${i + 1}`, + icon: `https://example.com/icon${i + 1}.png`, + })) + ); + + renderWithProviders(); + + const nextPageButton = await screen.findByRole('button', { name: /next/i }); + fireEvent.click(nextPageButton); + expect(nextPageButton).toBeInTheDocument(); + }); + + it('should render the component with initial state', async () => { + renderWithProviders(); + await waitFor(() => + expect(screen.getByText(/Categories/i)).toBeInTheDocument() + ); + }); + + it('paginates categories', async () => { + mock.onGet('/api/categories').reply( + 200, + Array.from({ length: 20 }, (_, i) => ({ + id: i + 1, + name: `Category ${i + 1}`, + description: `Description ${i + 1}`, + icon: `https://example.com/icon${i + 1}.png`, + })) + ); + + renderWithProviders(); + + const nextPageButton = await screen.findByRole('button', { name: /next/i }); + fireEvent.click(nextPageButton); + expect(nextPageButton).toBeInTheDocument(); + }); + + it('should display validation errors if form is submitted with invalid data', async () => { + renderWithProviders(); + + // Open the form + fireEvent.click(screen.getByText(/Add Category/i)); + + // Trigger form submission + fireEvent.click(screen.getByText(/Save/i)); + + // Wait for validation errors to appear + await waitFor(() => { + expect(screen.getByText(/Name/i)).toBeInTheDocument(); + expect(screen.getByText(/Icon/i)).toBeInTheDocument(); + expect(screen.getByText(/Description/i)).toBeInTheDocument(); + }); + }); + + it('should submit form and handle API response', async () => { + mock.onPost(`${import.meta.env.VITE_BASE_URL}/category/`).reply(201); + + renderWithProviders(); + + // Open the form + fireEvent.click(screen.getByText(/Add Category/i)); + + // Fill out the form + fireEvent.change(screen.getByPlaceholderText(/Name of the category/i), { + target: { value: 'New Category' }, + }); + fireEvent.change( + screen.getByPlaceholderText(/Description of the category/i), + { + target: { value: 'Category description' }, + } + ); + fireEvent.change(screen.getByPlaceholderText(/URL of the category icon/i), { + target: { value: 'https://example.com/icon.png' }, + }); + + fireEvent.click(screen.getByText(/Save/i)); + + await waitFor(() => { + expect(mock.history.post.length).toBe(1); + }); + }); +}); diff --git a/src/__test__/dashBoard/Seller.test.tsx b/src/__test__/dashBoard/Seller.test.tsx index db47d0a8..12f91188 100644 --- a/src/__test__/dashBoard/Seller.test.tsx +++ b/src/__test__/dashBoard/Seller.test.tsx @@ -1,9 +1,9 @@ import axios from 'axios'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import MockAdapter from 'axios-mock-adapter'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { configureStore } from '@reduxjs/toolkit'; import Seller from '@/pages/Seller'; import productReducer from '@/app/Dashboard/AllProductSlices'; @@ -82,42 +82,25 @@ describe('Seller Component', () => { expect(screen.getByText('Sellers')).toBeInTheDocument(); }); - it('should display elements of the table', () => { - renderWithProviders(); - - expect(screen.getByText(/Image/)).toBeInTheDocument(); - expect(screen.getByText(/First Name/)).toBeInTheDocument(); - expect(screen.getByText(/Last Name/)).toBeInTheDocument(); - expect(screen.getByText(/Email/)).toBeInTheDocument(); - expect(screen.getByText(/Items Count/)).toBeInTheDocument(); - expect(screen.getByText(/Date/)).toBeInTheDocument(); - expect(screen.getByText(/Status/)).toBeInTheDocument(); - expect(screen.getByText(/Action/)).toBeInTheDocument(); - }); - - it('should filter sellers by search term', async () => { - // Mock API response + it('should display elements of the table', async () => { mock .onGet(`${import.meta.env.VITE_BASE_URL}/user/getAllUsers`) .reply(200, { users: mockBuyers }); - // Render component renderWithProviders(); - - // Dispatch fetchBuyers to populate the state await store.dispatch(fetchBuyers() as any); - // Perform search action - const searchInput = screen.getByPlaceholderText('Search Seller'); - fireEvent.change(searchInput, { target: { value: 'Vendor1' } }); - - // Check filtered results - expect(screen.getByText('Vendor1')).toBeInTheDocument(); - expect(screen.queryByText('Customer1')).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Email')).toBeInTheDocument(); + expect(screen.getByText('Items')).toBeInTheDocument(); + expect(screen.getByText('Date')).toBeInTheDocument(); + expect(screen.getByText('Status')).toBeInTheDocument(); + expect(screen.getByText('Action')).toBeInTheDocument(); + }); }); it('should handle loading state', async () => { - // Mock API response mock .onGet(`${import.meta.env.VITE_BASE_URL}/user/getAllUsers`) .reply(200, { users: mockBuyers }); @@ -137,7 +120,10 @@ describe('Seller Component', () => { renderWithProviders(); await store.dispatch(fetchBuyers() as any); - expect(screen.queryByText('Vendor1')).toBeNull(); - expect(screen.queryByText('Customer1')).toBeNull(); + + await waitFor(() => { + expect(screen.queryByText('Vendor1')).toBeNull(); + expect(screen.queryByText('Customer1')).toBeNull(); + }); }); }); diff --git a/src/components/dashBoard/Category.tsx b/src/components/dashBoard/Category.tsx new file mode 100644 index 00000000..28232b15 --- /dev/null +++ b/src/components/dashBoard/Category.tsx @@ -0,0 +1,454 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { useDispatch, useSelector } from 'react-redux'; +import { MdOutlineEdit } from 'react-icons/md'; +import { FaRegTrashAlt } from 'react-icons/fa'; +import { IoIosSearch } from 'react-icons/io'; +import * as Yup from 'yup'; +import { ErrorMessage, Field, Form, Formik } from 'formik'; +import PuffLoader from 'react-spinners/PuffLoader'; +import CircularPagination from './NavigateonPage'; +import Button from '@/components/form/Button'; +import { showErrorToast, showSuccessToast } from '@/utils/ToastConfig'; +import HSInput from '@/components/form/HSInput'; +import { AppDispatch, RootState } from '@/app/store'; +import { fetchCategories } from '@/features/Products/categorySlice'; + +interface ICategory { + id?: number; + name: string; + description: string; + icon?: string; +} + +function Category() { + const dispatch = useDispatch(); + const [currentPage, setCurrentPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(''); + const [deleteModal, setDeleteModal] = useState(false); + const [selectedCategory, setSelectedCategory] = useState( + null + ); + const [edit, setEdit] = useState(false); + const { categories, isLoading } = useSelector( + (state: RootState) => state.categories + ); + + const [visibleCategory, setVisbleCategory] = useState(false); + + useEffect(() => { + dispatch(fetchCategories()); + }, [dispatch]); + + const categoryPerPage = 8; + const totalPages = Math.ceil(categories.length / categoryPerPage); + const startIndex = (currentPage - 1) * categoryPerPage; + + const paginatedData = categories + .filter((category) => + category.description.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .slice(startIndex, startIndex + categoryPerPage); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handleCancelDelete = () => { + setDeleteModal(false); + setSelectedCategory(null); + }; + + const handleConfirmDelete = async () => { + if (selectedCategory === null) return; + + try { + const response = await axios.delete( + `${import.meta.env.VITE_BASE_URL}/category/${selectedCategory.id}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + } + ); + if (response.status === 200) { + showSuccessToast(' Deleted sucessfully'); + dispatch(fetchCategories()); + } + } catch (error) { + showErrorToast('Failed to delete category'); + } + }; + + const handleEdit = (category: ICategory) => { + setEdit(!edit); + setSelectedCategory(category); + }; + + const updateCategory = async (category: ICategory) => { + if (!category.id) return; + try { + const response = await axios.put( + `${import.meta.env.VITE_BASE_URL}/category/${category.id}`, + { + name: category.name, + description: category.description, + icon: category.icon, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + } + ); + if (response.status === 200) { + showSuccessToast(`${category.name} Updated sucessfully`); + dispatch(fetchCategories()); + } + } catch (error) { + showErrorToast('Failed to update category'); + } + }; + + const HandleAddCategory = () => { + setVisbleCategory(true); + }; + const showDeleteModal = (category: ICategory) => { + setSelectedCategory(category); + setDeleteModal(true); + }; + + const categoryInitialVal = { + name: '', + icon: '', + description: '', + }; + const validationSchemaCategory = Yup.object({ + name: Yup.string().required('Category name is required'), + description: Yup.string().required('Category icon is required'), + icon: Yup.string().required('Category description is required'), + }); + + const onsubmiting = async ( + values: ICategory, + { resetForm }: { resetForm: () => void } + ) => { + const token = localStorage.getItem('token'); + + try { + const response = await axios.post( + `${import.meta.env.VITE_BASE_URL}/category/`, + values, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (response.status === 201) { + showSuccessToast('Category added successfully'); + dispatch(fetchCategories()); + resetForm(); + } + } catch (error) { + showErrorToast('Failed to add category'); + } finally { + resetForm(); + } + }; + + return ( +
+
+

Categories

+ +
+
+
    +
  • + + ({categories.length}) +
  • +
+
+ + setSearchTerm(e.target.value)} + className="w-full outline-none bg-white placeholder:text-gray-400 font-light" + /> +
+
+ + {visibleCategory && ( +
+ +
+
+
+

+ Add Category +

+ + + +
+
+ + + +
+ +
+ + + +
+
+ +
+
+
+
+
+ )} + + {edit && selectedCategory && ( +
+
+
+ + + setSelectedCategory({ + ...selectedCategory, + name: e.target.value, + }) + } + /> +
+
+ + + setSelectedCategory({ + ...selectedCategory, + description: e.target.value, + }) + } + /> +
+ +
+ + {selectedCategory.icon && ( +
+ Icon Preview +
+ )} + + + setSelectedCategory({ + ...selectedCategory, + icon: e.target.value, + }) + } + /> +
+
+
+
+
+ )} + {deleteModal && ( +
+
+
+ Are you sure you want to delete this category? +
+

{selectedCategory?.name}

+
+
+
+
+
+
+ )} + {isLoading ? ( +
+ +
+ ) : ( +
+ + + + + + + + + + + + {paginatedData.map((category, index) => ( + + + + + + + + + + + ))} + +
+ # + + Category + + Icon + + Description + + Action +
+ {index + 1 + (currentPage - 1) * categoryPerPage} + {category.name} +
+ {category.name} +
+
{category.description} + + + +
+
+ +
+
+ )} +
+ ); +} + +export default Category; diff --git a/src/components/dashBoard/DashboardSideNav.tsx b/src/components/dashBoard/DashboardSideNav.tsx index a28db4e3..073f3c43 100644 --- a/src/components/dashBoard/DashboardSideNav.tsx +++ b/src/components/dashBoard/DashboardSideNav.tsx @@ -49,20 +49,9 @@ const sideBarItems = [ }, { name: 'seller', - icon: , - subItems: [ - { - name: 'All Seller', - path: '/dashboard/seller', - role: ['Admin'], - }, - { - name: 'Add New', - path: '/dashboard/addSeller', - role: ['Admin'], - }, - ], + path: '/dashboard/seller', role: ['Admin'], + icon: , }, { name: 'Products', @@ -79,7 +68,7 @@ const sideBarItems = [ role: ['Vendor'], }, { - path: '/products/categories', + path: '/dashboard/category', name: 'Categories', role: ['Admin'], }, diff --git a/src/pages/Seller.tsx b/src/pages/Seller.tsx index a2bb6d8c..2635b4dd 100644 --- a/src/pages/Seller.tsx +++ b/src/pages/Seller.tsx @@ -1,19 +1,12 @@ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import axios from 'axios'; -import { - ChevronLeft, - ChevronRight, - Power, - RefreshCcw, - Search, -} from 'lucide-react'; +import { ChevronLeft, ChevronRight, Power, Search } from 'lucide-react'; import PuffLoader from 'react-spinners/PuffLoader'; import { RootState, AppDispatch } from '@/app/store'; import { fetchProducts } from '@/app/Dashboard/AllProductSlices'; import { fetchBuyers } from '@/app/Dashboard/buyerSlice'; import Button from '@/components/form/Button'; - import { showErrorToast, showSuccessToast } from '@/utils/ToastConfig'; interface Vendor { @@ -36,7 +29,6 @@ function Seller() { const [searchTerm, setSearch] = useState(''); const [filteredVendors, setFilteredVendors] = useState([]); const [reRenderTrigger, setReRenderTrigger] = useState(false); - const [clickedVendor, setClickedVendor] = useState(null); const [deactivate, setDeactivate] = useState(false); const [activate, setActivate] = useState(false); @@ -81,6 +73,7 @@ function Seller() { if (res.status === 200) { showSuccessToast(`${vendor?.firstName} Activated Successfully`); setReRenderTrigger((prev) => !prev); + dispatch(fetchBuyers()); } else { showErrorToast('Failed to Activate the vendor'); } @@ -107,6 +100,7 @@ function Seller() { if (res.status === 200) { showSuccessToast(`${vendor?.firstName} Suspended Successfully`); setReRenderTrigger((prev) => !prev); + dispatch(fetchBuyers()); } else { showErrorToast('Failed to Suspend the vendor'); } @@ -158,7 +152,7 @@ function Seller() { return (
{deactivate && ( -
+
Are you sure you want to suspend? @@ -208,7 +202,7 @@ function Seller() { )}
-
+
Sellers
-
+

All ({vendors.length})

@@ -243,174 +237,121 @@ function Seller() {

{status === 'loading' && ( -
- +
+
)} -
-
-
-
-
Image
-
- First Name -
-
- Last Name -
-
Email
-
- Items Count -
-
Date
-
Status
-
Action
-
- {vendors.length > 0 ? ( - visiblePage.map((v, id) => ( -
+ + + + + + + + + + + + + {visiblePage.map((vendor, i) => ( + + + + + + + + + ))} + +
NameEmailItemsDateStatusAction
+ seller avatar +
+ {`${vendor.firstName} ${vendor.lastName}`} +
+
{vendor.email}{ItemCount(vendor.firstName)}{DateFormat(vendor.updatedAt)} + + {vendor.status} + + +
+ {vendor.status === 'inactive' && ( + + )} + {vendor.status === 'active' && ( + + )} +
+
+
+ + {pages && + pages.map((page) => ( +
-
- - -
-
- )) - ) : ( -
No Vendor Found
- )} -
- - {pages && - pages.map((page) => ( - - ))} - -
-
-
- -
- {vendors && - visiblePage.map((v, id) => ( -
-
- -
-

- {v.firstName} {v.lastName} -

-

{v.email}

-
-
-
-

First Name: {v.firstName}

-

Items: {ItemCount(v.lastName)}

-

Date: {DateFormat(v.updatedAt)}

-

- Status: - - {v.status} - -

-
-
- - -
+ ))} +
- ))} +
+ )} + {status === 'failed' && ( +
+
Failed to fetch sellers
+
+ )}
); diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index bb9c1d16..45f46682 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -28,6 +28,7 @@ import Coupons from '@/pages/Coupons'; import EditCoupon from '@/pages/EditCoupon'; import TableUserRole from '@/components/dashBoard/UserRole'; import Customer from '@/pages/customer'; +import Category from '@/components/dashBoard/Category'; function AppRoutes() { return ( @@ -68,6 +69,7 @@ function AppRoutes() { } /> } /> } /> + } /> Date: Fri, 26 Jul 2024 15:55:12 +0200 Subject: [PATCH 2/2] fix(test-cov): increase test coverage (#132) - add more tests to cover uncovered lines [#Fixes #124] --- src/__test__/Cart/Cart.test.tsx | 22 ++++ src/__test__/productDetails.test.tsx | 169 ++++++++++++++++++++++++++- src/__test__/shop.test.tsx | 13 +++ src/components/salesMap/SalesMap.tsx | 2 +- src/features/Auth/SignInSlice.ts | 2 +- src/features/Orders/ordersSlice.ts | 27 ++++- src/pages/Coupons.tsx | 2 +- src/pages/ProductDetails.tsx | 2 + src/pages/Shop.tsx | 1 + src/routes/AppRoutes.tsx | 19 ++- vite.config.ts | 8 ++ 11 files changed, 255 insertions(+), 12 deletions(-) diff --git a/src/__test__/Cart/Cart.test.tsx b/src/__test__/Cart/Cart.test.tsx index ee4867af..0c3d5611 100644 --- a/src/__test__/Cart/Cart.test.tsx +++ b/src/__test__/Cart/Cart.test.tsx @@ -1,12 +1,15 @@ import { configureStore } from '@reduxjs/toolkit'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { Provider } from 'react-redux'; +import { render, screen } from '@testing-library/react'; import cartReducer, { fetchCartItems, addCartItem, updateCartItemQuantity, removeCartItem, } from '@/features/Cart/cartSlice'; +import CartItem from '@/components/Cart/CartItem'; describe('cartSlice', () => { let store = configureStore({ reducer: { cartItems: cartReducer } }); @@ -79,3 +82,22 @@ describe('cartSlice', () => { expect(state.error).toBeNull(); }); }); + +describe('Cart component', () => { + const store = configureStore({ + reducer: {}, + }); + + it('renders cart item', async () => { + render( + + + + ); + + expect(screen.getByText('$300')).toBeInTheDocument(); + expect(screen.getByText('Test Product')).toBeInTheDocument(); + expect(screen.getByText('Size')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + }); +}); diff --git a/src/__test__/productDetails.test.tsx b/src/__test__/productDetails.test.tsx index f60ae28f..58d5e76f 100644 --- a/src/__test__/productDetails.test.tsx +++ b/src/__test__/productDetails.test.tsx @@ -3,13 +3,13 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Store, configureStore } from '@reduxjs/toolkit'; import { waitFor } from '@testing-library/dom'; -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; import { MemoryRouter, Route, Routes } from 'react-router'; import productsReducer, { fetchProductDetails, } from '@/features/Products/ProductSlice'; -import signInReducer from '@/features/Auth/SignInSlice'; +import signInReducer, { SignInState } from '@/features/Auth/SignInSlice'; import { AppDispatch, RootState } from '@/app/store'; import ProductDetails from '@/pages/ProductDetails'; import bestSellingReducer from '@/features/Popular/bestSellingProductSlice'; @@ -24,8 +24,22 @@ const mockProduct = { totalQtySold: 25, longDesc: 'This is a mock product used for testing purposes.', shortDesc: 'This is a short description', - category: 'Electronics', - similarProducts: [], + category: { + id: 5, + name: 'Electronics', + }, + similarProducts: [ + { + id: 3, + name: 'Mock Similar Product', + image: '/images/mock.png', + averageRating: 0, + salesPrice: 100, + regularPrice: 200, + longDesc: 'This is a mock product used for testing purposes.', + shortDesc: 'This is a short description', + }, + ], reviews: [ { id: 1, @@ -42,7 +56,8 @@ const mockProduct = { gallery: [], tags: ['testTag'], vendor: { - name: 'Tester', + firstName: 'Tester', + lastName: 'Testing', email: 'testervendor@gmail.com', picture: 'https://fake.png', }, @@ -57,6 +72,22 @@ const renderWithProviders = ( bestSellingProducts: bestSellingReducer, signIn: signInReducer, }, + preloadedState: { + signIn: { + token: 'fake token', + user: null, + role: null, + loading: null, + error: null, + message: null, + needsVerification: false, + needs2FA: false, + vendor: { + id: null, + email: null, + }, + } as unknown as SignInState, + }, }), } = {} ) => { @@ -101,6 +132,12 @@ describe('ProductDetails Page', () => { expect(screen.getByText(/\$149.99/i)).toBeInTheDocument(); expect(screen.getByText('1')).toBeInTheDocument(); expect(screen.getByText('25')).toBeInTheDocument(); + expect(screen.getByText('33% Off')).toBeInTheDocument(); + expect(screen.getByText('Add to Cart')).toBeInTheDocument(); + expect(screen.getByText('Add to wishlist')).toBeInTheDocument(); + // expect(screen.getAllByTestId('ratingStar').length).toBe(4); + // expect(screen.getAllByTestId('halfStar').length).toBe(1); + expect( screen.getByText(/This is a mock product used for testing purposes./i) ).toBeInTheDocument(); @@ -123,6 +160,128 @@ describe('ProductDetails Page', () => { ).toBeInTheDocument(); }); }); + + it('should display similar products', async () => { + mock + .onGet(`${import.meta.env.VITE_BASE_URL}/buyer/get_product/1`) + .reply(200, { product: mockProduct }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText('Mock Similar Prod...')).toBeInTheDocument(); + expect(screen.getByText('$100')).toBeInTheDocument(); + expect(screen.getByText('50% Off')).toBeInTheDocument(); + }); + }); + + it('should display product details, reviews, about store', async () => { + mock + .onGet(`${import.meta.env.VITE_BASE_URL}/buyer/get_product/1`) + .reply(200, { product: mockProduct }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText('Product Details')).toBeInTheDocument(); + expect( + screen.getByText('This is a mock product used for testing purposes.') + ).toBeInTheDocument(); + expect( + screen.getByText('This is a short description') + ).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Reviews (1)' })); + + await waitFor(() => { + expect(screen.getByText('new user')).toBeInTheDocument(); + expect(screen.getByText('excellent product')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'About Store' })); + + await waitFor(() => { + expect(screen.getAllByText('Tester Testing')[0]).toBeInTheDocument(); + expect( + screen.getAllByText('testervendor@gmail.com')[0] + ).toBeInTheDocument(); + }); + }); + + it('should display error message when no reviews are found', async () => { + mock + .onGet(`${import.meta.env.VITE_BASE_URL}/buyer/get_product/1`) + .reply(200, { product: { ...mockProduct, reviews: [] } }); + + renderWithProviders(); + + await waitFor(() => { + fireEvent.click(screen.getByRole('button', { name: 'Reviews (0)' })); + }); + + await waitFor(() => { + expect(screen.getByText('No reviews found')).toBeInTheDocument(); + }); + }); + + it('should submit a review successfully', async () => { + mock + .onGet(`${import.meta.env.VITE_BASE_URL}/buyer/get_product/1`) + .reply(200, { product: mockProduct }); + + mock + .onPost(`${import.meta.env.VITE_BASE_URL}/review`) + .reply(200, { message: 'Review submitted successfully' }); + + renderWithProviders(); + + mock + .onGet(`${import.meta.env.VITE_BASE_URL}/buyer/get_product/1`) + .reply(200, { + product: { + ...mockProduct, + reviews: [ + { + id: 1, + user: { + id: 1, + firstName: 'new', + lastName: 'rating', + picture: 'http://fake.png', + }, + rating: 1, + content: 'this is a bad product', + }, + ], + }, + }); + + await waitFor(() => { + const star = screen.getAllByTitle('inputStar')[0]; + fireEvent.click(star); + const contentTextArea = screen.getByTitle('inputContent'); + fireEvent.change(contentTextArea, { + target: { + value: 'this is a bad product', + }, + }); + }); + + await waitFor(() => { + const submitBtn = screen.getByText('Submit'); + fireEvent.click(submitBtn); + }); + + await waitFor(() => { + fireEvent.click(screen.getByRole('button', { name: 'Reviews (1)' })); + }); + + await waitFor(() => { + expect(screen.getByText('this is a bad product')).toBeInTheDocument(); + expect(screen.getByText('new rating')).toBeInTheDocument(); + }); + }); }); describe('Product Details async action', () => { diff --git a/src/__test__/shop.test.tsx b/src/__test__/shop.test.tsx index d6fe8d26..62f70e81 100644 --- a/src/__test__/shop.test.tsx +++ b/src/__test__/shop.test.tsx @@ -108,6 +108,19 @@ describe('Shop Component', () => { expect(screen.queryByText(/Product 2/i)).not.toBeInTheDocument(); }); }); + + it('displays the filter section on mobile devices', async () => { + renderWithProviders(); + const filterBtn = screen.getByTitle('filter'); + fireEvent.click(filterBtn); + + await waitFor(() => { + expect(screen.getAllByText('Filters')[1]).toBeInTheDocument(); + expect(screen.getAllByText('Clear All')[1]).toBeInTheDocument(); + expect(screen.getAllByText('Categories')[1]).toBeInTheDocument(); + expect(screen.getAllByText('Rating')[1]).toBeInTheDocument(); + }); + }); }); describe('ProductSlice', () => { diff --git a/src/components/salesMap/SalesMap.tsx b/src/components/salesMap/SalesMap.tsx index fc3039f6..b7e8288b 100644 --- a/src/components/salesMap/SalesMap.tsx +++ b/src/components/salesMap/SalesMap.tsx @@ -53,7 +53,7 @@ function SalesMap() { (label as any).html( `

${(label as any).html()}

-

Sales: ${Data ? Data[code] : 0}

+

Sales: ${code in Data ? Data[code] : 0}

` ); }} diff --git a/src/features/Auth/SignInSlice.ts b/src/features/Auth/SignInSlice.ts index 79120e39..d29c48eb 100644 --- a/src/features/Auth/SignInSlice.ts +++ b/src/features/Auth/SignInSlice.ts @@ -16,7 +16,7 @@ interface User { }; } -interface SignInState { +export interface SignInState { token: string | null; user: User | null; loading: boolean; diff --git a/src/features/Orders/ordersSlice.ts b/src/features/Orders/ordersSlice.ts index 2918ba7f..1c01b27c 100644 --- a/src/features/Orders/ordersSlice.ts +++ b/src/features/Orders/ordersSlice.ts @@ -1,7 +1,7 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from 'axios'; import Order from '@/interfaces/order'; -import { RootState } from '../../app/store'; +import { RootState, store } from '../../app/store'; interface OrdersState { orders: Order[]; @@ -35,6 +35,31 @@ export const fetchOrders = createAsyncThunk('orders/fetchOrders', async () => { }, }; }); + + const { signIn } = store.getState(); + + if (signIn.user?.userType.name === 'Admin') { + return orders; + } + if (signIn.user?.userType.name === 'Vendor') { + const filteredOrders: Order[] = []; + /* eslint-disable no-restricted-syntax */ + for (const order of orders) { + const newDetails = []; + /* eslint-disable no-restricted-syntax */ + for (const orderDetail of order.orderDetails) { + if (orderDetail.product.vendor.id === signIn.user.id) { + newDetails.push(orderDetail); + } + } + + if (newDetails.length > 0) { + filteredOrders.push({ ...order, orderDetails: newDetails }); + } + } + + return filteredOrders; + } return orders; }); diff --git a/src/pages/Coupons.tsx b/src/pages/Coupons.tsx index 2842afa1..f139312a 100644 --- a/src/pages/Coupons.tsx +++ b/src/pages/Coupons.tsx @@ -147,7 +147,7 @@ function Coupons() { setSearchTerm(e.target.value)} diff --git a/src/pages/ProductDetails.tsx b/src/pages/ProductDetails.tsx index 37d2583e..1580c9be 100644 --- a/src/pages/ProductDetails.tsx +++ b/src/pages/ProductDetails.tsx @@ -689,6 +689,7 @@ function ProductDetails() {
{Array.from({ length: 5 }, (_, i) => ( *