Skip to content

Commit c2ec9f1

Browse files
authored
Merge pull request #57 from Sam-Rytech/fix-issues-20-21
test: add coverage for components and utils and fix claim error bug (#20, #21)
2 parents 05ea0f4 + 1671521 commit c2ec9f1

11 files changed

Lines changed: 402 additions & 7 deletions

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"dev": "vite",
2828
"build": "tsc -b && vite build",
2929
"lint": "eslint .",
30-
"preview": "vite preview"
30+
"preview": "vite preview",
31+
"test": "vitest run"
3132
},
3233
"dependencies": {
3334
"@hugeicons/core-free-icons": "^4.1.2",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { describe, it, expect, vi, beforeEach } from "vitest";
3+
import { AccountCard } from "./AccountCard";
4+
import { useSorokit } from "@/context/useSorokit";
5+
6+
vi.mock("@/context/useSorokit", () => ({
7+
useSorokit: vi.fn(),
8+
}));
9+
10+
describe("AccountCard", () => {
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
});
14+
15+
it("renders a skeleton during loading", () => {
16+
(useSorokit as any).mockReturnValue({
17+
address: "GABC",
18+
account: null,
19+
isLoadingAccount: true,
20+
});
21+
22+
const { container } = render(<AccountCard />);
23+
// Skeleton renders when isLoadingAccount is true. We can check for a div with animate-pulse
24+
// The skeleton from ui/Skeleton uses animate-pulse. Wait, the actual Skeleton component wasn't mocked.
25+
// It's just a div.
26+
expect(container.querySelectorAll(".animate-pulse")).toBeTruthy();
27+
expect(screen.queryByText("Sequence")).not.toBeInTheDocument();
28+
});
29+
30+
it("renders account fields after load", () => {
31+
(useSorokit as any).mockReturnValue({
32+
address: "GABC",
33+
account: {
34+
sequence: "123456",
35+
subentryCount: 2,
36+
},
37+
isLoadingAccount: false,
38+
});
39+
40+
render(<AccountCard />);
41+
42+
expect(screen.getByText("Sequence")).toBeInTheDocument();
43+
expect(screen.getByText("123456")).toBeInTheDocument();
44+
expect(screen.getByText("Subentries")).toBeInTheDocument();
45+
expect(screen.getByText("2")).toBeInTheDocument();
46+
});
47+
48+
it("returns null when no address is present", () => {
49+
(useSorokit as any).mockReturnValue({
50+
address: null,
51+
account: null,
52+
isLoadingAccount: false,
53+
});
54+
55+
const { container } = render(<AccountCard />);
56+
expect(container).toBeEmptyDOMElement();
57+
});
58+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
2+
import { describe, it, expect, vi, beforeEach } from "vitest";
3+
import { ClaimableBalanceCard } from "./ClaimableBalanceCard";
4+
import { getClient } from "@/lib/client";
5+
import { useSorokit } from "@/context/useSorokit";
6+
7+
vi.mock("@/context/useSorokit", () => ({
8+
useSorokit: vi.fn(),
9+
}));
10+
11+
vi.mock("@/lib/client", () => ({
12+
getClient: vi.fn(),
13+
}));
14+
15+
describe("ClaimableBalanceCard", () => {
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
});
19+
20+
it("renders an error message and re-enables button on claim failure, shows Claimed badge on success", async () => {
21+
(useSorokit as any).mockReturnValue({
22+
address: "GABC123",
23+
isConnected: true,
24+
});
25+
26+
const mockClaimBalance = vi.fn()
27+
.mockResolvedValueOnce({ data: null, error: "Network error" }) // Failure first
28+
.mockResolvedValueOnce({ data: { hash: "tx123" }, error: null }); // Success second
29+
30+
const mockGetClaimableBalances = vi.fn().mockResolvedValue({
31+
data: [
32+
{
33+
id: "cb1",
34+
asset: "XLM:GABC",
35+
amount: "10.0",
36+
sponsor: "GDEF",
37+
claimants: [],
38+
},
39+
],
40+
error: null,
41+
});
42+
43+
(getClient as any).mockReturnValue({
44+
account: {
45+
getClaimableBalances: mockGetClaimableBalances,
46+
claimBalance: mockClaimBalance,
47+
},
48+
});
49+
50+
render(<ClaimableBalanceCard />);
51+
52+
// Wait for the balance to load
53+
expect(await screen.findByText("10")).toBeInTheDocument();
54+
55+
const claimButton = screen.getByRole("button", { name: "Claim" });
56+
57+
// Simulate first click: Failure
58+
fireEvent.click(claimButton);
59+
60+
// Wait for error to show up
61+
expect(await screen.findByText("Network error")).toBeInTheDocument();
62+
expect(claimButton).not.toBeDisabled(); // Should re-enable
63+
64+
// Simulate second click: Success
65+
fireEvent.click(claimButton);
66+
67+
// Wait for "Claimed" badge
68+
expect(await screen.findByText("Claimed")).toBeInTheDocument();
69+
70+
// Button should be gone after claim
71+
expect(screen.queryByRole("button", { name: "Claim" })).not.toBeInTheDocument();
72+
// Error should be gone
73+
expect(screen.queryByText("Network error")).not.toBeInTheDocument();
74+
});
75+
});

