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", () => {