diff --git a/src/components/SorobanInvokeButton.test.tsx b/src/components/SorobanInvokeButton.test.tsx index 1f95a54..1243790 100644 --- a/src/components/SorobanInvokeButton.test.tsx +++ b/src/components/SorobanInvokeButton.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach,describe, expect, it, vi } from "vitest"; import { useSorokit } from "@/context/useSorokit"; @@ -49,6 +49,82 @@ describe("SorobanInvokeButton", () => { expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument(); }); + it("shows 'Invoking {method}…' as the loading label while an invocation is in flight", async () => { + let resolveInvoke: (value: { + data: unknown; + error: string | null; + status: string; + }) => void; + vi.mocked(getClient).mockReturnValue({ + soroban: { + invokeContract: vi + .fn() + .mockImplementation(() => new Promise((resolve) => { resolveInvoke = resolve; })), + }, + } as unknown as SorokitClient); + + render(); + fireEvent.click(screen.getByRole("button", { name: "transfer()" })); + + const button = screen.getByRole("button"); + expect(button.textContent).toContain("Invoking transfer…"); + + await act(async () => { + resolveInvoke!({ data: { ok: true }, error: null, status: "success" }); + }); + expect( + screen.getByRole("button", { name: "transfer()" }), + ).toHaveTextContent("transfer()"); + }); + + it("shows a generic 'Invoking…' loading label when a custom label is provided", async () => { + let resolveInvoke: (value: { + data: unknown; + error: string | null; + status: string; + }) => void; + vi.mocked(getClient).mockReturnValue({ + soroban: { + invokeContract: vi + .fn() + .mockImplementation(() => new Promise((resolve) => { resolveInvoke = resolve; })), + }, + } as unknown as SorokitClient); + + render(); + fireEvent.click(screen.getByRole("button", { name: "Send" })); + + expect(screen.getByRole("button").textContent).toContain("Invoking…"); + + await act(async () => { + resolveInvoke!({ data: { ok: true }, error: null, status: "success" }); + }); + }); + + it("renders the tooltip prop as the button's title when connected", () => { + mockInvokeContract({ data: null, error: null, status: "idle" }); + render(); + expect(screen.getByRole("button", { name: "transfer()" })).toHaveAttribute( + "title", + "Runs transfer on-chain", + ); + }); + + it("falls back to a connect hint title when the wallet is not connected", () => { + vi.mocked(useSorokit).mockReturnValue({ + isConnected: false, + } as unknown as ReturnType); + vi.mocked(getClient).mockReturnValue({ + soroban: { invokeContract: vi.fn() }, + } as unknown as SorokitClient); + + render(); + expect(screen.getByRole("button", { name: "transfer()" })).toHaveAttribute( + "title", + "Connect wallet to invoke", + ); + }); + it("calls onSuccess with the returned data on a successful invocation", async () => { const onSuccess = vi.fn(); mockInvokeContract({ data: { txHash: "abc" }, error: null, status: "success" }); @@ -116,6 +192,27 @@ describe("SorobanInvokeButton", () => { expect(screen.queryByText(/something went wrong/i)).not.toBeInTheDocument(); }); + it("caps the result container height with maxResultHeight for overflow", async () => { + mockInvokeContract({ data: { txHash: "abc" }, error: null, status: "success" }); + render(); + fireEvent.click(screen.getByRole("button", { name: "transfer()" })); + + await waitFor(() => expect(screen.getByText("Done")).toBeInTheDocument()); + // The result container has a bounded max-height that scrolls on overflow. + const resultScroller = screen.getByText(/txHash/).closest("div.overflow-y-auto"); + expect(resultScroller).toHaveStyle({ maxHeight: "120px" }); + }); + + it("uses the default 200px max result height when maxResultHeight is not provided", async () => { + mockInvokeContract({ data: { txHash: "abc" }, error: null, status: "success" }); + render(); + fireEvent.click(screen.getByRole("button", { name: "transfer()" })); + + await waitFor(() => expect(screen.getByText("Done")).toBeInTheDocument()); + const resultScroller = screen.getByText(/txHash/).closest("div.overflow-y-auto"); + expect(resultScroller).toHaveStyle({ maxHeight: "200px" }); + }); + it("disables the button and shows connect wallet hint when not connected", () => { vi.mocked(useSorokit).mockReturnValue({ isConnected: false, diff --git a/src/components/ui/Button.test.tsx b/src/components/ui/Button.test.tsx index 462707f..b359268 100644 --- a/src/components/ui/Button.test.tsx +++ b/src/components/ui/Button.test.tsx @@ -259,26 +259,32 @@ describe("ButtonGroup", () => { render( + ); const group = screen.getByRole("group"); expect(group).toHaveAttribute("data-orientation", "horizontal"); expect(group.className).toContain("flex-row"); - expect(group.className).toContain("rounded-l-none"); - expect(group.className).toContain("rounded-r-none"); + // The shared radius is collapsed through child-combinator selectors so the + // buttons read as one connected control: non-first siblings lose the left + // radius, non-last siblings lose the right radius. + expect(group.className).toContain("[&>*:not(:first-child)]:rounded-l-none"); + expect(group.className).toContain("[&>*:not(:last-child)]:rounded-r-none"); }); it("collapses the shared border radius vertically", () => { render( + ); const group = screen.getByRole("group"); expect(group).toHaveAttribute("data-orientation", "vertical"); expect(group.className).toContain("flex-col"); - expect(group.className).toContain("rounded-t-none"); - expect(group.className).toContain("rounded-b-none"); + // Non-first buttons drop the top radius, non-last buttons drop the bottom. + expect(group.className).toContain("[&>*:not(:first-child)]:rounded-t-none"); + expect(group.className).toContain("[&>*:not(:last-child)]:rounded-b-none"); }); it("merges a custom className", () => {