‎src/components/ClaimableBalanceCard.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ function BalanceRow({ cb }: { cb: ClaimableBalance }) {
1919
setClaimError(null);
2020
try {
2121
const { error } = await getClient().account.claimBalance(cb.id);
22-
if (error) {
22+
if (!error) {
23+
setClaimed(true);
24+
} else {
2325
setClaimError(error);
24-
return;
2526
}
26-
setClaimed(true);
2727
} finally {
2828
setClaiming(false);
2929
}
@@ -59,11 +59,14 @@ function BalanceRow({ cb }: { cb: ClaimableBalance }) {
5959
size="sm"
6060
loading={claiming}
6161
onClick={handleClaim}
62+
className="shrink-0"
6263
>
6364
Claim
6465
</Button>
6566
{claimError && (
66-
<p className="text-[12px] text-red text-right">{claimError}</p>
67+
<span className="text-[10px] text-red max-w-[150px] text-right truncate">
68+
{claimError}
69+
</span>
6770
)}
6871
</div>
6972
)}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { render, screen, fireEvent } from "@testing-library/react";
2+
import { describe, it, expect, vi } from "vitest";
3+
import { ErrorBoundary } from "./ErrorBoundary";
4+
5+
const ThrowError = () => {
6+
throw new Error("Test error!");
7+
};
8+
9+
describe("ErrorBoundary", () => {
10+
it("renders custom fallback prop and passes error and reset function", () => {
11+
const fallbackSpy = vi.fn().mockImplementation((error, reset) => (
12+
<div>
13+
<p>Custom Fallback</p>
14+
<p>{error.message}</p>
15+
<button onClick={reset}>Reset Custom</button>
16+
</div>
17+
));
18+
19+
// Suppress console.error for expected thrown error
20+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
21+
22+
render(
23+
<ErrorBoundary fallback={fallbackSpy}>
24+
<ThrowError />
25+
</ErrorBoundary>
26+
);
27+
28+
expect(fallbackSpy).toHaveBeenCalled();
29+
expect(screen.getByText("Custom Fallback")).toBeInTheDocument();
30+
expect(screen.getByText("Test error!")).toBeInTheDocument();
31+
32+
const resetBtn = screen.getByText("Reset Custom");
33+
expect(resetBtn).toBeInTheDocument();
34+
35+
// Reset should be callable and reset the error state (though it will just throw again because we still render ThrowError)
36+
// but we can verify it doesn't crash.
37+
fireEvent.click(resetBtn);
38+
39+
consoleSpy.mockRestore();
40+
});
41+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
2+
import { describe, it, expect, vi, beforeEach } from "vitest";
3+
import { TransactionPanel } from "./TransactionPanel";
4+
import { getClient } from "@/lib/client";
5+
import { useSorokit } from "@/context/useSorokit";
6+
7+
vi.mock("@/context/useSorokit", () => ({
8+
useSorokit: vi.fn(),
9+
}));
10+
11+
vi.mock("@/lib/client", () => ({
12+
getClient: vi.fn(),
13+
}));
14+
15+
describe("TransactionPanel", () => {
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
(useSorokit as any).mockReturnValue({
19+
address: "GABC",
20+
isConnected: true,
21+
});
22+
});
23+
24+
it("handles loading, success, and error states", async () => {
25+
const mockSubmit = vi.fn().mockImplementation(() => {
26+
return new Promise(resolve => {
27+
setTimeout(() => resolve({ data: { hash: "txhash123", ledger: 100 }, error: null }), 50);
28+
});
29+
});
30+
31+
(getClient as any).mockReturnValue({
32+
transaction: {
33+
submit: mockSubmit,
34+
},
35+
});
36+
37+
render(<TransactionPanel />);
38+
39+
const destInput = screen.getByLabelText("Destination Address");
40+
const amountInput = screen.getByLabelText("Amount (XLM)");
41+
const submitBtn = screen.getByRole("button", { name: "Send Payment" });
42+
43+
fireEvent.change(destInput, { target: { value: "GDEF" } });
44+
fireEvent.change(amountInput, { target: { value: "10" } });
45+
46+
// Submit and check loading state
47+
fireEvent.click(submitBtn);
48+
expect(submitBtn).toBeDisabled();
49+
expect(screen.getByRole("button", { name: "Submitting…" })).toBeInTheDocument();
50+
51+
// Check success state
52+
expect(await screen.findByText("Transaction submitted")).toBeInTheDocument();
53+
expect(screen.getByText("Ledger #100")).toBeInTheDocument();
54+
expect(screen.getByText("txhash123")).toBeInTheDocument();
55+
56+
// Test "New Transaction" button resets state
57+
const newTxBtn = screen.getByRole("button", { name: "New Transaction" });
58+
fireEvent.click(newTxBtn);
59+
60+
expect(screen.getByLabelText("Destination Address")).toHaveValue("");
61+
expect(screen.getByLabelText("Amount (XLM)")).toHaveValue(null);
62+
});
63+
64+
it("handles error state", async () => {
65+
const mockSubmit = vi.fn().mockResolvedValue({ data: null, error: "Insufficient balance" });
66+
67+
(getClient as any).mockReturnValue({
68+
transaction: {
69+
submit: mockSubmit,
70+
},
71+
});
72+
73+
render(<TransactionPanel />);
74+
75+
fireEvent.change(screen.getByLabelText("Destination Address"), { target: { value: "GDEF" } });
76+
fireEvent.change(screen.getByLabelText("Amount (XLM)"), { target: { value: "10" } });
77+
78+
fireEvent.click(screen.getByRole("button", { name: "Send Payment" }));
79+
80+
expect(await screen.findByText("Transaction failed")).toBeInTheDocument();
81+
expect(screen.getByText("Insufficient balance")).toBeInTheDocument();
82+
});
83+
});

0 commit comments

Comments
 (0)