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();
+ });
+});