diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1979ef5..a2295ec 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -70,6 +70,7 @@ module.exports = { "react/no-unescaped-entities": "off", '@typescript-eslint/no-explicit-any': 0, "react/prop-types": "off", + "jsx-a11y/control-has-associated-label": "off", "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, diff --git a/src/__test__/addProducts.test.tsx b/src/__test__/addProducts.test.tsx index e4e55bf..ba3524a 100644 --- a/src/__test__/addProducts.test.tsx +++ b/src/__test__/addProducts.test.tsx @@ -75,9 +75,6 @@ describe("FileUpload component", () => { render( , ); - - expect(screen.getByText(/example.png/)).toBeInTheDocument(); - expect(screen.getByText(/example.png/)).toHaveClass("text-gray-500"); expect(screen.getByRole("button", { name: /remove/i })).toBeInTheDocument(); }); @@ -158,11 +155,11 @@ describe("test seller dashboard components", () => { fireEvent.click(select); - const categoryOption = screen.getByText("Category 1"); + const categoryOption = screen.getByText("Select a category"); expect(categoryOption).toBeDefined(); fireEvent.click(categoryOption); - expect(select).toHaveTextContent("Category 1"); + expect(select).toHaveTextContent("Select a category"); }); it("should render the TextInput with label, placeholder, and no error", () => { diff --git a/src/__test__/productActions.test.tsx b/src/__test__/productActions.test.tsx new file mode 100644 index 0000000..2865f66 --- /dev/null +++ b/src/__test__/productActions.test.tsx @@ -0,0 +1,101 @@ +import "@testing-library/jest-dom"; +import { + fireEvent, render, screen, waitFor, +} from "@testing-library/react"; +import { Provider } from "react-redux"; +import { BrowserRouter as Router } from "react-router-dom"; + +import store from "../redux/store"; +import { fetchProducts } from "../redux/reducers/productsSlice"; + +jest.mock("react-dropzone", () => ({ + useDropzone: jest.fn(), +})); + +jest.mock("../redux/api/productsApiSlice", () => ({ + addProduct: jest.fn(), + updateProduct: jest.fn(), + deleteProduct: jest.fn(), +})); + +jest.mock("react-toastify", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock("../components/dashboard/ConfirmModal", () => () => ( +
ConfirmModal
+)); +jest.mock("../components/dashboard/Spinner", () => () =>
Spinner
); +jest.mock( + "../components/dashboard/ToggleSwitch", + () => ({ checked, onChange }) => ( +
+ ToggleSwitch + {checked ? "On" : "Off"} +
+ ), +); + +const mockProducts = [ + { + id: 1, + name: "Product 1", + stockQuantity: 10, + expiryDate: "2024-12-31T00:00:00.000Z", + price: 100, + category: { name: "Electronics" }, + isAvailable: true, + images: ["image1.png"], + }, + { + id: 2, + name: "Product 2", + stockQuantity: 5, + expiryDate: "2025-06-30T00:00:00.000Z", + price: 50, + category: { name: "Fashion" }, + isAvailable: true, + images: ["image2.png"], + }, +]; + +global.URL.createObjectURL = jest.fn(); + +describe("Products slice tests", () => { + it("should handle products initial state", () => { + expect(store.getState().products).toEqual({ + loading: false, + data: [], + error: null, + }); + }); + + it("should handle products pending", () => { + store.dispatch(fetchProducts.pending("")); + expect(store.getState().products).toEqual({ + loading: true, + data: [], + error: null, + }); + }); + + it("should handle products fulfilled", () => { + const mockData = { message: "success" }; + // @ts-ignore + store.dispatch(fetchProducts.fulfilled(mockData, "", {})); + expect(store.getState().products).toEqual({ + loading: false, + data: mockData, + error: null, + }); + }); + + it("should handle products fetch rejected", () => { + // @ts-ignore + store.dispatch(fetchProducts.rejected(null, "", {})); + expect(store.getState().products.error).toEqual("Rejected"); + }); +}); diff --git a/src/components/dashboard/ConfirmModal.tsx b/src/components/dashboard/ConfirmModal.tsx new file mode 100644 index 0000000..ee56612 --- /dev/null +++ b/src/components/dashboard/ConfirmModal.tsx @@ -0,0 +1,50 @@ +import React from "react"; + +interface ConfirmDeleteModalProps { + onConfirm: () => void; + onCancel: () => void; + message: string; + product: any; + loading: boolean; +} + +const ConfirmModal: React.FC = ({ + onConfirm, + onCancel, + message, + product, + loading, +}) => ( +
+
+
+

{message}

+

+ + {product.name} + . + + {' '} + This can't be + undone +

+
+
+ + +
+
+
+); + +export default ConfirmModal; diff --git a/src/components/dashboard/CustomSelect.tsx b/src/components/dashboard/CustomSelect.tsx index 240eae1..9338e72 100644 --- a/src/components/dashboard/CustomSelect.tsx +++ b/src/components/dashboard/CustomSelect.tsx @@ -18,28 +18,28 @@ const CustomSelect: React.FC = ({ testId, }) => { const [isOpen, setIsOpen] = useState(false); - const [selectedOption, setSelectedOption] = useState(defaultValue); const loading = useSelector((state: RootState) => state.categories.loading); - const handleOptionClick = (option) => { - setSelectedOption(option.name); + const handleOptionClick = (option: any) => { setIsOpen(false); - onSelect(option); + if (option !== "Category") { + onSelect(option); + } }; return (
setIsOpen(!isOpen)} data-testid={testId} > - {selectedOption} + {defaultValue} {' '}
{isOpen && ( -
+
{loading ? (
loading categories... @@ -52,7 +52,7 @@ const CustomSelect: React.FC = ({ options.map((option) => (
handleOptionClick(option)} > {option.name} diff --git a/src/components/dashboard/FileUpload.tsx b/src/components/dashboard/FileUpload.tsx index 4fbe595..89a7775 100644 --- a/src/components/dashboard/FileUpload.tsx +++ b/src/components/dashboard/FileUpload.tsx @@ -6,10 +6,18 @@ interface FileUploadProps { onDrop: (acceptedFiles: File[]) => void; remove: (file: File, event: React.MouseEvent) => void; files: File[]; + existingImages?: string[]; + removeExistingImage: ( + index: number, + image: string, + event: React.MouseEvent, + ) => void; } const FileUpload = forwardRef( - ({ onDrop, remove, files }, ref) => { + ({ + onDrop, remove, files, existingImages, removeExistingImage, + }, ref) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { @@ -22,38 +30,62 @@ const FileUpload = forwardRef( return (
- {files.length > 0 ? ( - files.map((file) => ( -
- Preview -

{`${file.name.slice(0, 15)}...`}

- +
+ ))} + + )} + {files.length > 0 ? ( + files.map((file, index) => ( +
- Remove - + Preview + +
+ )) + ) : existingImages && existingImages.length > 0 ? null : ( +
+ +

Browse Images...

- )) - ) : ( -
- -

Browse Images...

-
- )} + )} +
); }, diff --git a/src/components/dashboard/Header.tsx b/src/components/dashboard/Header.tsx index 3a8161c..7e99d7d 100644 --- a/src/components/dashboard/Header.tsx +++ b/src/components/dashboard/Header.tsx @@ -10,7 +10,7 @@ interface HeaderProps { } const Header: React.FC = ({ toggleSidebar }) => ( -
+
diff --git a/src/components/dashboard/SideBar.tsx b/src/components/dashboard/SideBar.tsx index 5e838cf..a6b1980 100644 --- a/src/components/dashboard/SideBar.tsx +++ b/src/components/dashboard/SideBar.tsx @@ -4,88 +4,99 @@ import { IoBriefcaseOutline, IoSettingsOutline } from "react-icons/io5"; import { AiFillProduct } from "react-icons/ai"; import { MdInsertChartOutlined } from "react-icons/md"; import { FiLogOut } from "react-icons/fi"; -import { NavLink } from "react-router-dom"; +import { NavLink, useLocation } from "react-router-dom"; import { FaCircle } from "react-icons/fa"; interface SidebarProps { isOpen: boolean; } -const SideBar: React.FC = ({ isOpen }) => ( -
= ({ isOpen }) => { + const location = useLocation(); + + const getLinkClass = (path: string) => (location.pathname === path ? "text-primary" : "text-dark-gray"); + + return ( +
-
-
-
- eagles - + > +
+
+
+ eagles + +
+ +
+
+ + Log Out
- -
-
- - {' '} - Log Out
-
-); + ); +}; export default SideBar; diff --git a/src/components/dashboard/Spinner.tsx b/src/components/dashboard/Spinner.tsx new file mode 100644 index 0000000..940ec9e --- /dev/null +++ b/src/components/dashboard/Spinner.tsx @@ -0,0 +1,16 @@ +const Spinner = () => ( + + + + + +); + +export default Spinner; diff --git a/src/components/dashboard/ToggleSwitch.tsx b/src/components/dashboard/ToggleSwitch.tsx new file mode 100644 index 0000000..c99019b --- /dev/null +++ b/src/components/dashboard/ToggleSwitch.tsx @@ -0,0 +1,59 @@ +import React from "react"; + +interface ToggleSwitchProps { + checked: boolean; + onChange: () => void; +} + +const ToggleSwitch: React.FC = ({ checked, onChange }) => ( +