Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion src/components/SorobanInvokeButton.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(<SorobanInvokeButton params={PARAMS} />);
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(<SorobanInvokeButton params={PARAMS} label="Send" />);
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(<SorobanInvokeButton params={PARAMS} tooltip="Runs transfer on-chain" />);
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<typeof useSorokit>);
vi.mocked(getClient).mockReturnValue({
soroban: { invokeContract: vi.fn() },
} as unknown as SorokitClient);

render(<SorobanInvokeButton params={PARAMS} tooltip="Runs transfer on-chain" />);
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" });
Expand Down Expand Up @@ -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(<SorobanInvokeButton params={PARAMS} maxResultHeight={120} />);
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(<SorobanInvokeButton params={PARAMS} />);
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,
Expand Down
14 changes: 10 additions & 4 deletions src/components/ui/Button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,26 +259,32 @@ describe("ButtonGroup", () => {
render(
<ButtonGroup>
<Button>Prev</Button>
<Button>Next</Button>
</ButtonGroup>
);
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(
<ButtonGroup orientation="vertical">
<Button>Top</Button>
<Button>Bottom</Button>
</ButtonGroup>
);
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", () => {
Expand Down
Loading