Skip to content

Commit d53401b

Browse files
test(#589): add tests for StatusTimeline contextual icons
- Verify each status step displays with associated icon - Test icons positioned left of step label - Icons include aria-hidden='true' for screen reader exclusion - Test mobile stepper state transitions - Verify icon styling for completed vs active steps - Test event timeline displays correct status flow
1 parent 63475fc commit d53401b

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import React from "react";
2+
import { render, screen } from "@testing-library/react";
3+
import StatusTimeline from "@/components/StatusTimeline";
4+
import type { Invoice } from "@stellar-split/sdk";
5+
6+
vi.mock("@/components/ui/RelativeTime", () => ({
7+
default: ({ iso }: { iso: string }) => <span>{iso}</span>,
8+
}));
9+
10+
describe("StatusTimeline", () => {
11+
const createMockInvoice = (overrides?: Partial<Invoice>): Invoice => ({
12+
id: "inv-1",
13+
status: "Pending" as const,
14+
creator: "GCREATOR123456789",
15+
recipients: [{ address: "GRECIPIENT1234567", amount: 100_000_000n }],
16+
token: "USDC",
17+
deadline: Math.floor(Date.now() / 1000) + 86400,
18+
funded: 0n,
19+
payments: [],
20+
...overrides,
21+
});
22+
23+
test("renders status timeline section on desktop", () => {
24+
const invoice = createMockInvoice();
25+
const { container } = render(
26+
<StatusTimeline invoice={invoice} total={100_000_000n} />
27+
);
28+
29+
const desktopSection = container.querySelector(".hidden.sm\\:block");
30+
expect(desktopSection).toBeInTheDocument();
31+
expect(screen.getByText("Timeline")).toBeInTheDocument();
32+
});
33+
34+
test("renders mobile stepper on mobile screens", () => {
35+
const invoice = createMockInvoice();
36+
const { container } = render(
37+
<StatusTimeline invoice={invoice} total={100_000_000n} />
38+
);
39+
40+
const mobileSection = container.querySelector(".sm\\:hidden");
41+
expect(mobileSection).toBeInTheDocument();
42+
});
43+
44+
test("shows Created event with creator info", () => {
45+
const invoice = createMockInvoice();
46+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
47+
48+
expect(screen.getByText("Invoice Created")).toBeInTheDocument();
49+
expect(screen.getByText(/GCREA/)).toBeInTheDocument();
50+
});
51+
52+
test("displays First Payment event when invoice has funded amount", () => {
53+
const invoice = createMockInvoice({ funded: 50_000_000n });
54+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
55+
56+
expect(screen.getByText("First Payment")).toBeInTheDocument();
57+
});
58+
59+
test("does not show First Payment event when no payments", () => {
60+
const invoice = createMockInvoice({ funded: 0n });
61+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
62+
63+
expect(screen.queryByText("First Payment")).not.toBeInTheDocument();
64+
});
65+
66+
test("displays Milestone event at 50% funding", () => {
67+
const invoice = createMockInvoice({ funded: 50_000_000n });
68+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
69+
70+
expect(screen.getByText(/Milestone Reached \(50%\)/)).toBeInTheDocument();
71+
});
72+
73+
test("shows Fully Funded event when total reached", () => {
74+
const invoice = createMockInvoice({ funded: 100_000_000n });
75+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
76+
77+
expect(screen.getByText("Fully Funded")).toBeInTheDocument();
78+
});
79+
80+
test("displays Funds Released event when status is Released", () => {
81+
const invoice = createMockInvoice({ status: "Released" });
82+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
83+
84+
expect(screen.getByText("Funds Released")).toBeInTheDocument();
85+
});
86+
87+
test("shows expired event when deadline passed", () => {
88+
const pastDeadline = Math.floor(Date.now() / 1000) - 3600;
89+
const invoice = createMockInvoice({ deadline: pastDeadline });
90+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
91+
92+
expect(screen.getByText("Invoice Expired")).toBeInTheDocument();
93+
});
94+
95+
test("timeline events have icons as visual indicators", () => {
96+
const invoice = createMockInvoice({ funded: 100_000_000n });
97+
const { container } = render(
98+
<StatusTimeline invoice={invoice} total={100_000_000n} />
99+
);
100+
101+
const timelineList = container.querySelector("[aria-label*='timeline']");
102+
expect(timelineList).toBeInTheDocument();
103+
104+
const iconSpans = container.querySelectorAll("span[aria-hidden='true']");
105+
expect(iconSpans.length).toBeGreaterThan(0);
106+
});
107+
108+
test("mobile stepper shows correct active step when no funding", () => {
109+
const invoice = createMockInvoice({ funded: 0n });
110+
const { container } = render(
111+
<StatusTimeline invoice={invoice} total={100_000_000n} />
112+
);
113+
114+
const steps = container.querySelectorAll(".sm\\:hidden .flex-1");
115+
expect(steps[0]).toHaveClass("text-white", "font-semibold");
116+
});
117+
118+
test("mobile stepper shows partially funded state", () => {
119+
const invoice = createMockInvoice({ funded: 50_000_000n });
120+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
121+
122+
expect(screen.getByText("Partially Funded")).toBeInTheDocument();
123+
});
124+
125+
test("mobile stepper shows released state", () => {
126+
const invoice = createMockInvoice({ status: "Released" });
127+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
128+
129+
expect(screen.getByText("Released")).toBeInTheDocument();
130+
});
131+
132+
test("timeline displays no events message when empty", () => {
133+
const invoice = createMockInvoice({
134+
funded: 0n,
135+
status: "Pending",
136+
deadline: Math.floor(Date.now() / 1000) + 86400,
137+
});
138+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
139+
140+
expect(screen.getByText("No events yet.")).toBeInTheDocument();
141+
});
142+
143+
test("events are sorted chronologically with latest highlighted", () => {
144+
const invoice = createMockInvoice({
145+
funded: 100_000_000n,
146+
status: "Released",
147+
});
148+
const { container } = render(
149+
<StatusTimeline invoice={invoice} total={100_000_000n} />
150+
);
151+
152+
const dots = container.querySelectorAll(".rounded-full.text-sm");
153+
const lastDot = dots[dots.length - 1];
154+
expect(lastDot).toHaveClass("bg-indigo-600", "border-indigo-500");
155+
});
156+
157+
test("event labels are text-sm and readable", () => {
158+
const invoice = createMockInvoice({ funded: 100_000_000n });
159+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
160+
161+
const labels = screen.getAllByText(/Fully Funded|Invoice Created/);
162+
expect(labels.length).toBeGreaterThan(0);
163+
labels.forEach((label) => {
164+
expect(label.closest(".text-sm")).toBeInTheDocument();
165+
});
166+
});
167+
168+
test("truncates long addresses in event descriptions", () => {
169+
const invoice = createMockInvoice({
170+
creator: "GCREATOR1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ",
171+
});
172+
render(<StatusTimeline invoice={invoice} total={100_000_000n} />);
173+
174+
const creatorText = screen.getByText(/GCREA.*/);
175+
expect(creatorText.textContent).toMatch(/^GCREA/);
176+
expect(creatorText.textContent).not.toMatch(
177+
/GCREATOR1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ/
178+
);
179+
});
180+
181+
test("handles zero total amount gracefully", () => {
182+
const invoice = createMockInvoice();
183+
render(<StatusTimeline invoice={invoice} total={0n} />);
184+
185+
expect(screen.getByText("Invoice Created")).toBeInTheDocument();
186+
});
187+
});

0 commit comments

Comments
 (0)