From 5b1f0655fc28563abebf7aa78832478766d11b7e Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Thu, 2 Jul 2026 10:19:39 +0100 Subject: [PATCH] feat(wallet): implement useNetwork normalization and test suite --- apps/web/register-dom.ts | 2 + apps/web/setup-tests.ts | 8 +- .../wallet/components/AccountBadge.tsx | 4 +- .../wallet/components/ConnectButton.test.tsx | 6 +- .../components/NetworkMismatchBanner.tsx | 4 +- .../features/wallet/hooks/useNetwork.test.ts | 132 ++++++++++++++++++ .../src/features/wallet/hooks/useNetwork.ts | 47 ++++++- .../src/features/wallet/store/wallet-store.ts | 4 +- 8 files changed, 189 insertions(+), 18 deletions(-) create mode 100644 apps/web/register-dom.ts create mode 100644 apps/web/src/features/wallet/hooks/useNetwork.test.ts diff --git a/apps/web/register-dom.ts b/apps/web/register-dom.ts new file mode 100644 index 0000000..a2fb3a1 --- /dev/null +++ b/apps/web/register-dom.ts @@ -0,0 +1,2 @@ +import { GlobalRegistrator } from '@happy-dom/global-registrator'; +GlobalRegistrator.register(); diff --git a/apps/web/setup-tests.ts b/apps/web/setup-tests.ts index 96a8606..a77137d 100644 --- a/apps/web/setup-tests.ts +++ b/apps/web/setup-tests.ts @@ -1,12 +1,10 @@ +import "./register-dom" import "@testing-library/jest-dom/vitest" -import { GlobalRegistrator } from '@happy-dom/global-registrator'; import { afterAll, afterEach, beforeAll } from "vitest"; import { server } from "./test/msw/server"; +import { cleanup } from "@testing-library/react"; -GlobalRegistrator.register(); - -afterEach(async () => { - const { cleanup } = await import("@testing-library/react"); +afterEach(() => { cleanup(); }); diff --git a/apps/web/src/features/wallet/components/AccountBadge.tsx b/apps/web/src/features/wallet/components/AccountBadge.tsx index e012229..cb160ad 100644 --- a/apps/web/src/features/wallet/components/AccountBadge.tsx +++ b/apps/web/src/features/wallet/components/AccountBadge.tsx @@ -22,7 +22,7 @@ export function AccountBadge({ address, className, ...props }: AccountBadgeProps const balanceData = useBalance() const balance = balanceData?.xlm const isLoading = balanceData?.isLoading ?? false - const { isMainnet } = useNetwork() + const { displayLabel } = useNetwork() useEffect(() => { if (!open) return @@ -103,7 +103,7 @@ export function AccountBadge({ address, className, ...props }: AccountBadgeProps
Network - {isMainnet ? "Mainnet" : "Testnet"} + {displayLabel}
diff --git a/apps/web/src/features/wallet/components/ConnectButton.test.tsx b/apps/web/src/features/wallet/components/ConnectButton.test.tsx index 8e3c174..af98b58 100644 --- a/apps/web/src/features/wallet/components/ConnectButton.test.tsx +++ b/apps/web/src/features/wallet/components/ConnectButton.test.tsx @@ -109,7 +109,7 @@ describe("ConnectButton - Disconnected State", () => { const dialog = screen.getByRole("dialog") expect(dialog).toBeInTheDocument() - const dialogTitle = screen.getByText("Connect Wallet") + const dialogTitle = screen.getByRole("heading", { name: "Connect Wallet" }) expect(dialogTitle).toBeInTheDocument() }) @@ -259,7 +259,7 @@ describe("ConnectButton - Disconnected State", () => { render() // Account badge should be rendered instead - expect(screen.getByText(/GAAAAAA/)).toBeInTheDocument() + expect(screen.getByText(/GAAAAA/)).toBeInTheDocument() }) }) @@ -356,7 +356,7 @@ describe("ConnectButton - Disconnected State", () => { expect( screen.queryByRole("button", { name: /Connect wallet/i }), ).not.toBeInTheDocument() - expect(screen.getByText(/GAAAAAA/)).toBeInTheDocument() + expect(screen.getByText(/GAAAAA/)).toBeInTheDocument() }) }) diff --git a/apps/web/src/features/wallet/components/NetworkMismatchBanner.tsx b/apps/web/src/features/wallet/components/NetworkMismatchBanner.tsx index 7ca1c4e..892e6a5 100644 --- a/apps/web/src/features/wallet/components/NetworkMismatchBanner.tsx +++ b/apps/web/src/features/wallet/components/NetworkMismatchBanner.tsx @@ -8,7 +8,7 @@ const SESSION_KEY = "so4-network-mismatch-dismissed" export function NetworkMismatchBanner() { const { pathname } = useLocation() - const { mismatch, network } = useNetwork() + const { mismatch, displayLabel } = useNetwork() const { status } = useWalletStore() const [dismissed, setDismissed] = useState( () => sessionStorage.getItem(SESSION_KEY) === "1" @@ -18,7 +18,7 @@ export function NetworkMismatchBanner() { if (pathname === "/") return null if (!mismatch || status !== "connected" || dismissed) return null - const walletLabel = network === "mainnet" ? "Mainnet" : "Testnet" + const walletLabel = displayLabel const appLabel = NETWORK.name === "mainnet" ? "Mainnet" : "Testnet" function dismiss() { diff --git a/apps/web/src/features/wallet/hooks/useNetwork.test.ts b/apps/web/src/features/wallet/hooks/useNetwork.test.ts new file mode 100644 index 0000000..00bfbb8 --- /dev/null +++ b/apps/web/src/features/wallet/hooks/useNetwork.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it, beforeEach } from "vitest" +import { renderHook } from "@testing-library/react" +import { useNetwork, normalizeNetwork } from "./useNetwork" +import { useWalletStore } from "../store/wallet-store" +import { NETWORK } from "@/app/config/network" + +describe("normalizeNetwork", () => { + it("should normalize Stellar Testnet strings", () => { + expect(normalizeNetwork("testnet")).toBe("testnet") + expect(normalizeNetwork("TESTNET")).toBe("testnet") + expect(normalizeNetwork("Test SDF Network ; September 2015")).toBe("testnet") + expect(normalizeNetwork(" testnet ")).toBe("testnet") + }) + + it("should normalize Stellar Mainnet/Public strings", () => { + expect(normalizeNetwork("public")).toBe("mainnet") + expect(normalizeNetwork("mainnet")).toBe("mainnet") + expect(normalizeNetwork("PUBLIC")).toBe("mainnet") + expect(normalizeNetwork("Public Global Stellar Network ; September 2015")).toBe("mainnet") + expect(normalizeNetwork(" public ")).toBe("mainnet") + }) + + it("should return unknown for other strings", () => { + expect(normalizeNetwork("unknown")).toBe("unknown") + expect(normalizeNetwork("custom")).toBe("unknown") + expect(normalizeNetwork("arbitrary")).toBe("unknown") + }) + + it("should return unknown for empty or missing inputs", () => { + expect(normalizeNetwork(null)).toBe("unknown") + expect(normalizeNetwork(undefined)).toBe("unknown") + expect(normalizeNetwork("")).toBe("unknown") + expect(normalizeNetwork(" ")).toBe("unknown") + }) +}) + +describe("useNetwork Hook", () => { + beforeEach(() => { + useWalletStore.setState({ + address: null, + walletId: null, + status: "disconnected", + pendingTransactionXdr: null, + network: "testnet", + }) + }) + + describe("when status is disconnected", () => { + it("should always return mismatch as false", () => { + // Set network to a mismatching network but status disconnected + const testNetwork = NETWORK.name === "mainnet" ? "testnet" : "mainnet" + useWalletStore.setState({ status: "disconnected", network: testNetwork }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.mismatch).toBe(false) + }) + }) + + describe("when status is connected", () => { + it("should return mismatch as false if wallet network matches app network", () => { + useWalletStore.setState({ status: "connected", network: NETWORK.name }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.mismatch).toBe(false) + }) + + it("should return mismatch as true if wallet network does not match app network", () => { + const opposingNetwork = NETWORK.name === "mainnet" ? "testnet" : "mainnet" + useWalletStore.setState({ status: "connected", network: opposingNetwork }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.mismatch).toBe(true) + }) + + it("should return mismatch as true for unknown wallet networks", () => { + useWalletStore.setState({ status: "connected", network: "custom-network" }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.mismatch).toBe(true) + }) + }) + + describe("network classification and labels", () => { + it("should expose correct details for testnet value", () => { + useWalletStore.setState({ network: "Test SDF Network ; September 2015" }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.normalizedNetwork).toBe("testnet") + expect(result.current.isTestnet).toBe(true) + expect(result.current.isMainnet).toBe(false) + expect(result.current.displayLabel).toBe("Testnet") + }) + + it("should expose correct details for mainnet/public value", () => { + useWalletStore.setState({ network: "Public Global Stellar Network ; September 2015" }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.normalizedNetwork).toBe("mainnet") + expect(result.current.isTestnet).toBe(false) + expect(result.current.isMainnet).toBe(true) + expect(result.current.displayLabel).toBe("Mainnet") + }) + + it("should expose correct details for unknown value", () => { + useWalletStore.setState({ network: "unknown" }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.normalizedNetwork).toBe("unknown") + expect(result.current.isTestnet).toBe(false) + expect(result.current.isMainnet).toBe(false) + expect(result.current.displayLabel).toBe("Unknown") + }) + + it("should expose correct details for missing/null value", () => { + useWalletStore.setState({ network: null }) + + const { result } = renderHook(() => useNetwork()) + + expect(result.current.normalizedNetwork).toBe("unknown") + expect(result.current.isTestnet).toBe(false) + expect(result.current.isMainnet).toBe(false) + expect(result.current.displayLabel).toBe("Unknown") + }) + }) +}) diff --git a/apps/web/src/features/wallet/hooks/useNetwork.ts b/apps/web/src/features/wallet/hooks/useNetwork.ts index 8d196d0..b600a90 100644 --- a/apps/web/src/features/wallet/hooks/useNetwork.ts +++ b/apps/web/src/features/wallet/hooks/useNetwork.ts @@ -1,13 +1,52 @@ import { useWalletStore } from "../store/wallet-store" import { NETWORK } from "@/app/config/network" +export function normalizeNetwork(network: string | null | undefined): "testnet" | "mainnet" | "unknown" { + if (!network) return "unknown" + + const normalized = network.trim().toLowerCase() + + if ( + normalized === "testnet" || + normalized === "test sdf network ; september 2015" + ) { + return "testnet" + } + + if ( + normalized === "public" || + normalized === "mainnet" || + normalized === "public global stellar network ; september 2015" + ) { + return "mainnet" + } + + return "unknown" +} + export function useNetwork() { const { network, status } = useWalletStore() - const isTestnet = network === "testnet" - const isMainnet = network === "mainnet" + const normalizedNetwork = normalizeNetwork(network) + const isTestnet = normalizedNetwork === "testnet" + const isMainnet = normalizedNetwork === "mainnet" + // Mismatch only meaningful when a wallet is connected - const mismatch = status === "connected" && network !== NETWORK.name + const mismatch = status === "connected" && normalizedNetwork !== NETWORK.name + + const displayLabel = + normalizedNetwork === "testnet" + ? "Testnet" + : normalizedNetwork === "mainnet" + ? "Mainnet" + : "Unknown" - return { network, isTestnet, isMainnet, mismatch } + return { + network, + normalizedNetwork, + isTestnet, + isMainnet, + mismatch, + displayLabel, + } } diff --git a/apps/web/src/features/wallet/store/wallet-store.ts b/apps/web/src/features/wallet/store/wallet-store.ts index cc19ba0..885f9bf 100644 --- a/apps/web/src/features/wallet/store/wallet-store.ts +++ b/apps/web/src/features/wallet/store/wallet-store.ts @@ -2,7 +2,7 @@ import { create } from "zustand" import { persist } from "zustand/middleware" type WalletStatus = "disconnected" | "connecting" | "connected" | "error" -type Network = "testnet" | "mainnet" +type Network = string | null type WalletStore = { address: string | null @@ -17,7 +17,7 @@ type WalletStore = { } const DEFAULT_NETWORK: Network = - (import.meta.env.VITE_NETWORK as Network) === "mainnet" ? "mainnet" : "testnet" + (import.meta.env.VITE_NETWORK as string) === "mainnet" ? "mainnet" : "testnet" export const useWalletStore = create()( persist(