diff --git a/__mocks__/fixtures/wallet.ts b/__mocks__/fixtures/wallet.ts index 64e7adcf6..20dffd664 100644 --- a/__mocks__/fixtures/wallet.ts +++ b/__mocks__/fixtures/wallet.ts @@ -18,7 +18,7 @@ export const mockWallet: Wallet = { balance: 0, user_id: mockUser.id, token: "ICP", - address: "", + address: "0xd05AfA87A599b8AD8Cff71b1eC7129e3Edfe08b9", payouts: [mockPayout], description: "" } \ No newline at end of file diff --git a/__tests__/components/cards/wallet/CashoutAddress.test.tsx b/__tests__/components/cards/wallet/CashoutAddress.test.tsx new file mode 100644 index 000000000..d58f707f0 --- /dev/null +++ b/__tests__/components/cards/wallet/CashoutAddress.test.tsx @@ -0,0 +1,110 @@ +import "@testing-library/jest-dom"; +import { screen, fireEvent } from "@testing-library/react"; +import { Wallet } from "@/types/wallet"; +import { renderWithRedux } from "@__mocks__/renderWithRedux"; +import CashoutAddress from "@/components/cards/wallet/CashoutAddress"; +import { mockUser } from "@__mocks__/fixtures/user"; +import { mockWallet } from "@__mocks__/fixtures/wallet"; +import { KYCSTATUS, openVerificationModal } from "@/store/feature/kyc.slice"; +import { useDispatch } from "@/hooks/useTypedDispatch"; + +jest.mock("@/hooks/useTypedDispatch.ts", () => ({ + useDispatch: jest.fn(), +})); + +const walletMock: Wallet = { + ...mockWallet, + title: "Test Wallet", + token: "BTC", + balance: 100, + description: "Test Wallet Description", + payouts: [], +}; + +jest.mock("@/store/feature/kyc.slice", () => { + const actual = jest.requireActual("@/store/feature/kyc.slice"); + return { + ...actual, + openVerificationModal: jest.fn(), + }; +}); + +const mockUserStore = { + user: { + data: mockUser, + userBalance: null, + balance: null, + walletAddresses: null, + token: null, + referrals: null, + fetchingUserLoading: false, + filteredUsers: null, + }, +}; + +const verifiedUserStore = { + user: { + data: { + ...mockUser, + kycStatus: KYCSTATUS.VERIFIED, + }, + userBalance: null, + balance: null, + walletAddresses: null, + token: null, + referrals: null, + fetchingUserLoading: false, + filteredUsers: null, + }, +}; +const address = walletMock.address ? walletMock.address.match(/.{1,4}/g) : null; + +describe("CashoutAddress component", () => { + let setShowEditModal: () => void; + let setShowPayoutModal: () => void; + const mockDispatch = jest.fn(); + + beforeEach(() => { + setShowEditModal = jest.fn(); + setShowPayoutModal = jest.fn(); + (useDispatch as jest.Mock).mockReturnValue(mockDispatch); + }); + + test("renders CashoutAddress component correctly with given props when cashable is true and there's address", () => { + renderWithRedux(, mockUserStore); + expect(screen.getByTestId("cashoutAddressId")).toBeInTheDocument(); + address?.forEach((text) => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + expect(screen.getByText("profile.wallets.address-change")).toBeInTheDocument(); + }); + + it("Renders wallet description when there is no address", () => { + renderWithRedux( + , + mockUserStore + ); + expect(screen.getByText(walletMock.description)).toBeInTheDocument(); + expect(screen.getByText("profile.wallets.address-set")).toBeInTheDocument(); + }); + + it("triggers cashout for verified user", () => { + renderWithRedux(, verifiedUserStore); + fireEvent.click(screen.getByText("profile.wallets.cash-out")); + expect(setShowPayoutModal).toHaveBeenCalledWith(true); + }); + + it("Should trigger KYC verification for non-verified user", () => { + renderWithRedux(, mockUserStore); + fireEvent.click(screen.getByText("profile.wallets.cash-out")); + expect(openVerificationModal).toHaveBeenCalled(); + }); + + it("Should trigger edit address modal", () => { + renderWithRedux(, verifiedUserStore); + fireEvent.click(screen.getByText("profile.wallets.address-change")); + expect(setShowEditModal).toHaveBeenCalledWith(true); + expect(mockDispatch).toHaveBeenCalled(); + }); + +}); diff --git a/__tests__/components/cards/wallet/Overview.test.tsx b/__tests__/components/cards/wallet/Overview.test.tsx new file mode 100644 index 000000000..6b18162a3 --- /dev/null +++ b/__tests__/components/cards/wallet/Overview.test.tsx @@ -0,0 +1,22 @@ +import Overview from "@/components/cards/wallet/Overview"; +import { mockWallet } from "@__mocks__/fixtures/wallet"; +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; + +const wallet = { + ...mockWallet, + title: "some title", +}; + +describe("Overview component", () => { + it("Should render Overview component and all the required elements with props value", () => { + render(); + expect(screen.getByTestId("overviewId")).toBeInTheDocument(); + expect(screen.getByText(wallet.title)).toBeInTheDocument(); + expect(screen.getByText("profile.wallets.balance")).toBeInTheDocument(); + expect(screen.getByTestId("currencyId")).toBeInTheDocument(); + expect(screen.getByTestId("coin")).toBeInTheDocument(); + expect(screen.getByTestId("tag")).toBeInTheDocument(); + }); + +}); diff --git a/__tests__/components/cards/wallet/WalletHint.test.tsx b/__tests__/components/cards/wallet/WalletHint.test.tsx new file mode 100644 index 000000000..1e71d7326 --- /dev/null +++ b/__tests__/components/cards/wallet/WalletHint.test.tsx @@ -0,0 +1,37 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { Wallet } from "@/types/wallet"; +import WalletHint from "@/components/cards/wallet/WalletHint"; +import { mockWallet } from "@__mocks__/fixtures/wallet"; + +const wallet: Wallet = { + ...mockWallet, + payouts: [ + { amount: 100, token: "ETH" }, + { amount: 200, token: "BTC" }, + ], +}; + +describe("WalletHint component", () => { + it("should render WalletHint component with the correct number of Hint components", () => { + render(); + const textElements = screen.getAllByText("profile.wallet.payout.text"); + expect(textElements).toHaveLength(wallet.payouts.length); + }); + + it("renders the correct number of Hint components", () => { + render(); + const hints = screen.getAllByText("profile.wallet.payout.text"); + expect(hints).toHaveLength(wallet.payouts.length); + }); + + it("renders Currency components with correct values", () => { + render(); + const currencyElements = screen.getAllByTestId("currencyId"); + expect(currencyElements).toHaveLength(wallet.payouts.length); + wallet.payouts.forEach((payout, index) => { + const element = currencyElements[index]; + expect(element).toHaveTextContent(new RegExp(`${payout.token}`)); + }); + }); +}); diff --git a/__tests__/components/cards/wallet/indext.test.tsx b/__tests__/components/cards/wallet/indext.test.tsx new file mode 100644 index 000000000..9b26244da --- /dev/null +++ b/__tests__/components/cards/wallet/indext.test.tsx @@ -0,0 +1,24 @@ +import CardsWallet from "@/components/cards/wallet"; +import "@testing-library/jest-dom"; +import { mockWallet } from "@__mocks__/fixtures/wallet"; +import { screen } from "@testing-library/react"; +import { renderWithRedux } from "@__mocks__/renderWithRedux"; + +// use the actual component when it is done tested +jest.mock("@/components/sections/profile/modals/EditAddress", () => { + return
Edit address component
; +}); + +jest.mock("@/hooks/useTypedDispatch.ts", () => ({ + useDispatch: jest.fn(), +})); + +describe("Wallet card component", () => { + it("renders the wallet component with all the children", () => { + renderWithRedux(); + expect(screen.getByTestId("cardWalletId")).toBeInTheDocument(); + expect(screen.getByTestId("overviewId")).toBeInTheDocument(); + expect(screen.getByTestId("cashoutAddressId")).toBeInTheDocument(); + expect(screen.queryByText("profile.wallet.payout.text")).not.toBeNull(); + }); +}); diff --git a/jest.config.js b/jest.config.js index e7ccd4e16..8c1a24d2f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -28,6 +28,7 @@ const config = { "react-markdown": "/__mocks__/react-markdown.tsx", [`^(${esModules})-.*`]: "/__mocks__/plugin.ts", unified: "/__mocks__/unified.ts", + "^@/(.*)$": "/src/$1", }, }; diff --git a/jest.polyfills.js b/jest.polyfills.js index c53fcc585..bc857172f 100644 --- a/jest.polyfills.js +++ b/jest.polyfills.js @@ -21,6 +21,7 @@ Object.defineProperties(globalThis, { const { Blob, File } = require("node:buffer"); const { fetch, Headers, FormData, Request, Response } = require("undici"); +const { clearImmediate } = require("node:timers"); Object.defineProperties(globalThis, { fetch: { value: fetch, writable: true }, @@ -30,4 +31,5 @@ Object.defineProperties(globalThis, { FormData: { value: FormData }, Request: { value: Request }, Response: { value: Response }, + clearImmediate: { value: clearImmediate }, }); diff --git a/src/components/cards/Wallet.tsx b/src/components/cards/Wallet.tsx deleted file mode 100644 index d5175b574..000000000 --- a/src/components/cards/Wallet.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { ReactElement, useState } from "react"; -import Coin from "@/components/ui/Coin"; -import ArrowButton from "@/components/ui/button/Arrow"; -import Tag from "@/components/ui/Tag"; -import Currency from "@/components/ui/Currency"; -import EditAddress from "@/components/sections/profile/modals/EditAddress"; -import Payout from "@/components/sections/profile/modals/Payout"; -import Hint from "@/components/ui/Hint"; -import { useTranslation } from "next-i18next"; -import { Wallet } from "@/types/wallet"; -import { toggleBodyScrolling } from "@/store/feature/ui.slice"; -import { useDispatch } from "@/hooks/useTypedDispatch"; -import { useSelector } from "@/hooks/useTypedSelector"; -import { setCurrentWallet } from "@/store/feature/user/wallets.slice"; -import { openVerificationModal } from "@/store/feature/kyc.slice"; - -/** - * Cards wallet props interface - */ -interface CardsWalletProps { - wallet: Wallet; - disabled?: boolean; -} - -/** - * Cards wallet component - * - * @returns {ReactElement} - */ - -export default function CardsWallet({ wallet, disabled = false }: CardsWalletProps): ReactElement { - const { t } = useTranslation(); - const [showEditModal, setShowEditModal] = useState(false); - const [showPayoutModal, setShowPayoutModal] = useState(false); - const dispatch = useDispatch(); - const user = useSelector((state) => state.user.data); - - const isKycVerified = user?.kycStatus === "VERIFIED"; - const address = wallet.address ? wallet.address.match(/.{1,4}/g) : null; - - const cashable = String(wallet.token).toUpperCase() !== "DAC"; - const triggerCashout = () => { - setShowPayoutModal(true); - dispatch(toggleBodyScrolling(true)); - }; - - const triggerKYCVerification = () => { - dispatch( - openVerificationModal({ - description: t("kyc.payout.reason"), - completedActionText: t("kyc.payout.button.completed"), - completedAction: () => { - triggerCashout(); - }, - }) - ); - }; - - const cashout = () => { - if (!isKycVerified) return triggerKYCVerification(); - triggerCashout(); - }; - - const onClose = () => { - setShowEditModal(false); - setShowPayoutModal(false); - dispatch(toggleBodyScrolling(false)); - }; - - const triggerEditAddress = () => { - dispatch(setCurrentWallet(wallet)); - setShowEditModal(true); - dispatch(toggleBodyScrolling(true)); - }; - - return ( -
-
- {showEditModal && } - -
-
-
-

{wallet.title}

- -
- -
-
-
-
-

{t("profile.wallets.balance")}

-
-
-

- -

-
-
-
-
-
- {cashable ? ( -
- {address ? ( -

- {address.map((part, k) => ( - - {part} - - ))} -

- ) : ( -

{wallet.description}

- )} -
- - {address ? t("profile.wallets.address-change") : t("profile.wallets.address-set")} - -
-
- ) : ( -
-

-

- )} - {cashable && ( -
- - {t("profile.wallets.cash-out")} - -
- )} -
-
- {wallet.payouts.map((payout, i) => ( - - - - {" "} - {t("profile.wallet.payout.text")} - - ))} -
- ); -} diff --git a/src/components/cards/wallet/CashoutAddress.tsx b/src/components/cards/wallet/CashoutAddress.tsx new file mode 100644 index 000000000..ed2eda109 --- /dev/null +++ b/src/components/cards/wallet/CashoutAddress.tsx @@ -0,0 +1,96 @@ +import ArrowButton from "@/components/ui/button/Arrow"; +import { useTranslation } from "next-i18next"; +import { useDispatch } from "@/hooks/useTypedDispatch"; +import { setCurrentWallet } from "@/store/feature/user/wallets.slice"; +import { toggleBodyScrolling } from "@/store/feature/ui.slice"; +import { useSelector } from "@/hooks/useTypedSelector"; +import { openVerificationModal } from "@/store/feature/kyc.slice"; +import { Wallet } from "@/types/wallet"; +import { useCallback, useMemo } from "react"; + +interface CashoutAddressProps { + wallet: Wallet; + setShowEditModal: (show: boolean) => void; + disabled: boolean; + setShowPayoutModal: (show: boolean) => void; + testId?:string +} + +export default function CashoutAddress({ wallet, setShowEditModal, disabled, setShowPayoutModal, testId='cashoutAddressId' }: CashoutAddressProps) { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const user = useSelector((state) => state.user.data); + const isKycVerified = user?.kycStatus === "VERIFIED"; + const address = useMemo(() => (wallet.address ? wallet.address.match(/.{1,4}/g) : null), [wallet.address]); + const cashable = useMemo(() => String(wallet.token).toUpperCase() !== "DAC", [wallet.token]); + + + const triggerEditAddress = useCallback(() => { + dispatch(setCurrentWallet(wallet)); + setShowEditModal(true); + dispatch(toggleBodyScrolling(true)); + }, [dispatch, setShowEditModal, wallet]); + + const triggerCashout = useCallback(() => { + setShowPayoutModal(true); + dispatch(toggleBodyScrolling(true)); + }, [setShowPayoutModal, dispatch]); + + const triggerKYCVerification = useCallback(() => { + dispatch( + openVerificationModal({ + description: t("kyc.payout.reason"), + completedActionText: t("kyc.payout.button.completed"), + completedAction: () => { + triggerCashout(); + }, + }) + ); + }, [dispatch, t, triggerCashout]); + const cashout = () => { + if (!isKycVerified) return triggerKYCVerification(); + triggerCashout(); + }; + return ( +
+ {cashable ? ( +
+ {address ? ( +

+ {address.map((part, k) => ( + + {part} + + ))} +

+ ) : ( +

{wallet.description}

+ )} +
+ + {address ? t("profile.wallets.address-change") : t("profile.wallets.address-set")} + +
+
+ ) : ( +
+

+

+ )} + {cashable && ( +
+ + {t("profile.wallets.cash-out")} + +
+ )} +
+ ); +} diff --git a/src/components/cards/wallet/Overview.tsx b/src/components/cards/wallet/Overview.tsx new file mode 100644 index 000000000..04426774d --- /dev/null +++ b/src/components/cards/wallet/Overview.tsx @@ -0,0 +1,37 @@ +import Coin from "@/components/ui/Coin"; +import Tag from "@/components/ui/Tag"; +import Currency from "@/components/ui/Currency"; +import { Wallet } from "@/types/wallet"; +import { useTranslation } from "next-i18next"; + +interface OverviewProps { + wallet: Wallet; + testId?: string +} + +export default function Overview({ wallet, testId='overviewId' }: OverviewProps) { + const { t } = useTranslation(); + return ( +
+
+
+

{wallet.title}

+ +
+ +
+
+
+
+

{t("profile.wallets.balance")}

+
+
+

+ +

+
+
+
+
+ ); +} diff --git a/src/components/cards/wallet/WalletHint.tsx b/src/components/cards/wallet/WalletHint.tsx new file mode 100644 index 000000000..63cd7eb10 --- /dev/null +++ b/src/components/cards/wallet/WalletHint.tsx @@ -0,0 +1,23 @@ +import { Wallet } from "@/types/wallet"; +import Currency from "@/components/ui/Currency"; +import Hint from "@/components/ui/Hint"; +import { useTranslation } from "next-i18next"; + +interface HintProps { + wallet: Wallet; +} +export default function WalletHint({ wallet }: HintProps) { + const { t } = useTranslation(); + return ( + <> + {wallet.payouts.map((payout, i) => ( + + + + {" "} + {t("profile.wallet.payout.text")} + + ))} + + ); +} diff --git a/src/components/cards/wallet/index.tsx b/src/components/cards/wallet/index.tsx new file mode 100644 index 000000000..ee28be31c --- /dev/null +++ b/src/components/cards/wallet/index.tsx @@ -0,0 +1,48 @@ +import { ReactElement, useCallback, useState } from "react"; +import EditAddress from "@/components/sections/profile/modals/EditAddress"; +import Payout from "@/components/sections/profile/modals/Payout"; +import { Wallet } from "@/types/wallet"; +import { toggleBodyScrolling } from "@/store/feature/ui.slice"; +import { useDispatch } from "@/hooks/useTypedDispatch"; +import CashoutAddress from "./CashoutAddress"; +import Overview from "./Overview"; +import WalletHint from "./WalletHint"; + +/** + * Cards wallet props interface + */ +interface CardsWalletProps { + wallet: Wallet; + disabled?: boolean; + testId?: string; +} + +/** + * Cards wallet component + * + * @returns {ReactElement} + */ + +export default function CardsWallet({ wallet, disabled = false, testId = "cardWalletId" }: CardsWalletProps): ReactElement { + const [showEditModal, setShowEditModal] = useState(false); + const [showPayoutModal, setShowPayoutModal] = useState(false); + const dispatch = useDispatch(); + + const onClose = useCallback(() => { + setShowEditModal(false); + setShowPayoutModal(false); + dispatch(toggleBodyScrolling(false)); + }, [dispatch]); + + return ( +
+
+ {showEditModal && } + + + +
+ +
+ ); +} diff --git a/src/components/sections/profile/modals/Payout.tsx b/src/components/sections/profile/modals/Payout.tsx index 99fc0ee9a..0aebef768 100644 --- a/src/components/sections/profile/modals/Payout.tsx +++ b/src/components/sections/profile/modals/Payout.tsx @@ -20,6 +20,7 @@ interface PayoutProps { show: boolean; wallet: Wallet; onClose: () => void; + testId?: string; } /** @@ -34,7 +35,7 @@ interface PayoutProps { } * @returns {ReactElement} */ -export default function Payout({ show, wallet, onClose }: PayoutProps): ReactElement { +export default function Payout({ show, wallet, onClose, testId = "payoutId" }: PayoutProps): ReactElement { const [loading, setLoading] = useState(false); const dispatch = useDispatch(); @@ -49,7 +50,7 @@ export default function Payout({ show, wallet, onClose }: PayoutProps): ReactEle return ( -
+

{wallet.title}

{t("profile.wallet.payout.request")}

diff --git a/src/pages/profile/wallets.tsx b/src/pages/profile/wallets.tsx index 1eb234846..2ace942a7 100644 --- a/src/pages/profile/wallets.tsx +++ b/src/pages/profile/wallets.tsx @@ -6,7 +6,7 @@ import { fetchAllWallets } from "@/store/services/wallets.service"; import { GetStaticProps } from "next"; import EditProfile from "@/components/sections/profile/modals/EditProfile"; -import Wallet from "@/components/cards/Wallet"; +import Wallet from "@/components/cards/wallet"; import Hint from "@/components/ui/Hint"; import ProfileLayout from "@/layouts/ProfileLayout"; import i18Translate from "@/utilities/I18Translate";