diff --git a/src/components/WalletConnectButton.test.tsx b/src/components/WalletConnectButton.test.tsx index 57cc463..231b595 100644 --- a/src/components/WalletConnectButton.test.tsx +++ b/src/components/WalletConnectButton.test.tsx @@ -1,7 +1,8 @@ -import { fireEvent,render, screen, waitFor } from "@testing-library/react"; -import { beforeEach,describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { useSorokit } from "@/context/useSorokit"; +import { truncateAddress } from "@/lib/utils"; import { WalletConnectButton } from "./WalletConnectButton"; @@ -11,156 +12,289 @@ vi.mock("@/context/useSorokit", () => ({ describe("WalletConnectButton", () => { const mockConnect = vi.fn(); + const mockDisconnect = vi.fn(); const mockClearError = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); - function mockUseSorokit(overrides: Partial> = {}) { + function mockUseSorokit( + overrides: Partial> = {}, + ) { return { address: null, + walletName: null, isConnected: false, isConnecting: false, - connectWallet: vi.fn(), - disconnectWallet: vi.fn(), + isLoading: false, + isDisconnecting: false, + connectWallet: vi.fn().mockResolvedValue(undefined), + disconnectWallet: vi.fn().mockResolvedValue(undefined), account: null, balances: [], isLoadingAccount: false, - refreshAccount: vi.fn(), + refreshAccount: vi.fn().mockResolvedValue(undefined), network: null, - switchNetwork: vi.fn(), + switchNetwork: vi.fn().mockResolvedValue(undefined), error: null, + errorHistory: [], clearError: vi.fn(), ...overrides, }; } - it("renders 'Connect Wallet' when not connected", () => { - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - connectWallet: mockConnect, - clearError: mockClearError, - })); + describe("Disconnected State", () => { + it("renders 'Connect Wallet' text when not connected", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: false, + isConnecting: false, + address: null, + connectWallet: mockConnect, + clearError: mockClearError, + }), + ); - render(); - expect(screen.getByRole("button", { name: "Connect Wallet" })).toBeInTheDocument(); - }); + render(); + expect( + screen.getByRole("button", { name: "Connect Wallet" }), + ).toBeInTheDocument(); + expect(screen.getByText("Connect Wallet")).toBeInTheDocument(); + }); - it("opens the wallet connect modal on click", async () => { - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - connectWallet: mockConnect, - clearError: mockClearError, - })); - - render(); - fireEvent.click(screen.getByRole("button", { name: "Connect Wallet" })); - await waitFor(() => - screen.getByRole("dialog", { name: /connect a wallet/i }), - ); - }); + it("opens the wallet connect modal on click", async () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: false, + connectWallet: mockConnect, + clearError: mockClearError, + }), + ); + + render(); + fireEvent.click(screen.getByRole("button", { name: "Connect Wallet" })); + await waitFor(() => + expect( + screen.getByRole("dialog", { name: /connect a wallet/i }), + ).toBeInTheDocument(), + ); + }); - it("triggers connectWallet when a wallet is selected in the modal", async () => { - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - connectWallet: mockConnect, - clearError: mockClearError, - })); - - render(); - fireEvent.click(screen.getByRole("button", { name: "Connect Wallet" })); - await waitFor(() => - screen.getByRole("dialog", { name: /connect a wallet/i }), - ); - fireEvent.click(screen.getByRole("button", { name: "Freighter" })); - expect(mockConnect).toHaveBeenCalledTimes(1); + it("triggers connectWallet from context when a wallet is selected in the modal", async () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: false, + connectWallet: mockConnect, + clearError: mockClearError, + }), + ); + + render(); + fireEvent.click(screen.getByRole("button", { name: "Connect Wallet" })); + await waitFor(() => + expect( + screen.getByRole("dialog", { name: /connect a wallet/i }), + ).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByRole("button", { name: "Freighter" })); + expect(mockConnect).toHaveBeenCalledTimes(1); + }); }); - it("renders loading state when connecting", () => { - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - isConnecting: true, - connectWallet: mockConnect, - clearError: mockClearError, - })); + describe("Connecting State", () => { + it("renders button text showing 'Connecting…' when connecting", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: false, + isConnecting: true, + connectWallet: mockConnect, + clearError: mockClearError, + }), + ); + + render(); + expect( + screen.getByRole("button", { name: /connecting…/i }), + ).toBeInTheDocument(); + expect(screen.getByText("Connecting…")).toBeInTheDocument(); + }); + + it("renders loading indicator and spinner while isConnecting is true", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: false, + isConnecting: true, + connectWallet: mockConnect, + clearError: mockClearError, + }), + ); + + const { container } = render(); + const button = screen.getByRole("button", { name: /connecting…/i }); - render(); - expect(screen.getByRole("button", { name: "Connecting…" })).toBeInTheDocument(); + expect(button).toHaveAttribute("aria-busy", "true"); + expect(button).toBeDisabled(); + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(container.querySelector(".animate-spin")).toBeInTheDocument(); + }); }); - it("renders connected state with correct address and aria-label", () => { + describe("Connected State", () => { const fullAddress = "GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - isConnected: true, - address: fullAddress, - connectWallet: mockConnect, - clearError: mockClearError, - network: { name: "testnet" }, - })); - - render(); - const button = screen.getByRole("button", { - name: `Wallet connected: ${fullAddress}. Click to manage.`, + + it("renders truncated wallet address in the pill using truncateAddress()", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: true, + address: fullAddress, + connectWallet: mockConnect, + clearError: mockClearError, + }), + ); + + const { container } = render(); + const expectedTruncated = truncateAddress(fullAddress); + + expect(expectedTruncated).toBe("GABC12...WXYZ"); + expect(screen.getByText(expectedTruncated)).toBeInTheDocument(); + + const addressSpan = container.querySelector("[data-address]"); + expect(addressSpan).toHaveTextContent(expectedTruncated); }); - expect(button).toBeInTheDocument(); - expect(screen.getByText("GABC12...WXYZ")).toBeInTheDocument(); - }); - it("renders inline error message and handles clearError", () => { - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - connectWallet: mockConnect, - error: "Connection failed", - clearError: mockClearError, - })); + it("renders connected state with correct aria-label and green indicator dot", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: true, + address: fullAddress, + }), + ); - render(); - expect(screen.getByText("Connection failed")).toBeInTheDocument(); + const { container } = render(); + const button = screen.getByRole("button", { + name: `Wallet connected: ${fullAddress}. Click to manage.`, + }); + expect(button).toBeInTheDocument(); + + const statusDot = container.querySelector(".bg-green"); + expect(statusDot).toBeInTheDocument(); + }); - const clearBtn = screen.getByRole("button", { name: "Clear error" }); - expect(clearBtn).toBeInTheDocument(); - fireEvent.click(clearBtn); - expect(mockClearError).toHaveBeenCalledTimes(1); + it("calls onOpenModal when address pill is clicked and the prop is provided", () => { + const mockOnOpenModal = vi.fn(); + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: true, + address: fullAddress, + }), + ); + + render(); + const addressPill = screen.getByRole("button", { + name: `Wallet connected: ${fullAddress}. Click to manage.`, + }); + + fireEvent.click(addressPill); + expect(mockOnOpenModal).toHaveBeenCalledTimes(1); + }); + + it("does not crash and toggles dropdown when address pill is clicked without onOpenModal", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: true, + address: fullAddress, + disconnectWallet: mockDisconnect, + }), + ); + + render(); + const addressPill = screen.getByRole("button", { + name: `Wallet connected: ${fullAddress}. Click to manage.`, + }); + + expect(() => { + fireEvent.click(addressPill); + }).not.toThrow(); + + // Dropdown with disconnect button appears + const disconnectBtn = screen.getByRole("button", { name: /disconnect/i }); + expect(disconnectBtn).toBeInTheDocument(); + + // Clicking again closes the dropdown + fireEvent.click(addressPill); + expect( + screen.queryByRole("button", { name: /disconnect/i }), + ).not.toBeInTheDocument(); + }); }); - it("renders disconnect loading state when isDisconnecting is true", async () => { - const mockDisconnect = vi.fn(); - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - isConnected: true, - address: "GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", - disconnectWallet: mockDisconnect, - isDisconnecting: true, - network: { name: "testnet" }, - })); - - render(); - // Click the wallet button to open the Radix dropdown - fireEvent.click(screen.getByRole("button", { name: /wallet connected/i })); - await waitFor(() => { + describe("Error & Disconnect Handling", () => { + it("renders inline error message and handles clearError", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + connectWallet: mockConnect, + error: "Connection failed", + clearError: mockClearError, + }), + ); + + render(); + expect(screen.getByText("Connection failed")).toBeInTheDocument(); + + const clearBtn = screen.getByRole("button", { name: "Clear error" }); + expect(clearBtn).toBeInTheDocument(); + fireEvent.click(clearBtn); + expect(mockClearError).toHaveBeenCalledTimes(1); + }); + + it("renders disconnect loading state when isDisconnecting is true", () => { + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: true, + address: "GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", + disconnectWallet: mockDisconnect, + isDisconnecting: true, + }), + ); + + render(); + fireEvent.click(screen.getByRole("button", { name: /wallet connected/i })); expect(screen.getByText("Disconnecting…")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /disconnect/i }), + ).toBeDisabled(); }); - expect(screen.getByRole("menuitem", { name: /disconnect/i })).toHaveAttribute("aria-disabled", "true"); - }); - it("clears the error banner after a successful connect clears the error", () => { - // First render with an error (not connected state) - const { rerender } = render(); - - // Simulate error state - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - connectWallet: mockConnect, - error: "Previous error", - clearError: mockClearError, - })); - rerender(); - expect(screen.getByText("Previous error")).toBeInTheDocument(); - - // Simulate successful connect (error is cleared, connected state shown) - vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ - isConnected: true, - address: "GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", - error: null, - clearError: mockClearError, - network: { name: "testnet" }, - })); - rerender(); - expect(screen.queryByText("Previous error")).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: /wallet connected/i })).toBeInTheDocument(); + it("clears the error banner after a successful connect clears the error", () => { + const { rerender } = render(); + + // Simulate error state + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + connectWallet: mockConnect, + error: "Previous error", + clearError: mockClearError, + }), + ); + rerender(); + expect(screen.getByText("Previous error")).toBeInTheDocument(); + + // Simulate successful connect (error is cleared, connected state shown) + vi.mocked(useSorokit).mockReturnValue( + mockUseSorokit({ + isConnected: true, + address: "GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", + error: null, + clearError: mockClearError, + }), + ); + rerender(); + expect(screen.queryByText("Previous error")).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /wallet connected/i }), + ).toBeInTheDocument(); + }); }); });