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";