diff --git a/jest.config.ts b/jest.config.ts index 0cbc147..13a1b81 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -8,5 +8,6 @@ export default { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$": "/src/__test__/__mock__/fileMock.ts", "\\.(css|less)$": "identity-obj-proxy", + "~src/(.*)": "/src/$1", }, }; diff --git a/package-lock.json b/package-lock.json index 41239ca..17bc653 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "gsap": "^3.12.5", "install": "^0.13.0", "jest-environment-jsdom": "^29.7.0", - "jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^3.0.7", "jwt-decode": "^4.0.0", "moment": "^2.30.1", @@ -62,6 +61,7 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@testing-library/dom": "^10.2.0", + "@testing-library/jest-dom": "^6.4.8", "@types/jest": "^29.5.12", "@types/jwt-decode": "^3.1.0", "@types/node": "^20.14.8", @@ -90,6 +90,7 @@ "husky": "^8.0.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "lint-staged": "^15.2.5", "msw": "^2.3.1", "postcss": "^8.4.38", @@ -3766,9 +3767,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", - "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", + "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", "dev": true, "dependencies": { "@adobe/css-tools": "^4.4.0", @@ -3784,30 +3785,6 @@ "node": ">=14", "npm": ">=6", "yarn": ">=1" - }, - "peerDependencies": { - "@jest/globals": ">= 28", - "@types/bun": "latest", - "@types/jest": ">= 28", - "jest": ">= 28", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "@jest/globals": { - "optional": true - }, - "@types/bun": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "jest": { - "optional": true - }, - "vitest": { - "optional": true - } } }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { @@ -5709,6 +5686,7 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, "dependencies": { "node-fetch": "^2.6.12" } @@ -5717,6 +5695,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -5735,17 +5714,20 @@ "node_modules/cross-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true }, "node_modules/cross-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true }, "node_modules/cross-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -9293,6 +9275,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, "dependencies": { "cross-fetch": "^3.0.4", "promise-polyfill": "^8.1.3" @@ -14053,7 +14036,8 @@ "node_modules/promise-polyfill": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", - "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==" + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true }, "node_modules/prompts": { "version": "2.4.2", diff --git a/package.json b/package.json index 01897b9..7b88bb6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "gsap": "^3.12.5", "install": "^0.13.0", "jest-environment-jsdom": "^29.7.0", - "jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^3.0.7", "jwt-decode": "^4.0.0", "moment": "^2.30.1", @@ -70,6 +69,7 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@testing-library/dom": "^10.2.0", + "@testing-library/jest-dom": "^6.4.8", "@types/jest": "^29.5.12", "@types/jwt-decode": "^3.1.0", "@types/node": "^20.14.8", @@ -98,6 +98,7 @@ "husky": "^8.0.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "lint-staged": "^15.2.5", "msw": "^2.3.1", "postcss": "^8.4.38", diff --git a/src/App.tsx b/src/App.tsx index 8250142..dcdb582 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,19 +32,19 @@ const App: React.FC = () => { }; initializeSocket(); - dispatch(getUserNotifications()); return () => { disconnectFromSocket(); }; }, [dispatch]); - React.useEffect(() => { - dispatch(getUserNotifications()); - }, [dispatch]); React.useEffect(() => { - dispatch(handleCurrentUser()); - }, []); + const token = localStorage.getItem("accessToken"); + if (token) { + dispatch(handleCurrentUser()); + dispatch(getUserNotifications()); + } + }, [dispatch]); const { isPasswordExpired } = useAppSelector((state) => state.updatePin); diff --git a/src/__test__/currentUser.test.ts b/src/__test__/currentUser.test.ts new file mode 100644 index 0000000..a9e3b42 --- /dev/null +++ b/src/__test__/currentUser.test.ts @@ -0,0 +1,41 @@ +import api from "../redux/api/api"; +import { ICurrentUser } from "../redux/reducers/notificationSlice"; +import { getCurrentUser } from "../utils/currentuser"; + +jest.mock("../redux/api/api"); + +const mockUser: ICurrentUser = { + id: 1, + name: "John Doe", + username: "johndoe", + email: "john.doe@example.com", + password: "securepassword", + lastPasswordUpdateTime: new Date("2023-01-01T00:00:00Z"), + roleId: 2, + isActive: true, + isVerified: true, + createdAt: new Date("2022-01-01T00:00:00Z"), + updatedAt: new Date("2023-01-01T00:00:00Z"), +}; + +describe("getCurrentUser", () => { + beforeEach(() => { + (api.get as jest.Mock).mockReset(); + }); + + it("should return user data when API call is successful", async () => { + (api.get as jest.Mock).mockResolvedValue({ data: mockUser }); + + const result = await getCurrentUser(); + + expect(result).toEqual(mockUser); + }); + + it("should return null when API call fails", async () => { + (api.get as jest.Mock).mockRejectedValue(new Error("Network Error")); + + const result = await getCurrentUser(); + + expect(result).toBeNull(); + }); +}); diff --git a/src/__test__/notificationSlice.test.ts b/src/__test__/notificationSlice.test.ts index ce95b28..041cff5 100644 --- a/src/__test__/notificationSlice.test.ts +++ b/src/__test__/notificationSlice.test.ts @@ -87,7 +87,7 @@ describe("notificationSlice", () => { expect(state.unreadCount).toBe(1); }); - it.skip("should handle getUserNotifications thunk", async () => { + it("should handle getUserNotifications thunk", async () => { const mockNotifications: INotificationR[] = [ { id: 1, @@ -113,17 +113,13 @@ describe("notificationSlice", () => { data: { notifications: mockNotifications }, }); - (connectSocketMock as jest.Mock).mockReturnValue(socketMock); - - await store.dispatch(getUserNotifications()); + const result = await store.dispatch(getUserNotifications()); + console.log("Thunk result:", result); const state = store.getState().notifications; - console.log("State after getUserNotifications:", state); - // expect(state.notifications).toEqual(mockNotifications); - expect(state.unreadCount).toBe(1); - expect(socketMock.emit).not.toHaveBeenCalled(); - expect(socketMock.on).not.toHaveBeenCalled(); + expect(state.notifications).toEqual(mockNotifications); + expect(state.unreadCount).toBe(2); }); it("should handle readNotification thunk", async () => { diff --git a/src/__test__/userNotification.test.tsx b/src/__test__/userNotification.test.tsx new file mode 100644 index 0000000..6b2deca --- /dev/null +++ b/src/__test__/userNotification.test.tsx @@ -0,0 +1,209 @@ +import { render, screen } from "@testing-library/react"; +import { BrowserRouter as Router } from "react-router-dom"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; + +import * as currentUserUtils from "../utils/currentuser"; +import { + ICurrentUser, + INotificationR, +} from "../redux/reducers/notificationSlice"; +import UserNotifications from "../components/common/user-notifications/UserNotifcations"; + +// Mock the react-icons +jest.mock("react-icons/fa", () => ({ + FaEnvelope: () =>
, + FaEnvelopeOpenText: () =>
, +})); + +const mockStore = configureStore([]); + +describe("UserNotifications", () => { + let store; + + beforeEach(() => { + store = mockStore({ + notifications: { + notifications: [] as INotificationR[], + currentUser: null as ICurrentUser | null, + }, + }); + }); + + it("renders no notifications message when there are no notifications", () => { + render( + + + + + , + ); + + expect(screen.queryByTestId("no-notifications")).toBeDefined(); + expect( + screen.queryByText("You dont have any notification yet."), + ).toBeDefined(); + }); + + it("renders login prompt when user is not logged in", () => { + // @ts-ignore + jest.spyOn(currentUserUtils, "getCurrentUser").mockReturnValue(null); + + render( + + + + + , + ); + + expect(screen.queryByTestId("login-prompt")).toBeDefined(); + expect(screen.queryByTestId("login-button")).toBeDefined(); + }); + + it("renders notifications list when there are notifications", () => { + const mockNotifications: INotificationR[] = [ + { + id: 1, + userId: 1, + title: "Notification 1", + message: "Test notification 1", + isRead: false, + createdAt: new Date("2023-01-01T12:00:00"), + updatedAt: new Date(), + }, + { + id: 2, + userId: 1, + title: "Notification 2", + message: "Test notification 2", + isRead: true, + createdAt: new Date("2023-01-02T12:00:00"), + updatedAt: new Date(), + }, + ]; + + store = mockStore({ + notifications: { + notifications: mockNotifications, + currentUser: { roleId: 1 } as ICurrentUser, + }, + }); + + render( + + + + + , + ); + + expect(screen.queryByTestId("notifications-list")).toBeDefined(); + expect(screen.queryAllByTestId(/^notification-/)).toHaveLength(6); + expect(screen.queryByTestId("unread-icon")).toBeDefined(); + expect(screen.queryByTestId("read-icon")).toBeDefined(); + }); + + it("sorts notifications by date in descending order", () => { + const mockNotifications: INotificationR[] = [ + { + id: 1, + userId: 1, + title: "Older notification", + message: "Older notification", + isRead: false, + createdAt: new Date("2023-01-01T12:00:00"), + updatedAt: new Date(), + }, + { + id: 2, + userId: 1, + title: "Newer notification", + message: "Newer notification", + isRead: true, + createdAt: new Date("2023-01-02T12:00:00"), + updatedAt: new Date(), + }, + ]; + + store = mockStore({ + notifications: { + notifications: mockNotifications, + currentUser: { roleId: 1 } as ICurrentUser, + }, + }); + + render( + + + + + , + ); + + expect(screen.queryByTestId("notification-message-2")).toBeDefined(); + expect(screen.queryByTestId("notification-message-1")).toBeDefined(); + }); + + it("renders correct link for regular user", () => { + const mockNotifications: INotificationR[] = [ + { + id: 1, + userId: 1, + title: "Test notification", + message: "Test notification", + isRead: false, + createdAt: new Date("2023-01-01T12:00:00"), + updatedAt: new Date(), + }, + ]; + + store = mockStore({ + notifications: { + notifications: mockNotifications, + currentUser: { roleId: 1 } as ICurrentUser, + }, + }); + + render( + + + + + , + ); + + expect(screen.queryByTestId("notification-1")).toBeDefined(); + }); + + it("renders correct link for admin user", () => { + const mockNotifications: INotificationR[] = [ + { + id: 1, + userId: 1, + title: "Test notification", + message: "Test notification", + isRead: false, + createdAt: new Date("2023-01-01T12:00:00"), + updatedAt: new Date(), + }, + ]; + + store = mockStore({ + notifications: { + notifications: mockNotifications, + currentUser: { roleId: 2 } as ICurrentUser, + }, + }); + + render( + + + + + , + ); + + expect(screen.queryByTestId("notification-1")).toBeDefined(); + }); +}); diff --git a/src/components/common/header/Header.tsx b/src/components/common/header/Header.tsx index 92aa527..b6d733d 100644 --- a/src/components/common/header/Header.tsx +++ b/src/components/common/header/Header.tsx @@ -200,7 +200,7 @@ const Header: React.FC = ({ searchQuery, setSearchQuery }) => { )} - {userInfo.name.split(" ")[0]} + {userInfo.name?.split(" ")[0]} {dropdownOpen && } diff --git a/src/components/common/user-notifications/UserNotifcations.tsx b/src/components/common/user-notifications/UserNotifcations.tsx index 744a517..52c5436 100644 --- a/src/components/common/user-notifications/UserNotifcations.tsx +++ b/src/components/common/user-notifications/UserNotifcations.tsx @@ -34,7 +34,10 @@ const UserNotifications = () => { if (notifications.length < 1) { return ( -
+

You dont have any notification yet.

@@ -48,12 +51,16 @@ const UserNotifications = () => { if (!getCurrentUser) { return ( -
+
@@ -63,7 +70,8 @@ const UserNotifications = () => { return (
{sortedNotifications.map((notification, index) => ( @@ -74,24 +82,40 @@ const UserNotifications = () => { : `/notifications/${notification.id}` } key={index} + data-testid={`notification-${notification.id}`} className={`flex sm:flex-row flex-col justify-between items-center mb-[3px] p-4 rounded-md gap-4 ${notification.isRead ? "bg-[#FFFFFF]" : "bg-[#E1ECF4]"}`} >
{notification.isRead ? ( - + ) : ( - + )}
-

+

{notification.message}

-

+

{formatDate(notification.createdAt)}

-

+

View Detail

diff --git a/src/pages/ProductPage.tsx b/src/pages/ProductPage.tsx index af02cfe..206fb16 100644 --- a/src/pages/ProductPage.tsx +++ b/src/pages/ProductPage.tsx @@ -91,7 +91,7 @@ const ProductPage = () => { const handleFilter = (filtered: IProduct[]) => { setFilteredProducts(filtered); - setCurrentPage(1); + // setCurrentPage(1); }; const indexOfLastItem = currentPage * itemsPerPage; @@ -109,10 +109,9 @@ const ProductPage = () => { }; const handleItemsPerPageChange = ( - event: React.ChangeEvent<{ value: unknown }>, + event: React.ChangeEvent, ) => { - setItemsPerPage(event.target.value as number); - setCurrentPage(1); + setItemsPerPage(Number(event.target.value)); }; if (loading) { diff --git a/src/pages/paymentPage.tsx b/src/pages/paymentPage.tsx index 3bb3dec..07ee29f 100644 --- a/src/pages/paymentPage.tsx +++ b/src/pages/paymentPage.tsx @@ -174,5 +174,24 @@ const SuccessfulPayment = () => { ); }; +const CancelledPayment = () => ( +
+
+

+ ❌ Payment Was Cancelled +

+

+ Checkout Details about your carts If you wish to adjust ! +

+

Thank you for shopping with us.

+ + + + +
+
+); export default Payment; -export { SuccessfulPayment }; +export { SuccessfulPayment, CancelledPayment }; diff --git a/src/redux/reducers/notificationSlice.ts b/src/redux/reducers/notificationSlice.ts index e11016b..9032b68 100644 --- a/src/redux/reducers/notificationSlice.ts +++ b/src/redux/reducers/notificationSlice.ts @@ -121,10 +121,10 @@ const notificationSlice = createSlice({ .addCase( getUserNotifications.fulfilled, (state, action: PayloadAction) => { - // state.notifications = action.payload; - // state.unreadCount = action.payload.filter( - // (notification) => !notification.isRead, - // ).length; + state.notifications = action.payload; + state.unreadCount = action.payload.filter( + (notification) => !notification.isRead, + ).length; }, ) .addCase(handleCurrentUser.fulfilled, (state, action) => { diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 4054464..feb3318 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -33,10 +33,13 @@ import BuyerOrders from "../pages/BuyerOrders"; import SignupVerification from "../pages/SignupVerification"; import SmoothScroll from "../utils/SmoothScroll"; import NotFound from "../pages/NotFound"; -import Payment, { SuccessfulPayment } from "../pages/paymentPage"; import UserNotifications from "../components/common/user-notifications/UserNotifcations"; import UserNotificationDetail from "../components/common/user-notifications/UserNotificationDetail"; import { LogoutProvider } from "../components/dashboard/admin/LogoutContext"; +import Payment, { + CancelledPayment, + SuccessfulPayment, +} from "../pages/paymentPage"; const AppRoutes = () => { const navigate = useNavigate(); @@ -60,62 +63,64 @@ const AppRoutes = () => { }; return ( - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + + } /> + } /> + } /> + } /> + } /> + } + path="/login" + element={( + + + + )} /> - - } /> - } /> - } /> - } /> - } /> - - - - - )} - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } - /> - } /> - } /> - + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } + /> + } /> + } /> + + ); }; diff --git a/src/utils/socket.ts b/src/utils/socket.ts index 796fdc4..5ed3897 100644 --- a/src/utils/socket.ts +++ b/src/utils/socket.ts @@ -25,9 +25,13 @@ export const connectToSocket = async () => { // alert("CONNECTED TO THE SERVER"); }); - const user = await getCurrentUser(); - if (user && user.id !== null) { - socket.emit("joinRoom", `${user.id}`); + const token = localStorage.getItem("accessToken"); + + if (token) { + const user = await getCurrentUser(); + if (user && user.id !== null) { + socket.emit("joinRoom", `${user.id}`); + } } socket.on("notification", (notification) => {