From fd1d4849bd7db8eaea890d044eb15e82224ebc5b Mon Sep 17 00:00:00 2001 From: ajulaybeeb Date: Tue, 30 Jun 2026 06:35:31 +0100 Subject: [PATCH] test: add integration tests for wallet flow, payment flow, and exports Closes #50 --- src/__tests__/exports.test.ts | 16 +++++ src/__tests__/payment-flow.test.tsx | 90 +++++++++++++++++++++++++++++ src/__tests__/wallet-flow.test.tsx | 66 +++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/__tests__/exports.test.ts create mode 100644 src/__tests__/payment-flow.test.tsx create mode 100644 src/__tests__/wallet-flow.test.tsx diff --git a/src/__tests__/exports.test.ts b/src/__tests__/exports.test.ts new file mode 100644 index 0000000..fec62c9 --- /dev/null +++ b/src/__tests__/exports.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest"; +import * as exports from "@/components/index"; + +describe("Public Exports Smoke Test", () => { + it("exports are valid functions or components", () => { + const namedExports = Object.entries(exports); + + expect(namedExports.length).toBeGreaterThan(0); + + for (const [name, value] of namedExports) { + // Components, providers, and hooks should be functions + // We skip checking types as they disappear at runtime and won't be in the object + expect(typeof value).toBe("function", `Export ${name} is not a function. Got ${typeof value}`); + } + }); +}); diff --git a/src/__tests__/payment-flow.test.tsx b/src/__tests__/payment-flow.test.tsx new file mode 100644 index 0000000..3cfc6fa --- /dev/null +++ b/src/__tests__/payment-flow.test.tsx @@ -0,0 +1,90 @@ +import { render, screen, act, fireEvent, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { SorokitProvider } from "@/context/SorokitProvider"; +import { TransactionPanel } from "@/components/TransactionPanel"; +import { getClient } from "@/lib/client"; + +describe("Payment Flow Integration", () => { + let mockClient: ReturnType; + const mockAddress = "GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + + beforeEach(() => { + mockClient = { + wallet: { + connect: vi.fn().mockResolvedValue({ data: { address: mockAddress }, error: null }), + disconnect: vi.fn().mockResolvedValue(undefined), + }, + account: { + getAccount: vi.fn().mockResolvedValue({ data: { sequence: "123456789" }, error: null }), + getBalances: vi.fn().mockResolvedValue({ data: [{ asset: "XLM", balance: "100.5" }], error: null }), + }, + network: { + getNetwork: vi.fn().mockResolvedValue({ data: { name: "mainnet" }, error: null }), + switchNetwork: vi.fn().mockResolvedValue({ data: { name: "testnet" }, error: null }), + }, + transaction: { + submit: vi.fn().mockResolvedValue({ data: { hash: "txhash123", ledger: 100 }, error: null }), + } + } as unknown as ReturnType; + }); + + it("submits payment with correct destination, amount, and source", async () => { + // We render SorokitProvider, but we must "connect" it first to have an active address. + // We can simulate this by calling client.wallet.connect() inside a wrapper, or we can just + // click a connect button if we add one. + // Alternatively, SorokitProvider might just rely on the client. + // Let's create a small component to trigger connection. + const Wrapper = () => { + return ( + + + + + ); + }; + + const ConnectTrigger = () => { + const { connectWallet } = require("@/context/useSorokit").useSorokit(); + return ; + }; + + render(); + + // Connect the wallet first so TransactionPanel doesn't show "Wallet not connected" + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: "Test Connect" })); + }); + + const destInput = screen.getByLabelText("Destination Address"); + const amountInput = screen.getByLabelText("Amount (XLM)"); + const submitBtn = screen.getByRole("button", { name: "Send Payment" }); + + const validDest = "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + const validAmount = "15.5"; + + fireEvent.change(destInput, { target: { value: validDest } }); + fireEvent.change(amountInput, { target: { value: validAmount } }); + + await act(async () => { + fireEvent.click(submitBtn); + }); + + // Verify loading state appears + expect(screen.getByRole("button", { name: "Submitting…" })).toBeInTheDocument(); + + // Verify success state renders + await waitFor(() => { + expect(screen.getByText("Transaction submitted")).toBeInTheDocument(); + expect(screen.getByText("txhash123")).toBeInTheDocument(); + }); + + // Most importantly, verify the mock client's transaction.submit was called with the correct parameters + expect(mockClient.transaction.submit).toHaveBeenCalledWith( + expect.objectContaining({ + destination: validDest, + amount: validAmount, + source: mockAddress + }) + ); + }); +}); diff --git a/src/__tests__/wallet-flow.test.tsx b/src/__tests__/wallet-flow.test.tsx new file mode 100644 index 0000000..cb883af --- /dev/null +++ b/src/__tests__/wallet-flow.test.tsx @@ -0,0 +1,66 @@ +import { render, screen, act, fireEvent, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { SorokitProvider } from "@/context/SorokitProvider"; +import { WalletConnectButton } from "@/components/WalletConnectButton"; +import { AccountCard } from "@/components/AccountCard"; +import { getClient } from "@/lib/client"; + +describe("Wallet Connect Flow Integration", () => { + let mockClient: ReturnType; + + beforeEach(() => { + mockClient = { + wallet: { + connect: vi.fn().mockResolvedValue({ data: { address: "GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" }, error: null }), + disconnect: vi.fn().mockResolvedValue(undefined), + }, + account: { + getAccount: vi.fn().mockResolvedValue({ data: { sequence: "123456789" }, error: null }), + getBalances: vi.fn().mockResolvedValue({ data: [{ asset: "XLM", balance: "100.5" }], error: null }), + }, + network: { + getNetwork: vi.fn().mockResolvedValue({ data: { name: "mainnet" }, error: null }), + switchNetwork: vi.fn().mockResolvedValue({ data: { name: "testnet" }, error: null }), + }, + } as unknown as ReturnType; + }); + + it("connects wallet, displays account, and disconnects", async () => { + render( + + + + + ); + + // Initial state: Wallet not connected, AccountCard should not show sequence + expect(screen.getByRole("button", { name: /connect/i })).toBeInTheDocument(); + expect(screen.queryByText(/Sequence:/i)).not.toBeInTheDocument(); + + // Action 1: Connect Wallet + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: /connect/i })); + }); + + // Verification 1: Wallet connected, AccountCard renders account data + expect(mockClient.wallet.connect).toHaveBeenCalled(); + + // Disconnect button should appear inside WalletConnectButton or similar if it toggles + // We wait for the account data to be fetched and rendered in AccountCard + await waitFor(() => { + expect(screen.getByText(/123456789/i)).toBeInTheDocument(); // Sequence number + }); + + // Action 2: Disconnect Wallet + // The WalletConnectButton usually turns into a Disconnect button when connected + const disconnectBtn = screen.getByRole("button", { name: /disconnect/i }); + await act(async () => { + fireEvent.click(disconnectBtn); + }); + + // Verification 2: Wallet disconnected, AccountCard returns null (or clears data) + expect(mockClient.wallet.disconnect).toHaveBeenCalled(); + expect(screen.getByRole("button", { name: /connect/i })).toBeInTheDocument(); + expect(screen.queryByText(/123456789/i)).not.toBeInTheDocument(); + }); +});