From f19602c9fb50575c675f65f84bfe61164d699ae2 Mon Sep 17 00:00:00 2001 From: Nazar Ilamanov Date: Tue, 21 Jan 2025 18:23:46 -0800 Subject: [PATCH] Add Vitest testing to the CI pipeline --- .env.test | 2 +- .github/workflows/main.yml | 5 +- ....test.tsx => useEvmTokenBalances.test.tsx} | 34 +-- ...s.test.tsx => useEvmTransactions.test.tsx} | 38 +-- __tests__/hooks/useSvmTokenBalances.test.tsx | 141 +++++++++++ __tests__/hooks/useSvmTransactions.test.tsx | 225 ++++++++++++++++++ src/svm/useSvmTransactions.ts | 2 +- 7 files changed, 410 insertions(+), 37 deletions(-) rename __tests__/hooks/{useTokenBalances.test.tsx => useEvmTokenBalances.test.tsx} (75%) rename __tests__/hooks/{useTransactions.test.tsx => useEvmTransactions.test.tsx} (82%) create mode 100644 __tests__/hooks/useSvmTokenBalances.test.tsx create mode 100644 __tests__/hooks/useSvmTransactions.test.tsx diff --git a/.env.test b/.env.test index f42629d..43326e1 100644 --- a/.env.test +++ b/.env.test @@ -1 +1 @@ -DUNE_API_KEY= \ No newline at end of file +DUNE_API_KEY=test \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a67c3e4..acddc48 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,5 +16,8 @@ jobs: - name: Install dependencies run: npm install --no-audit --no-fund - - name: "Test: prettier" + - name: "Test: Prettier" run: npm run test:prettier + + - name: "Test: Vitest" + run: npm test diff --git a/__tests__/hooks/useTokenBalances.test.tsx b/__tests__/hooks/useEvmTokenBalances.test.tsx similarity index 75% rename from __tests__/hooks/useTokenBalances.test.tsx rename to __tests__/hooks/useEvmTokenBalances.test.tsx index f052bb7..161b975 100644 --- a/__tests__/hooks/useTokenBalances.test.tsx +++ b/__tests__/hooks/useEvmTokenBalances.test.tsx @@ -1,16 +1,16 @@ import React from "react"; import { renderHook, waitFor } from "@testing-library/react"; import { DuneProvider } from "../../src/provider"; -import { useTokenBalances } from "../../src/useTokenBalances"; -import { fetchBalances } from "../../src/duneApi"; +import { useEvmTokenBalances } from "../../src/evm/useEvmTokenBalances"; +import { fetchEvmBalances } from "../../src/evm/duneApi"; import { vi } from "vitest"; // Mock the Dune API -vi.mock("../../src/duneApi", () => ({ - fetchBalances: vi.fn(), +vi.mock("../../src/evm/duneApi", () => ({ + fetchEvmBalances: vi.fn(), })); -const mockFetchBalances = fetchBalances as jest.Mock; +const mockFetchEvmBalances = fetchEvmBalances as jest.Mock; // A wrapper for the hook that provides the required context const wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -25,7 +25,9 @@ describe("useTokenBalances", () => { }); it("should return null if the wallet address is not a valid address", () => { - const { result } = renderHook(() => useTokenBalances("0x123"), { wrapper }); + const { result } = renderHook(() => useEvmTokenBalances("0x123"), { + wrapper, + }); expect(result.current).toEqual({ data: null, @@ -54,9 +56,9 @@ describe("useTokenBalances", () => { }, ], }; - mockFetchBalances.mockResolvedValueOnce(mockResponse); + mockFetchEvmBalances.mockResolvedValueOnce(mockResponse); - const { result } = renderHook(() => useTokenBalances(walletAddress), { + const { result } = renderHook(() => useEvmTokenBalances(walletAddress), { wrapper, }); @@ -68,7 +70,7 @@ describe("useTokenBalances", () => { expect(result.current.isLoading).toBe(false); }); - expect(mockFetchBalances).toHaveBeenCalledWith( + expect(mockFetchEvmBalances).toHaveBeenCalledWith( walletAddress, {}, process.env.DUNE_API_KEY @@ -82,9 +84,9 @@ describe("useTokenBalances", () => { const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; const mockError = new Error("Failed to fetch token balances"); - mockFetchBalances.mockRejectedValueOnce(mockError); + mockFetchEvmBalances.mockRejectedValueOnce(mockError); - const { result } = renderHook(() => useTokenBalances(walletAddress), { + const { result } = renderHook(() => useEvmTokenBalances(walletAddress), { wrapper, }); @@ -96,7 +98,7 @@ describe("useTokenBalances", () => { expect(result.current.isLoading).toBe(false); }); - expect(mockFetchBalances).toHaveBeenCalledWith( + expect(mockFetchEvmBalances).toHaveBeenCalledWith( walletAddress, {}, process.env.DUNE_API_KEY @@ -113,11 +115,11 @@ describe("useTokenBalances", () => { {children} ); - const { result } = renderHook(() => useTokenBalances(walletAddress), { + const { result } = renderHook(() => useEvmTokenBalances(walletAddress), { wrapper: localWrapper, }); - expect(mockFetchBalances).not.toHaveBeenCalled(); + expect(mockFetchEvmBalances).not.toHaveBeenCalled(); expect(result.current).toEqual({ data: null, error: null, @@ -126,9 +128,9 @@ describe("useTokenBalances", () => { }); it("should not fetch data if the wallet address is missing", () => { - const { result } = renderHook(() => useTokenBalances(""), { wrapper }); + const { result } = renderHook(() => useEvmTokenBalances(""), { wrapper }); - expect(mockFetchBalances).not.toHaveBeenCalled(); + expect(mockFetchEvmBalances).not.toHaveBeenCalled(); expect(result.current).toEqual({ data: null, error: null, diff --git a/__tests__/hooks/useTransactions.test.tsx b/__tests__/hooks/useEvmTransactions.test.tsx similarity index 82% rename from __tests__/hooks/useTransactions.test.tsx rename to __tests__/hooks/useEvmTransactions.test.tsx index 1286c02..43a2ab3 100644 --- a/__tests__/hooks/useTransactions.test.tsx +++ b/__tests__/hooks/useEvmTransactions.test.tsx @@ -1,16 +1,16 @@ import React from "react"; import { renderHook, act, waitFor } from "@testing-library/react"; import { DuneProvider } from "../../src/provider"; -import { useTransactions } from "../../src/useTransactions"; -import { fetchTransactions } from "../../src/duneApi"; +import { useEvmTransactions } from "../../src/evm/useEvmTransactions"; +import { fetchEvmTransactions } from "../../src/evm/duneApi"; import { vi } from "vitest"; // Mock the Dune API -vi.mock("../../src/duneApi", () => ({ - fetchTransactions: vi.fn(), +vi.mock("../../src/evm/duneApi", () => ({ + fetchEvmTransactions: vi.fn(), })); -const mockFetchTransactions = fetchTransactions as jest.Mock; +const mockFetchEvmTransactions = fetchEvmTransactions as jest.Mock; // A wrapper for the hook that provides the required context const wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -25,7 +25,9 @@ describe("useTransactions", () => { }); it("should return null if the wallet address is not a valid address", () => { - const { result } = renderHook(() => useTransactions("0x123"), { wrapper }); + const { result } = renderHook(() => useEvmTransactions("0x123"), { + wrapper, + }); expect(result.current).toEqual({ data: null, @@ -72,9 +74,9 @@ describe("useTransactions", () => { ], next_offset: "offset1", }; - mockFetchTransactions.mockResolvedValueOnce(mockResponse); + mockFetchEvmTransactions.mockResolvedValueOnce(mockResponse); - const { result } = renderHook(() => useTransactions(walletAddress), { + const { result } = renderHook(() => useEvmTransactions(walletAddress), { wrapper, }); @@ -86,7 +88,7 @@ describe("useTransactions", () => { expect(result.current.isLoading).toBe(false); }); - expect(mockFetchTransactions).toHaveBeenCalledWith( + expect(mockFetchEvmTransactions).toHaveBeenCalledWith( walletAddress, { offset: undefined }, process.env.DUNE_API_KEY @@ -100,9 +102,9 @@ describe("useTransactions", () => { const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; const mockError = new Error("Failed to fetch transactions"); - mockFetchTransactions.mockRejectedValueOnce(mockError); + mockFetchEvmTransactions.mockRejectedValueOnce(mockError); - const { result } = renderHook(() => useTransactions(walletAddress), { + const { result } = renderHook(() => useEvmTransactions(walletAddress), { wrapper, }); @@ -114,7 +116,7 @@ describe("useTransactions", () => { expect(result.current.isLoading).toBe(false); }); - expect(mockFetchTransactions).toHaveBeenCalledWith( + expect(mockFetchEvmTransactions).toHaveBeenCalledWith( walletAddress, { offset: undefined }, process.env.DUNE_API_KEY @@ -139,12 +141,12 @@ describe("useTransactions", () => { next_offset: "offset2", }; - mockFetchTransactions + mockFetchEvmTransactions .mockResolvedValueOnce(page1Response) .mockResolvedValueOnce(page2Response) .mockResolvedValueOnce(page1Response); - const { result } = renderHook(() => useTransactions(walletAddress), { + const { result } = renderHook(() => useEvmTransactions(walletAddress), { wrapper, }); @@ -190,11 +192,11 @@ describe("useTransactions", () => { {children} ); - const { result } = renderHook(() => useTransactions(walletAddress), { + const { result } = renderHook(() => useEvmTransactions(walletAddress), { wrapper: localWrapper, }); - expect(mockFetchTransactions).not.toHaveBeenCalled(); + expect(mockFetchEvmTransactions).not.toHaveBeenCalled(); expect(result.current).toEqual({ data: null, error: null, @@ -208,9 +210,9 @@ describe("useTransactions", () => { }); it("should not fetch data if the wallet address is missing", () => { - const { result } = renderHook(() => useTransactions(""), { wrapper }); + const { result } = renderHook(() => useEvmTransactions(""), { wrapper }); - expect(mockFetchTransactions).not.toHaveBeenCalled(); + expect(mockFetchEvmTransactions).not.toHaveBeenCalled(); expect(result.current).toEqual({ data: null, error: null, diff --git a/__tests__/hooks/useSvmTokenBalances.test.tsx b/__tests__/hooks/useSvmTokenBalances.test.tsx new file mode 100644 index 0000000..09ea00c --- /dev/null +++ b/__tests__/hooks/useSvmTokenBalances.test.tsx @@ -0,0 +1,141 @@ +import React from "react"; +import { renderHook, waitFor } from "@testing-library/react"; +import { DuneProvider } from "../../src/provider"; +import { useSvmTokenBalances } from "../../src/svm/useSvmTokenBalances"; +import { fetchSvmBalances } from "../../src/svm/duneApi"; +import { vi } from "vitest"; + +// Mock the Dune API +vi.mock("../../src/svm/duneApi", () => ({ + fetchSvmBalances: vi.fn(), +})); + +const mockFetchSvmBalances = fetchSvmBalances as jest.Mock; + +// A wrapper for the hook that provides the required context +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +describe("useTokenBalances", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should fetch token balances successfully", async () => { + const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; + + const mockResponse = { + request_time: "2025-01-16T18:09:37.116ZZ", + response_time: "2025-01-16T18:09:37.156ZZ", + wallet_address: walletAddress, + balances: [ + { + chain: "ethereum", + chain_id: 1, + address: "native", + amount: "121458493673814687", + symbol: "ETH", + decimals: 18, + price_usd: 3344.858473355283, + value_usd: 406.26147172582813, + }, + ], + }; + + mockFetchSvmBalances.mockResolvedValueOnce(mockResponse); + + const { result: svmResult } = renderHook( + () => useSvmTokenBalances(walletAddress), + { + wrapper, + } + ); + + // Initially, `isLoading` should be true + expect(svmResult.current.isLoading).toBe(true); + + // Wait for the hook to update + await waitFor(() => { + expect(svmResult.current.isLoading).toBe(false); + }); + + expect(mockFetchSvmBalances).toHaveBeenCalledWith( + walletAddress, + {}, + process.env.DUNE_API_KEY + ); + expect(svmResult.current.isLoading).toBe(false); + expect(svmResult.current.error).toBeNull(); + expect(svmResult.current.data).toEqual(mockResponse); + }); + + it("should handle errors when fetching token balances", async () => { + const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; + + const mockError = new Error("Failed to fetch token balances"); + + mockFetchSvmBalances.mockRejectedValueOnce(mockError); + + const { result: svmResult } = renderHook( + () => useSvmTokenBalances(walletAddress), + { + wrapper, + } + ); + + // Initially, `isLoading` should be true + expect(svmResult.current.isLoading).toBe(true); + + // Wait for the hook to update + await waitFor(() => { + expect(svmResult.current.isLoading).toBe(false); + }); + + expect(mockFetchSvmBalances).toHaveBeenCalledWith( + walletAddress, + {}, + process.env.DUNE_API_KEY + ); + expect(svmResult.current.isLoading).toBe(false); + expect(svmResult.current.error).toEqual(mockError); + expect(svmResult.current.data).toBeNull(); + }); + + it("should not fetch data if the API key is missing", () => { + const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; + + const localWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result: svmResult } = renderHook( + () => useSvmTokenBalances(walletAddress), + { + wrapper: localWrapper, + } + ); + + expect(mockFetchSvmBalances).not.toHaveBeenCalled(); + expect(svmResult.current).toEqual({ + data: null, + error: null, + isLoading: false, + }); + }); + + it("should not fetch data if the wallet address is missing", () => { + const { result: svmResult } = renderHook(() => useSvmTokenBalances(""), { + wrapper, + }); + + expect(mockFetchSvmBalances).not.toHaveBeenCalled(); + expect(svmResult.current).toEqual({ + data: null, + error: null, + isLoading: false, + }); + }); +}); diff --git a/__tests__/hooks/useSvmTransactions.test.tsx b/__tests__/hooks/useSvmTransactions.test.tsx new file mode 100644 index 0000000..143b4c2 --- /dev/null +++ b/__tests__/hooks/useSvmTransactions.test.tsx @@ -0,0 +1,225 @@ +import React from "react"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { DuneProvider } from "../../src/provider"; +import { useSvmTransactions } from "../../src/svm/useSvmTransactions"; +import { fetchSvmTransactions } from "../../src/svm/duneApi"; +import { vi } from "vitest"; + +// Mock the Dune API +vi.mock("../../src/svm/duneApi", () => ({ + fetchSvmTransactions: vi.fn(), +})); + +const mockFetchSvmTransactions = fetchSvmTransactions as jest.Mock; + +// A wrapper for the hook that provides the required context +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +describe("useTransactions", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should fetch transactions successfully", async () => { + const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; + + const mockResponse = { + rrequest_time: "2025-01-16T18:09:37.116ZZ", + response_time: "2025-01-16T18:09:37.156ZZ", + wallet_address: walletAddress, + transactions: [ + { + chain: "ethereum", + chain_id: 1, + address: "0x1234567890abcdef1234567890abcdef12345678", + block_time: "2025-01-15T08:00:11+00:00", + block_number: 21628549, + index: 187, + hash: "0xda2c542c33c62fdf9f9a46e271e9b730086f6bf05f19e2a886079ae71a470a49", + block_hash: + "0x38e3851e2c21edf1d71c46ef2d28afdfc7c571b8c57b00cfe8f3d3c34ef62375", + value: "0x3b9aca00", + transaction_type: "Receiver", + from: "0xf0ee8fc751a28d4f8f2716e74cf5da46e55ff437", + to: "0x1234567890abcdef1234567890abcdef12345678", + nonce: "0x8", + gas_price: "0xc0dbeb5e", + gas_used: "0x5208", + effective_gas_price: "0xc0dbeb5e", + success: true, + data: "0x", + logs: [], + }, + ], + next_offset: "offset1", + }; + + mockFetchSvmTransactions.mockResolvedValueOnce(mockResponse); + + const { result: svmResult } = renderHook( + () => useSvmTransactions(walletAddress), + { + wrapper, + } + ); + + // Initially, `isLoading` should be true + expect(svmResult.current.isLoading).toBe(true); + + // Wait for the hook to update + await waitFor(() => { + expect(svmResult.current.isLoading).toBe(false); + }); + + expect(mockFetchSvmTransactions).toHaveBeenCalledWith( + walletAddress, + { offset: undefined }, + process.env.DUNE_API_KEY + ); + expect(svmResult.current.data).toEqual(mockResponse); + expect(svmResult.current.nextOffset).toBe("offset1"); + expect(svmResult.current.error).toBeNull(); + }); + + it("should handle errors when fetching transactions", async () => { + const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; + + const mockError = new Error("Failed to fetch transactions"); + mockFetchSvmTransactions.mockRejectedValueOnce(mockError); + + const { result: svmResult } = renderHook( + () => useSvmTransactions(walletAddress), + { + wrapper, + } + ); + + // Initially, `isLoading` should be true + expect(svmResult.current.isLoading).toBe(true); + + // Wait for the hook to update + await waitFor(() => { + expect(svmResult.current.isLoading).toBe(false); + }); + + expect(mockFetchSvmTransactions).toHaveBeenCalledWith( + walletAddress, + { offset: undefined }, + process.env.DUNE_API_KEY + ); + expect(svmResult.current.error).toEqual(mockError); + expect(svmResult.current.data).toBeNull(); + }); + + it("should handle pagination: next and previous pages", async () => { + const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; + + const page1Response = { + transactions: [ + { hash: "0x1", from: "0xabc", to: "0xdef", block_number: 123 }, + ], + next_offset: "offset1", + }; + const page2Response = { + transactions: [ + { hash: "0x2", from: "0xghi", to: "0xjkl", block_number: 124 }, + ], + next_offset: "offset2", + }; + + mockFetchSvmTransactions + .mockResolvedValueOnce(page1Response) + .mockResolvedValueOnce(page2Response) + .mockResolvedValueOnce(page1Response); + + const { result: svmResult } = renderHook( + () => useSvmTransactions(walletAddress), + { + wrapper, + } + ); + + // Wait for the first page + await waitFor(() => { + expect(svmResult.current.isLoading).toBe(false); + }); + + expect(svmResult.current.data).toEqual(page1Response); + expect(svmResult.current.currentPage).toBe(0); + + // Fetch the next page + act(() => { + svmResult.current.nextPage(); + }); + + // Wait for the second page + await waitFor(() => { + expect(svmResult.current.isLoading).toBe(false); + }); + + expect(svmResult.current.data).toEqual(page2Response); + expect(svmResult.current.currentPage).toBe(1); + + // Fetch the previous page + act(() => { + svmResult.current.previousPage(); + }); + + // Wait for the first page again + await waitFor(() => { + expect(svmResult.current.isLoading).toBe(false); + }); + + expect(svmResult.current.data).toEqual(page1Response); + expect(svmResult.current.currentPage).toBe(0); + }); + + it("should not fetch data if the API key is missing", () => { + const walletAddress = "0x1234567890abcdef1234567890abcdef12345678"; + + const localWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result: svmResult } = renderHook( + () => useSvmTransactions(walletAddress), + { + wrapper: localWrapper, + } + ); + + expect(mockFetchSvmTransactions).not.toHaveBeenCalled(); + expect(svmResult.current).toEqual({ + data: null, + error: null, + isLoading: false, + nextOffset: null, + offsets: [], + currentPage: 0, + nextPage: expect.any(Function), + previousPage: expect.any(Function), + }); + }); + + it("should not fetch data if the wallet address is missing", () => { + const { result: svmResult } = renderHook(() => useSvmTransactions(""), { + wrapper, + }); + + expect(mockFetchSvmTransactions).not.toHaveBeenCalled(); + expect(svmResult.current).toEqual({ + data: null, + error: null, + isLoading: false, + nextOffset: null, + offsets: [], + currentPage: 0, + nextPage: expect.any(Function), + previousPage: expect.any(Function), + }); + }); +}); diff --git a/src/svm/useSvmTransactions.ts b/src/svm/useSvmTransactions.ts index b1a7eba..33314c9 100644 --- a/src/svm/useSvmTransactions.ts +++ b/src/svm/useSvmTransactions.ts @@ -30,7 +30,7 @@ export const useSvmTransactions = ( // Function to fetch data for a specific page const fetchDataAsync = async (offset: string | null) => { - if (!walletAddress) return; + if (!apiKey || !walletAddress) return; setState((prevState) => ({ ...prevState, isLoading: true }));