diff --git a/src/__tests__/hooks/useContribution.test.tsx b/src/__tests__/hooks/useContribution.test.tsx new file mode 100644 index 00000000..1ebd9d1b --- /dev/null +++ b/src/__tests__/hooks/useContribution.test.tsx @@ -0,0 +1,89 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ReactNode } from "react"; +import { useContribution } from "@/hooks/useContribution"; + +jest.mock("@/lib/contractClient", () => ({ + getContribution: jest.fn(), +})); + +import { getContribution } from "@/lib/contractClient"; + +const mockGetContribution = getContribution as jest.MockedFunction; + +function createWrapper() { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return function Wrapper({ children }: { children: ReactNode }) { + return {children}; + }; +} + +describe("useContribution", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("fetches the contribution for a valid numeric campaign id", async () => { + mockGetContribution.mockResolvedValue(BigInt(5_000_000)); + + const { result } = renderHook(() => useContribution(1, "GUSER"), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.contribution).toBe(BigInt(5_000_000)); + expect(mockGetContribution).toHaveBeenCalledWith(1, "GUSER"); + }); + + it("does not fetch when campaignId is NaN (e.g. parsed from an invalid string)", async () => { + const id = parseInt("not-a-number", 10); + expect(Number.isNaN(id)).toBe(true); + + const { result } = renderHook(() => useContribution(id, "GUSER"), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetContribution).not.toHaveBeenCalled(); + expect(result.current.contribution).toBe(BigInt(0)); + }); + + it("does not fetch when userAddress is null", async () => { + const { result } = renderHook(() => useContribution(1, null), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetContribution).not.toHaveBeenCalled(); + }); + + it("still fetches when campaignId is a legitimate id of 0 (regression: !!campaignId was falsy for 0)", async () => { + mockGetContribution.mockResolvedValue(BigInt(1_000)); + + const { result } = renderHook(() => useContribution(0, "GUSER"), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetContribution).toHaveBeenCalledWith(0, "GUSER"); + expect(result.current.contribution).toBe(BigInt(1_000)); + }); + + it("parses a string campaignId before querying", async () => { + mockGetContribution.mockResolvedValue(BigInt(2_000)); + + const { result } = renderHook(() => useContribution("42", "GUSER"), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetContribution).toHaveBeenCalledWith(42, "GUSER"); + }); +}); diff --git a/src/__tests__/hooks/useRevenueSharing.test.tsx b/src/__tests__/hooks/useRevenueSharing.test.tsx new file mode 100644 index 00000000..e2278856 --- /dev/null +++ b/src/__tests__/hooks/useRevenueSharing.test.tsx @@ -0,0 +1,96 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ReactNode } from "react"; +import { useRevenueSharing } from "@/hooks/useRevenueSharing"; + +jest.mock("@/lib/contractClient", () => ({ + getContribution: jest.fn(), + getRevenuePool: jest.fn(), + getRevenueClaimed: jest.fn(), +})); + +import { getContribution, getRevenuePool, getRevenueClaimed } from "@/lib/contractClient"; + +const mockGetContribution = getContribution as jest.MockedFunction; +const mockGetRevenuePool = getRevenuePool as jest.MockedFunction; +const mockGetRevenueClaimed = getRevenueClaimed as jest.MockedFunction; + +function createWrapper() { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return function Wrapper({ children }: { children: ReactNode }) { + return {children}; + }; +} + +describe("useRevenueSharing", () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetRevenuePool.mockResolvedValue(BigInt(0)); + mockGetContribution.mockResolvedValue(BigInt(0)); + mockGetRevenueClaimed.mockResolvedValue(BigInt(0)); + }); + + it("fetches revenue sharing data for a valid numeric campaign id", async () => { + mockGetRevenuePool.mockResolvedValue(BigInt(1_000_000)); + mockGetContribution.mockResolvedValue(BigInt(100_000)); + mockGetRevenueClaimed.mockResolvedValue(BigInt(0)); + + const { result } = renderHook(() => useRevenueSharing(1, "GUSER", BigInt(500_000), true), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetRevenuePool).toHaveBeenCalledWith(1); + expect(result.current.revenuePool).toBe(BigInt(1_000_000)); + expect(result.current.contribution).toBe(BigInt(100_000)); + }); + + it("does not fetch when campaignId is NaN (e.g. parsed from an invalid string)", async () => { + const id = parseInt("not-a-number", 10); + expect(Number.isNaN(id)).toBe(true); + + const { result } = renderHook(() => useRevenueSharing(id, "GUSER", BigInt(0), true), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetRevenuePool).not.toHaveBeenCalled(); + }); + + it("does not fetch when the caller-provided enabled flag is false", async () => { + const { result } = renderHook(() => useRevenueSharing(1, "GUSER", BigInt(0), false), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetRevenuePool).not.toHaveBeenCalled(); + }); + + it("still fetches when campaignId is a legitimate id of 0 (regression: !!campaignId was falsy for 0)", async () => { + mockGetRevenuePool.mockResolvedValue(BigInt(50_000)); + + const { result } = renderHook(() => useRevenueSharing(0, "GUSER", BigInt(0), true), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetRevenuePool).toHaveBeenCalledWith(0); + expect(result.current.revenuePool).toBe(BigInt(50_000)); + }); + + it("parses a string campaignId before querying", async () => { + const { result } = renderHook(() => useRevenueSharing("42", "GUSER", BigInt(0), true), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockGetRevenuePool).toHaveBeenCalledWith(42); + }); +}); diff --git a/src/hooks/useContribution.ts b/src/hooks/useContribution.ts index fec3d652..01b4bcb7 100644 --- a/src/hooks/useContribution.ts +++ b/src/hooks/useContribution.ts @@ -9,7 +9,7 @@ export function useContribution(campaignId: number | string, userAddress: string const { data, isLoading } = useQuery({ queryKey: ["contribution", id, userAddress], queryFn: () => getContribution(id, userAddress!), - enabled: !!userAddress && !!campaignId && !isNaN(id), + enabled: !!userAddress && Number.isFinite(id), staleTime: 30_000, }); diff --git a/src/hooks/useRevenueSharing.ts b/src/hooks/useRevenueSharing.ts index e086533c..f28676a9 100644 --- a/src/hooks/useRevenueSharing.ts +++ b/src/hooks/useRevenueSharing.ts @@ -37,7 +37,7 @@ export function useRevenueSharing( const { data, isLoading } = useQuery({ queryKey: ["revenueSharing", id, walletAddress], queryFn: () => fetchRevenueSharing(id, walletAddress), - enabled: enabled && !!campaignId && !isNaN(id), + enabled: enabled && Number.isFinite(id), staleTime: 30_000, });