From 01208ff8925086af719f2c5b7163b01455a9db6e Mon Sep 17 00:00:00 2001 From: KidDev88 Date: Fri, 23 Oct 2026 21:36:56 +0100 Subject: [PATCH 1/4] fix: remove legacy Freighter path from connectWallet The multi-wallet StellarWalletsKit migration left a dead Freighter-only code path in connectWallet that redeclared the address binding and called isFreighterInstalled()/connectFreighter() without importing them. Drop the legacy block so connectWallet uses connectStellarWallet() for all wallet types (Freighter, LOBSTR, Rabet). Closes #531 --- src/lib/hooks/use-auth.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/lib/hooks/use-auth.ts b/src/lib/hooks/use-auth.ts index df81e90..02b5a46 100644 --- a/src/lib/hooks/use-auth.ts +++ b/src/lib/hooks/use-auth.ts @@ -135,31 +135,13 @@ export function useAuth() { setWalletError(null); try { - const { address, providerId } = await connectStellarWallet(networkRef.current); - // Check Freighter is installed - const installed = await isFreighterInstalled(); - if (!installed) { - const walletErr: WalletError = { - type: "not_installed", - message: "Freighter wallet extension is not installed", - resolution: - "Install Freighter from freighter.app and refresh this page.", - }; - setWalletError(walletErr); - walletErrorSetInThisCall = true; - setError(walletErr.message); - throw new Error(walletErr.message); - } - - // Connect to Freighter setConnectionStage("connect"); - const address = await connectFreighter(); + const { address, providerId } = await connectStellarWallet(networkRef.current); // Get challenge from backend const challenge = await getChallenge(address); // Sign the backend challenge with the selected wallet. - // Sign challenge with Freighter setConnectionStage("sign"); const passphrase = getNetworkPassphrase(networkRef.current); const signedChallenge = await signChallenge(challenge, passphrase, providerId); From 91e04f2dbd3932c9dedf1d6a939a6406840c998c Mon Sep 17 00:00:00 2001 From: KidDev88 Date: Fri, 23 Oct 2026 21:37:02 +0100 Subject: [PATCH 2/4] fix: add missing react-easy-crop dependency avatar-upload.tsx imports react-easy-crop for image cropping but the package was never declared in package.json, so fresh installs fail to resolve the import and break the settings avatar upload flow. Closes #532 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 245c391..49feba6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "next": "^14.2.0", "react": "^18.3.0", "react-dom": "^18.3.0", + "react-easy-crop": "^6.2.4", "tailwind-merge": "^2.6.0", "zod": "^3.23.8", "zustand": "^5.0.0" From fbbabf2ff2917e9abc5784c1227b8c80e463c7dc Mon Sep 17 00:00:00 2001 From: KidDev88 Date: Fri, 23 Oct 2026 21:37:10 +0100 Subject: [PATCH 3/4] refactor: consolidate duplicate vitest configs and test directories Remove vitest.config.mts in favour of the more complete vitest.config.ts, merge the unique cleanup/useParams setup from src/test/setup.ts into src/tests/setup.tsx, and drop the src/test/ directory. Remove the duplicate format.test.ts (keeping the more comprehensive src/lib/utils/__tests__/format.test.ts) and the 15 co-located test files that duplicated the centralized copies under src/tests/. Closes #533 --- src/components/course/course-card.test.tsx | 39 ---------- src/components/course/progress-bar.test.tsx | 26 ------- .../credentials/credential-card.test.tsx | 38 ---------- src/components/layout/header.test.tsx | 61 ---------------- src/components/layout/mobile-nav.test.tsx | 31 -------- src/components/layout/sidebar.test.tsx | 31 -------- src/components/rewards/claim-button.test.tsx | 66 ----------------- .../rewards/reward-history.test.tsx | 73 ------------------- src/components/rewards/token-balance.test.tsx | 25 ------- src/components/shared/error-boundary.test.tsx | 46 ------------ src/components/shared/toast.test.tsx | 36 --------- src/components/ui/button.test.tsx | 37 ---------- src/components/ui/card.test.tsx | 36 --------- src/components/ui/input.test.tsx | 22 ------ .../wallet/wallet-provider.test.tsx | 35 --------- src/test/setup.ts | 32 -------- src/tests/setup.tsx | 11 ++- src/tests/shared/format.test.ts | 57 --------------- vitest.config.mts | 18 ----- 19 files changed, 9 insertions(+), 711 deletions(-) delete mode 100644 src/components/course/course-card.test.tsx delete mode 100644 src/components/course/progress-bar.test.tsx delete mode 100644 src/components/credentials/credential-card.test.tsx delete mode 100644 src/components/layout/header.test.tsx delete mode 100644 src/components/layout/mobile-nav.test.tsx delete mode 100644 src/components/layout/sidebar.test.tsx delete mode 100644 src/components/rewards/claim-button.test.tsx delete mode 100644 src/components/rewards/reward-history.test.tsx delete mode 100644 src/components/rewards/token-balance.test.tsx delete mode 100644 src/components/shared/error-boundary.test.tsx delete mode 100644 src/components/shared/toast.test.tsx delete mode 100644 src/components/ui/button.test.tsx delete mode 100644 src/components/ui/card.test.tsx delete mode 100644 src/components/ui/input.test.tsx delete mode 100644 src/components/wallet/wallet-provider.test.tsx delete mode 100644 src/test/setup.ts delete mode 100644 src/tests/shared/format.test.ts delete mode 100644 vitest.config.mts diff --git a/src/components/course/course-card.test.tsx b/src/components/course/course-card.test.tsx deleted file mode 100644 index 63b118b..0000000 --- a/src/components/course/course-card.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { CourseCard } from "@/components/course/course-card"; -import type { Course } from "@/types/course"; - -const mockCourse: Course = { - id: "c1", - title: "Stellar Basics", - description: "Learn the Stellar network fundamentals.", - difficulty: "beginner", - category: "Blockchain", - estimatedHours: 2, - modules: [], - totalModules: 4, - enrolledCount: 120, - rewardTokenAmount: 50, - createdAt: "2024-01-01T00:00:00Z", -}; - -describe("CourseCard", () => { - it("renders course title and difficulty", () => { - render(); - expect(screen.getByText("Stellar Basics")).toBeInTheDocument(); - expect(screen.getByText(/beginner/i)).toBeInTheDocument(); - expect(screen.getByText("Blockchain")).toBeInTheDocument(); - expect(screen.getByText("+50 LEARN")).toBeInTheDocument(); - }); - - it("links to the course detail page", () => { - render(); - expect(screen.getByRole("link")).toHaveAttribute("href", "/courses/c1"); - }); - - it("shows progress when enrolled", () => { - render(); - expect(screen.getByText("Progress")).toBeInTheDocument(); - expect(screen.getByText("40%")).toBeInTheDocument(); - }); -}); diff --git a/src/components/course/progress-bar.test.tsx b/src/components/course/progress-bar.test.tsx deleted file mode 100644 index 8ba5364..0000000 --- a/src/components/course/progress-bar.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { ProgressBar } from "@/components/course/progress-bar"; - -describe("ProgressBar", () => { - it("renders progress value and label", () => { - render(); - expect(screen.getByText("65%")).toBeInTheDocument(); - expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "65"); - }); - - it("clamps values outside 0-100", () => { - render(); - expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "100"); - }); - - it("hides label when showLabel is false", () => { - render(); - expect(screen.queryByText("Progress")).not.toBeInTheDocument(); - }); - - it("applies size classes", () => { - const { container } = render(); - expect(container.querySelector('[role="progressbar"]')).toHaveClass("h-3"); - }); -}); diff --git a/src/components/credentials/credential-card.test.tsx b/src/components/credentials/credential-card.test.tsx deleted file mode 100644 index 2f466f8..0000000 --- a/src/components/credentials/credential-card.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { CredentialCard } from "@/components/credentials/credential-card"; -import type { CredentialNFT } from "@/types/stellar"; - -const mockCredential: CredentialNFT = { - id: "cred-1", - tokenId: "1", - contractAddress: "CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - courseId: "c1", - courseTitle: "Soroban Development", - issuedAt: "2024-06-15T00:00:00Z", - metadata: { - learnerAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - courseTitle: "Soroban Development", - completionDate: "2024-06-15T00:00:00Z", - skills: ["Rust", "Soroban", "Stellar"], - issuerAddress: "GYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", - verificationUrl: "https://example.com/verify/cred-1", - }, -}; - -describe("CredentialCard", () => { - it("renders credential title and skills", () => { - render(); - expect(screen.getByText("Soroban Development")).toBeInTheDocument(); - expect(screen.getByText("Rust")).toBeInTheDocument(); - expect(screen.getByText("Soroban")).toBeInTheDocument(); - }); - - it("links to credential detail page", () => { - render(); - expect(screen.getByRole("link")).toHaveAttribute( - "href", - "/credentials/cred-1" - ); - }); -}); diff --git a/src/components/layout/header.test.tsx b/src/components/layout/header.test.tsx deleted file mode 100644 index e59b040..0000000 --- a/src/components/layout/header.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { beforeEach, describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Header } from "@/components/layout/header"; -import { useAuthStore } from "@/store/auth-store"; - -vi.mock("@/lib/hooks/use-auth", () => ({ - useAuth: () => ({ - isAuthenticated: useAuthStore.getState().isAuthenticated, - walletAddress: useAuthStore.getState().walletAddress, - isConnecting: false, - connectWallet: vi.fn(), - disconnect: vi.fn(), - }), -})); - -describe("Header", () => { - beforeEach(() => { - useAuthStore.setState({ - isAuthenticated: false, - walletAddress: null, - jwt: null, - }); - }); - - it("renders brand name", () => { - render(
); - expect(screen.getByText("ChainLearn")).toBeInTheDocument(); - }); - - it("hides nav links when unauthenticated", () => { - render(
); - expect(screen.queryByText("Dashboard")).not.toBeInTheDocument(); - }); - - it("shows nav links when authenticated", () => { - useAuthStore.setState({ - isAuthenticated: true, - walletAddress: "GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLM", - jwt: "token", - }); - render(
); - expect(screen.getByText("Dashboard")).toBeInTheDocument(); - expect(screen.getByText("Courses")).toBeInTheDocument(); - }); - - it("toggles mobile navigation", async () => { - const user = userEvent.setup(); - useAuthStore.setState({ - isAuthenticated: true, - walletAddress: "GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLM", - jwt: "token", - }); - render(
); - - const toggle = screen.getByLabelText(/Toggle navigation/i); - expect(toggle).toHaveAttribute("aria-expanded", "false"); - await user.click(toggle); - expect(toggle).toHaveAttribute("aria-expanded", "true"); - }); -}); diff --git a/src/components/layout/mobile-nav.test.tsx b/src/components/layout/mobile-nav.test.tsx deleted file mode 100644 index 8931ea9..0000000 --- a/src/components/layout/mobile-nav.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { vi } from "vitest"; - -vi.mock("next/navigation", () => ({ - usePathname: () => "/dashboard", - useRouter: () => ({ push: vi.fn() }), - useSearchParams: () => new URLSearchParams(), -})); -import { useAuthStore } from "@/store/auth-store"; -import { MobileNav } from "@/components/layout/mobile-nav"; - -describe("Nav", () => { - beforeEach(() => { - useAuthStore.setState({ isAuthenticated: true, hasHydrated: true }); - }); - - it("renders bottom navigation links", () => { - render(); - expect(screen.getByText("Dashboard")).toBeInTheDocument(); - expect(screen.getByText("Courses")).toBeInTheDocument(); - expect(screen.getByText("Rewards")).toBeInTheDocument(); - expect(screen.getByText("Credentials")).toBeInTheDocument(); - }); - - it("highlights the active link", () => { - render(); - const dashboard = screen.getByText("Dashboard").closest("a"); - expect(dashboard).toHaveClass("text-primary-600"); - }); -}); diff --git a/src/components/layout/sidebar.test.tsx b/src/components/layout/sidebar.test.tsx deleted file mode 100644 index 638b769..0000000 --- a/src/components/layout/sidebar.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { vi } from "vitest"; - -vi.mock("next/navigation", () => ({ - usePathname: () => "/dashboard", - useRouter: () => ({ push: vi.fn() }), - useSearchParams: () => new URLSearchParams(), -})); -import { useAuthStore } from "@/store/auth-store"; -import { Sidebar } from "@/components/layout/sidebar"; - -describe("Nav", () => { - beforeEach(() => { - useAuthStore.setState({ isAuthenticated: true, hasHydrated: true }); - }); - - it("renders all navigation links", () => { - render(); - expect(screen.getByText("Dashboard")).toBeInTheDocument(); - expect(screen.getByText("Courses")).toBeInTheDocument(); - expect(screen.getByText("Rewards")).toBeInTheDocument(); - expect(screen.getByText("Credentials")).toBeInTheDocument(); - }); - - it("marks the active route", () => { - render(); - const dashboard = screen.getByText("Dashboard").closest("a"); - expect(dashboard).toHaveClass("bg-primary-100"); - }); -}); diff --git a/src/components/rewards/claim-button.test.tsx b/src/components/rewards/claim-button.test.tsx deleted file mode 100644 index a8e2df1..0000000 --- a/src/components/rewards/claim-button.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { ClaimButton } from "@/components/rewards/claim-button"; - -describe("ClaimButton", () => { - it("renders claim amount", () => { - render( - - ); - expect(screen.getByRole("button", { name: /Claim 25 LEARN/i })).toBeInTheDocument(); - }); - - it("calls onClaim and shows claimed state", async () => { - const user = userEvent.setup(); - const onClaim = vi.fn().mockResolvedValue(undefined); - - render( - - ); - - await user.click(screen.getByRole("button", { name: /Claim 25 LEARN/i })); - - await waitFor(() => { - expect(onClaim).toHaveBeenCalledWith("claim-1"); - expect(screen.getByText("Claimed!")).toBeInTheDocument(); - }); - }); - - it("shows loading state while claiming", async () => { - const user = userEvent.setup(); - let resolveClaim!: () => void; - const onClaim = vi.fn( - () => - new Promise((resolve) => { - resolveClaim = resolve; - }) - ); - - render( - - ); - - await user.click(screen.getByRole("button")); - expect(screen.getByText("Claiming...")).toBeInTheDocument(); - resolveClaim(); - await waitFor(() => { - expect(screen.getByText("Claimed!")).toBeInTheDocument(); - }); - }); -}); diff --git a/src/components/rewards/reward-history.test.tsx b/src/components/rewards/reward-history.test.tsx deleted file mode 100644 index 0d88191..0000000 --- a/src/components/rewards/reward-history.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { beforeEach, describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { RewardHistory } from "@/components/rewards/reward-history"; -import { useAuthStore } from "@/store/auth-store"; -import type { RewardClaim } from "@/types/stellar"; - -const claims: RewardClaim[] = [ - { - id: "r1", - txHash: "abc123", - amount: "50000000", - tokenCode: "LEARN", - claimedAt: "2024-03-01T00:00:00Z", - status: "confirmed", - courseTitle: "Intro to Stellar", - }, - { - id: "r2", - txHash: "", - amount: "25000000", - tokenCode: "LEARN", - claimedAt: "2024-03-02T00:00:00Z", - status: "pending", - courseTitle: "Quiz Bonus", - }, -]; - -describe("RewardHistory", () => { - beforeEach(() => { - useAuthStore.setState({ network: "testnet" }); - }); - - it("renders empty state when no claims", () => { - render(); - expect(screen.getByText("No reward history yet.")).toBeInTheDocument(); - }); - - it("renders claim rows with amounts and titles", () => { - render(); - expect(screen.getByText("Intro to Stellar")).toBeInTheDocument(); - expect(screen.getByText("Quiz Bonus")).toBeInTheDocument(); - expect(screen.getByText(/\+5 LEARN/)).toBeInTheDocument(); - }); - - it("links confirmed claims to stellar.expert", () => { - render(); - const link = screen.getByRole("link"); - expect(link).toHaveAttribute( - "href", - "https://stellar.expert/explorer/testnet/tx/abc123" - ); - }); - - it("shows summary stats for total earned, confirmed, pending and failed", () => { - const mixed = [ - ...claims, - { - id: "r3", - txHash: "", - amount: "10000000", - tokenCode: "LEARN", - claimedAt: "2024-03-03T00:00:00Z", - status: "failed", - courseTitle: "Failed Quiz", - }, - ] as RewardClaim[]; - render(); - expect(screen.getByText("Total earned")).toBeInTheDocument(); - expect(screen.getByText("Confirmed")).toBeInTheDocument(); - expect(screen.getByText("Pending")).toBeInTheDocument(); - expect(screen.getByText("Failed")).toBeInTheDocument(); - }); -}); diff --git a/src/components/rewards/token-balance.test.tsx b/src/components/rewards/token-balance.test.tsx deleted file mode 100644 index 40a8ac7..0000000 --- a/src/components/rewards/token-balance.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { TokenBalance } from "@/components/rewards/token-balance"; -import type { TokenBalance as TokenBalanceType } from "@/types/stellar"; - -const mockBalance: TokenBalanceType = { - tokenCode: "LEARN", - balance: "100000000", // 10 LEARN with 7 decimals - decimals: 7, -}; - -describe("TokenBalance", () => { - it("renders token code and formatted balance", () => { - render(); - expect(screen.getByText("LEARN")).toBeInTheDocument(); - expect(screen.getByText("10")).toBeInTheDocument(); - }); - - it("applies custom className", () => { - const { container } = render( - - ); - expect(container.firstChild).toHaveClass("mt-4"); - }); -}); diff --git a/src/components/shared/error-boundary.test.tsx b/src/components/shared/error-boundary.test.tsx deleted file mode 100644 index 619bedc..0000000 --- a/src/components/shared/error-boundary.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { ErrorBoundary } from "@/components/shared/error-boundary"; - -function Boom(): never { - throw new Error("Boom exploded"); -} - -describe("ErrorBoundary", () => { - it("renders children when there is no error", () => { - render( - -

All good

-
- ); - expect(screen.getByText("All good")).toBeInTheDocument(); - }); - - it("shows fallback UI with error message and retry button", () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - render( - - - - ); - - expect(screen.getByText("Something went wrong")).toBeInTheDocument(); - expect(screen.getByText("Boom exploded")).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: /Try Again/i }) - ).toBeInTheDocument(); - consoleSpy.mockRestore(); - }); - - it("renders custom fallback when provided", () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - render( - Custom fallback

}> - -
- ); - expect(screen.getByText("Custom fallback")).toBeInTheDocument(); - consoleSpy.mockRestore(); - }); -}); diff --git a/src/components/shared/toast.test.tsx b/src/components/shared/toast.test.tsx deleted file mode 100644 index 382c76a..0000000 --- a/src/components/shared/toast.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Toast } from "@/components/shared/toast"; - -describe("Toast", () => { - it("renders message with default info variant", () => { - render(); - expect(screen.getByText("Hello toast")).toBeInTheDocument(); - }); - - it("applies success and error variants", () => { - const { rerender, container } = render( - - ); - expect(container.firstChild).toHaveClass("bg-green-50"); - - rerender( - - ); - expect(container.firstChild).toHaveClass("bg-red-50"); - }); - - it("calls onClose when dismiss button is clicked", async () => { - vi.useFakeTimers({ shouldAdvanceTime: true }); - const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); - const onClose = vi.fn(); - - render(); - await user.click(screen.getByRole("button")); - - vi.advanceTimersByTime(300); - expect(onClose).toHaveBeenCalled(); - vi.useRealTimers(); - }); -}); diff --git a/src/components/ui/button.test.tsx b/src/components/ui/button.test.tsx deleted file mode 100644 index 62ac074..0000000 --- a/src/components/ui/button.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Button } from "@/components/ui/button"; - -describe("Button", () => { - it("renders children", () => { - render(); - expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument(); - }); - - it("applies variant classes", () => { - const { rerender } = render(); - expect(screen.getByRole("button")).toHaveClass("bg-red-600"); - - rerender(); - expect(screen.getByRole("button")).toHaveClass("border"); - }); - - it("applies size classes", () => { - render(); - expect(screen.getByRole("button")).toHaveClass("h-12"); - }); - - it("calls onClick when clicked", async () => { - const user = userEvent.setup(); - const onClick = vi.fn(); - render(); - await user.click(screen.getByRole("button")); - expect(onClick).toHaveBeenCalledOnce(); - }); - - it("is disabled when disabled prop is set", () => { - render(); - expect(screen.getByRole("button")).toBeDisabled(); - }); -}); diff --git a/src/components/ui/card.test.tsx b/src/components/ui/card.test.tsx deleted file mode 100644 index 616a830..0000000 --- a/src/components/ui/card.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardFooter, -} from "@/components/ui/card"; - -describe("Card", () => { - it("renders card with composed sections", () => { - render( - - - Title - Description - - Body content - Footer - - ); - - expect(screen.getByText("Title")).toBeInTheDocument(); - expect(screen.getByText("Description")).toBeInTheDocument(); - expect(screen.getByText("Body content")).toBeInTheDocument(); - expect(screen.getByText("Footer")).toBeInTheDocument(); - }); - - it("merges custom className", () => { - const { container } = render(Hi); - expect(container.firstChild).toHaveClass("custom-card"); - expect(container.firstChild).toHaveClass("rounded-xl"); - }); -}); diff --git a/src/components/ui/input.test.tsx b/src/components/ui/input.test.tsx deleted file mode 100644 index 2f6d649..0000000 --- a/src/components/ui/input.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { Input } from "@/components/ui/input"; - -describe("Input", () => { - it("renders an input element", () => { - render(); - expect(screen.getByPlaceholderText("Email")).toBeInTheDocument(); - }); - - it("forwards type and disabled props", () => { - render(); - const input = screen.getByLabelText("Password"); - expect(input).toHaveAttribute("type", "password"); - expect(input).toBeDisabled(); - }); - - it("applies custom className", () => { - render(); - expect(screen.getByLabelText("Search")).toHaveClass("w-64"); - }); -}); diff --git a/src/components/wallet/wallet-provider.test.tsx b/src/components/wallet/wallet-provider.test.tsx deleted file mode 100644 index fb7c10f..0000000 --- a/src/components/wallet/wallet-provider.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { - WalletProvider, - useWalletContext, -} from "@/components/wallet/wallet-provider"; -import { useAuthStore } from "@/store/auth-store"; - -vi.mock("@/lib/stellar/wallet", () => ({ - getFreighterAddress: vi.fn().mockResolvedValue(null), -})); - -function ReadyProbe() { - const { isReady } = useWalletContext(); - return {isReady ? "ready" : "not-ready"}; -} - -describe("WalletProvider", () => { - beforeEach(() => { - useAuthStore.setState({ - isAuthenticated: false, - walletAddress: null, - jwt: null, - }); - }); - - it("renders children and exposes ready context", () => { - render( - - - - ); - expect(screen.getByText("ready")).toBeInTheDocument(); - }); -}); diff --git a/src/test/setup.ts b/src/test/setup.ts deleted file mode 100644 index 3e034ae..0000000 --- a/src/test/setup.ts +++ /dev/null @@ -1,32 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import React from "react"; -import { cleanup } from "@testing-library/react"; -import { afterEach, vi } from "vitest"; - -afterEach(() => { - cleanup(); -}); - -vi.mock("next/navigation", () => ({ - usePathname: () => "/dashboard", - useRouter: () => ({ - push: vi.fn(), - replace: vi.fn(), - back: vi.fn(), - prefetch: vi.fn(), - }), - useSearchParams: () => new URLSearchParams(), - useParams: () => ({}), -})); - -vi.mock("next/link", () => ({ - default: ({ - children, - href, - ...props - }: { - children: React.ReactNode; - href: string; - [key: string]: unknown; - }) => React.createElement("a", { href, ...props }, children), -})); diff --git a/src/tests/setup.tsx b/src/tests/setup.tsx index d8c8d50..e87fd65 100644 --- a/src/tests/setup.tsx +++ b/src/tests/setup.tsx @@ -1,4 +1,10 @@ -import "@testing-library/jest-dom"; +import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterAll, afterEach, beforeAll, vi } from "vitest"; + +afterEach(() => { + cleanup(); +}); // Radix UI (Select) relies on APIs jsdom doesn't implement. if (!window.HTMLElement.prototype.hasPointerCapture) { @@ -39,9 +45,10 @@ if (!("ResizeObserver" in window)) { // Suppress Next.js router / Link warnings in jsdom vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }), + useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), prefetch: vi.fn() }), usePathname: () => "/", useSearchParams: () => new URLSearchParams(), + useParams: () => ({}), })); vi.mock("next/link", () => ({ diff --git a/src/tests/shared/format.test.ts b/src/tests/shared/format.test.ts deleted file mode 100644 index 303312d..0000000 --- a/src/tests/shared/format.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - formatAddress, - formatDate, - formatDateTime, - formatDuration, - formatLargeNumber, - formatPercentage, - formatRelativeTime, - formatTokenAmount, -} from "@/lib/utils/format"; - -afterEach(() => { - vi.useRealTimers(); -}); - -describe("number formatting utilities", () => { - it("formats token amounts, percentages, compact counts, and addresses", () => { - expect(formatTokenAmount(1234.5)).toBe("1,234.5 LEARN"); - expect(formatPercentage(0.75)).toBe("75%"); - expect(formatPercentage(75, { isPercentValue: true })).toBe("75%"); - expect(formatLargeNumber(1200)).toBe("1.2K"); - expect(formatAddress("G".padEnd(56, "A"))).toBe("GAAA...AAAA"); - }); - - it("returns an empty string for non-finite values", () => { - expect(formatTokenAmount(Number.NaN)).toBe(""); - expect(formatPercentage(Number.POSITIVE_INFINITY)).toBe(""); - expect(formatLargeNumber(Number.NaN)).toBe(""); - }); -}); - -describe("date formatting utilities", () => { - it("formats dates and date-times in the requested timezone", () => { - const timestamp = "2024-01-15T00:30:00.000Z"; - expect(formatDate(timestamp, { timeZone: "UTC" })).toBe("Jan 15, 2024"); - expect(formatDate(timestamp, { timeZone: "America/Los_Angeles" })).toBe("Jan 14, 2024"); - expect(formatDateTime(timestamp, { timeZone: "UTC" })).toBe("Jan 15, 2024, 12:30 AM"); - expect(formatDate("not a date")).toBe(""); - }); - - it("formats relative times and falls back to a date for older timestamps", () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2024-01-15T12:00:00.000Z")); - - expect(formatRelativeTime(new Date("2024-01-15T10:00:00.000Z"))).toBe("2 hours ago"); - expect(formatRelativeTime(new Date("2024-01-05T12:00:00.000Z"), { timeZone: "UTC" })).toBe("Jan 5, 2024"); - expect(formatRelativeTime("invalid")).toBe(""); - }); - - it("formats minute durations as days, hours, and minutes", () => { - expect(formatDuration(3065)).toBe("2d 3h 5m"); - expect(formatDuration(60)).toBe("1h"); - expect(formatDuration(0)).toBe("0m"); - expect(formatDuration(-1)).toBe("0m"); - }); -}); \ No newline at end of file diff --git a/vitest.config.mts b/vitest.config.mts deleted file mode 100644 index aeea7e7..0000000 --- a/vitest.config.mts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "vitest/config"; -import react from "@vitejs/plugin-react"; -import path from "path"; - -export default defineConfig({ - plugins: [react()], - test: { - environment: "jsdom", - globals: true, - setupFiles: ["./src/test/setup.ts"], - css: false, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -}); From 18ca7b54520aa5f5100679c556c5feec34ce65ce Mon Sep 17 00:00:00 2001 From: KidDev88 Date: Fri, 23 Oct 2026 21:37:29 +0100 Subject: [PATCH 4/4] test: cover wallet helpers, challenge signing, toScVal, and simulation Add unit tests for the Stellar modules covering the gaps left by the existing stellar-*.test.ts suites: network helper functions (getNetworkPassphrase, getRpcUrl, getHorizonUrl, normalizeNetwork), SEP-10 challenge signing success and error paths, toScVal argument conversion through buildContractInvokeTx, and simulateContractCall success/RPC-error handling. All external dependencies (StellarWalletsKit, Soroban RPC, fetch) are mocked. Closes #535 --- src/tests/shared/stellar-core-gaps.test.ts | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 src/tests/shared/stellar-core-gaps.test.ts diff --git a/src/tests/shared/stellar-core-gaps.test.ts b/src/tests/shared/stellar-core-gaps.test.ts new file mode 100644 index 0000000..222539e --- /dev/null +++ b/src/tests/shared/stellar-core-gaps.test.ts @@ -0,0 +1,242 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + getAccount: vi.fn(), + prepareTransaction: vi.fn(), + signMessage: vi.fn(), + setWallet: vi.fn(), + storage: new Map(), +})); + +vi.mock("@creit.tech/stellar-wallets-kit/stellar-wallets-kit", () => ({ + StellarWalletsKit: class { + getSupportedWallets = vi.fn(); + setWallet = mocks.setWallet; + getAddress = vi.fn().mockResolvedValue({ address: "GADDRESS" }); + signMessage = mocks.signMessage; + signTransaction = vi.fn().mockResolvedValue({ signedTxXdr: "signed-xdr" }); + openModal = vi.fn(); + }, +})); + +vi.mock("@creit.tech/stellar-wallets-kit/types", () => ({ + WalletNetwork: { PUBLIC: "PUBLIC", TESTNET: "TESTNET" }, +})); + +vi.mock("@creit.tech/stellar-wallets-kit/modules/freighter.module", () => ({ + FreighterModule: class {}, +})); +vi.mock("@creit.tech/stellar-wallets-kit/modules/lobstr.module", () => ({ + LobstrModule: class {}, +})); +vi.mock("@creit.tech/stellar-wallets-kit/modules/rabet.module", () => ({ + RabetModule: class {}, +})); + +vi.mock("@stellar/stellar-sdk", async (importOriginal) => { + const sdk = await importOriginal(); + return { + ...sdk, + rpc: { + ...sdk.rpc, + Server: class { + getAccount = mocks.getAccount; + prepareTransaction = mocks.prepareTransaction; + sendTransaction = vi.fn(); + }, + }, + Horizon: { + ...sdk.Horizon, + Server: class { + loadAccount = vi.fn(); + }, + }, + }; +}); + +describe("wallet network helpers (#535)", () => { + beforeEach(() => { + vi.resetModules(); + mocks.storage.clear(); + Object.defineProperty(window, "localStorage", { + configurable: true, + value: { + getItem: (key: string) => mocks.storage.get(key) ?? null, + setItem: (key: string, value: string) => mocks.storage.set(key, value), + removeItem: (key: string) => mocks.storage.delete(key), + clear: () => mocks.storage.clear(), + }, + }); + }); + + it("returns the correct network passphrases", async () => { + const { getNetworkPassphrase } = await import("@/lib/stellar/wallet"); + expect(getNetworkPassphrase("public")).toBe( + "Public Global Stellar Network ; September 2015" + ); + expect(getNetworkPassphrase("testnet")).toBe( + "Test SDF Network ; September 2015" + ); + }); + + it("returns the correct Soroban RPC URLs", async () => { + const { getRpcUrl } = await import("@/lib/stellar/wallet"); + expect(getRpcUrl("public")).toContain("soroban-rpc.mainnet"); + expect(getRpcUrl("testnet")).toContain("soroban-rpc.testnet"); + }); + + it("returns the correct Horizon URLs", async () => { + const { getHorizonUrl } = await import("@/lib/stellar/wallet"); + expect(getHorizonUrl("public")).toBe("https://horizon.stellar.org"); + expect(getHorizonUrl("testnet")).toBe("https://horizon-testnet.stellar.org"); + }); + + it("normalizes Freighter network names to NetworkType", async () => { + const { normalizeNetwork } = await import("@/lib/stellar/wallet"); + expect(normalizeNetwork("PUBLIC")).toBe("public"); + expect(normalizeNetwork("public")).toBe("public"); + expect(normalizeNetwork("TESTNET")).toBe("testnet"); + expect(normalizeNetwork("unknown")).toBe("testnet"); + }); + + it("signs a SEP-10 challenge through the selected provider", async () => { + const { signChallenge } = await import("@/lib/stellar/wallet"); + mocks.signMessage.mockResolvedValue({ signedMessage: "signed-challenge" }); + + const result = await signChallenge( + "challenge-blob", + "Test SDF Network ; September 2015", + "freighter" + ); + expect(result).toBe("signed-challenge"); + expect(mocks.setWallet).toHaveBeenCalledWith("freighter"); + expect(mocks.signMessage).toHaveBeenCalledWith("challenge-blob", { + networkPassphrase: "Test SDF Network ; September 2015", + }); + }); + + it("throws when the wallet returns an empty signature", async () => { + const { signChallenge } = await import("@/lib/stellar/wallet"); + mocks.signMessage.mockResolvedValue({ signedMessage: "" }); + + await expect( + signChallenge("challenge-blob", "Test SDF Network ; September 2015", "freighter") + ).rejects.toThrow(/empty signature/i); + }); + + it("wraps LOBSTR message-signing limitations with a clear error", async () => { + const { signChallenge } = await import("@/lib/stellar/wallet"); + mocks.signMessage.mockRejectedValue( + new Error("LOBSTR does not support signMessage") + ); + + await expect( + signChallenge("challenge-blob", "Test SDF Network ; September 2015", "lobstr") + ).rejects.toThrow(/message signing/i); + }); +}); + +describe("toScVal conversion via buildContractInvokeTx (#535)", () => { + beforeEach(() => { + vi.resetModules(); + mocks.getAccount.mockResolvedValue({ accountId: () => "GSOURCE", sequenceNumber: () => "1" }); + mocks.prepareTransaction.mockImplementation((tx: { toXDR: () => string }) => ({ + toXDR: () => tx.toXDR(), + })); + Object.defineProperty(window, "localStorage", { + configurable: true, + value: { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + clear: () => {}, + }, + }); + }); + + it("converts string, integer, bigint, and boolean args without throwing", async () => { + const { buildContractInvokeTx } = await import("@/lib/stellar/transactions"); + + const xdr = await buildContractInvokeTx({ + sourceAddress: "GSOURCE", + contractAddress: "CCONTRACT", + method: "mint", + args: ["alice", 42, 2n ** 40n, true], + network: "testnet", + }); + expect(typeof xdr).toBe("string"); + expect(xdr.length).toBeGreaterThan(0); + }); + + it("rejects non-integer number arguments", async () => { + const { buildContractInvokeTx } = await import("@/lib/stellar/transactions"); + + await expect( + buildContractInvokeTx({ + sourceAddress: "GSOURCE", + contractAddress: "CCONTRACT", + method: "mint", + args: [3.14], + network: "testnet", + }) + ).rejects.toThrow(/integers/i); + }); + + it("rejects a blank contract method name", async () => { + const { buildContractInvokeTx } = await import("@/lib/stellar/transactions"); + + await expect( + buildContractInvokeTx({ + sourceAddress: "GSOURCE", + contractAddress: "CCONTRACT", + method: " ", + network: "testnet", + }) + ).rejects.toThrow(/method is required/i); + }); +}); + +describe("simulateContractCall error paths (#535)", () => { + beforeEach(() => { + vi.resetModules(); + Object.defineProperty(window, "localStorage", { + configurable: true, + value: { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + clear: () => {}, + }, + }); + }); + + it("returns the RPC simulation result on success", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ result: { results: [{ xdr: "AAAA" }] } }), + }) + ); + const { simulateContractCall } = await import("@/lib/stellar/transactions"); + + await expect( + simulateContractCall("CCONTRACT", "balance", ["GOWNER"], "testnet") + ).resolves.toEqual({ results: [{ xdr: "AAAA" }] }); + vi.unstubAllGlobals(); + }); + + it("throws the RPC error message when the simulation fails", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + json: async () => ({ error: { message: "contract not found" } }), + }) + ); + const { simulateContractCall } = await import("@/lib/stellar/transactions"); + + await expect( + simulateContractCall("CCONTRACT", "balance", ["GOWNER"], "testnet") + ).rejects.toThrow("contract not found"); + vi.unstubAllGlobals(); + }); +});