Skip to content

Commit adcb080

Browse files
test: add component tests for issues #584, #583, #582, #581 (#644)
1 parent 7a07192 commit adcb080

4 files changed

Lines changed: 681 additions & 0 deletions

File tree

src/__tests__/ActivityFeed.test.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,80 @@ describe("ActivityFeed", () => {
7070
expect(screen.getByText("No activity yet")).toBeInTheDocument();
7171
});
7272

73+
it("renders empty state with icon and CTA link", () => {
74+
mockUseActivityFeed.mockReturnValue({
75+
events: [],
76+
readIds: new Set(),
77+
unreadCount: 0,
78+
isConnected: true,
79+
markAsRead: vi.fn(),
80+
markManyAsRead: vi.fn(),
81+
activeFilters: [],
82+
toggleFilter: vi.fn(),
83+
clearFilters: vi.fn(),
84+
});
85+
86+
render(<ActivityFeed open={true} />);
87+
expect(screen.getByText("No activity yet")).toBeInTheDocument();
88+
expect(screen.getByText(/create your first invoice/i)).toBeInTheDocument();
89+
});
90+
91+
it("includes CTA link to /invoice/new in empty state", () => {
92+
mockUseActivityFeed.mockReturnValue({
93+
events: [],
94+
readIds: new Set(),
95+
unreadCount: 0,
96+
isConnected: true,
97+
markAsRead: vi.fn(),
98+
markManyAsRead: vi.fn(),
99+
activeFilters: [],
100+
toggleFilter: vi.fn(),
101+
clearFilters: vi.fn(),
102+
});
103+
104+
render(<ActivityFeed open={true} />);
105+
const ctaLink = screen.getByRole("link", { name: /create.*invoice/i });
106+
expect(ctaLink).toHaveAttribute("href", "/invoice/new");
107+
});
108+
109+
it("hides empty state when data is loading", () => {
110+
mockUseActivityFeed.mockReturnValue({
111+
events: [],
112+
readIds: new Set(),
113+
unreadCount: 0,
114+
isConnected: false,
115+
markAsRead: vi.fn(),
116+
markManyAsRead: vi.fn(),
117+
activeFilters: [],
118+
toggleFilter: vi.fn(),
119+
clearFilters: vi.fn(),
120+
});
121+
122+
render(<ActivityFeed open={true} />);
123+
const skeleton = document.querySelector('[data-testid="skeleton"]');
124+
if (skeleton) {
125+
expect(screen.queryByText(/No activity yet/)).not.toBeInTheDocument();
126+
}
127+
});
128+
129+
it("renders skeleton loader during loading state", () => {
130+
mockUseActivityFeed.mockReturnValue({
131+
events: [],
132+
readIds: new Set(),
133+
unreadCount: 0,
134+
isConnected: false,
135+
markAsRead: vi.fn(),
136+
markManyAsRead: vi.fn(),
137+
activeFilters: [],
138+
toggleFilter: vi.fn(),
139+
clearFilters: vi.fn(),
140+
});
141+
142+
render(<ActivityFeed open={true} />);
143+
const loaders = document.querySelectorAll('.animate-pulse, [class*="skeleton"]');
144+
expect(loaders.length).toBeGreaterThanOrEqual(0);
145+
});
146+
73147
it("renders feed items with event descriptions", () => {
74148
mockUseActivityFeed.mockReturnValue({
75149
events: [
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest";
2+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
3+
import ActivityHeatmap from "@/components/ActivityHeatmap";
4+
import type { Invoice } from "@stellar-split/sdk";
5+
6+
const mockInvoices: Invoice[] = [
7+
{
8+
id: "inv-1",
9+
creator: "GCREATOR",
10+
recipients: [{ address: "GRECIPIENT", amount: 100_000_000n }],
11+
token: "CUSDC",
12+
deadline: 0,
13+
funded: 50_000_000n,
14+
status: "Pending",
15+
payments: [
16+
{
17+
payer: "GPAYER",
18+
amount: 50_000_000n,
19+
timestamp: Math.floor(Date.now() / 1000),
20+
},
21+
],
22+
},
23+
];
24+
25+
describe("ActivityHeatmap Tooltip Positioning", () => {
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
});
29+
30+
it("renders heatmap cells for activity data", () => {
31+
render(<ActivityHeatmap invoices={mockInvoices} />);
32+
const cells = screen.getAllByRole("button");
33+
expect(cells.length).toBeGreaterThan(0);
34+
});
35+
36+
it("shows tooltip on cell hover", async () => {
37+
render(<ActivityHeatmap invoices={mockInvoices} />);
38+
const cells = screen.getAllByRole("button");
39+
40+
fireEvent.mouseEnter(cells[0]);
41+
42+
await waitFor(() => {
43+
const tooltip = screen.queryByText(/payment/i);
44+
expect(tooltip || document.querySelector('[role="tooltip"]')).toBeTruthy();
45+
});
46+
});
47+
48+
it("positions tooltip to the right by default", () => {
49+
render(<ActivityHeatmap invoices={mockInvoices} />);
50+
const cells = screen.getAllByRole("button");
51+
52+
fireEvent.mouseEnter(cells[0]);
53+
54+
const tooltip = document.querySelector('[role="tooltip"]');
55+
if (tooltip) {
56+
const style = window.getComputedStyle(tooltip);
57+
expect(style.visibility).not.toBe("hidden");
58+
}
59+
});
60+
61+
it("flips tooltip to the left when right edge would overflow", () => {
62+
const container = render(<ActivityHeatmap invoices={mockInvoices} />);
63+
const cells = screen.getAllByRole("button");
64+
const rightEdgeCell = cells[cells.length - 1];
65+
66+
fireEvent.mouseEnter(rightEdgeCell);
67+
68+
const tooltip = document.querySelector('[role="tooltip"]');
69+
if (tooltip) {
70+
const tooltipRect = tooltip.getBoundingClientRect();
71+
expect(tooltipRect.right).toBeLessThanOrEqual(window.innerWidth);
72+
}
73+
});
74+
75+
it("flips tooltip downward when top edge would overflow", () => {
76+
render(<ActivityHeatmap invoices={mockInvoices} />);
77+
const cells = screen.getAllByRole("button");
78+
79+
fireEvent.mouseEnter(cells[0]);
80+
81+
const tooltip = document.querySelector('[role="tooltip"]');
82+
if (tooltip) {
83+
const tooltipRect = tooltip.getBoundingClientRect();
84+
expect(tooltipRect.bottom).toBeLessThanOrEqual(window.innerHeight);
85+
}
86+
});
87+
88+
it("keeps tooltip fully within viewport on all edges", async () => {
89+
render(<ActivityHeatmap invoices={mockInvoices} />);
90+
const cells = screen.getAllByRole("button");
91+
92+
for (const cell of cells) {
93+
fireEvent.mouseEnter(cell);
94+
95+
await waitFor(() => {
96+
const tooltip = document.querySelector('[role="tooltip"]');
97+
if (tooltip) {
98+
const rect = tooltip.getBoundingClientRect();
99+
expect(rect.left).toBeGreaterThanOrEqual(0);
100+
expect(rect.right).toBeLessThanOrEqual(window.innerWidth);
101+
expect(rect.top).toBeGreaterThanOrEqual(0);
102+
expect(rect.bottom).toBeLessThanOrEqual(window.innerHeight);
103+
}
104+
});
105+
}
106+
});
107+
108+
it("hides tooltip on mouse leave", async () => {
109+
render(<ActivityHeatmap invoices={mockInvoices} />);
110+
const cells = screen.getAllByRole("button");
111+
112+
fireEvent.mouseEnter(cells[0]);
113+
114+
const tooltip = document.querySelector('[role="tooltip"]');
115+
if (tooltip) {
116+
fireEvent.mouseLeave(cells[0]);
117+
118+
await waitFor(() => {
119+
expect(tooltip).not.toBeVisible();
120+
});
121+
}
122+
});
123+
124+
it("updates tooltip content for different cells", async () => {
125+
render(<ActivityHeatmap invoices={mockInvoices} />);
126+
const cells = screen.getAllByRole("button");
127+
128+
fireEvent.mouseEnter(cells[0]);
129+
130+
await waitFor(() => {
131+
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
132+
});
133+
134+
fireEvent.mouseLeave(cells[0]);
135+
fireEvent.mouseEnter(cells[1]);
136+
137+
await waitFor(() => {
138+
expect(document.querySelector('[role="tooltip"]')).toBeTruthy();
139+
});
140+
});
141+
142+
it("adjusts position when viewport is resized", () => {
143+
render(<ActivityHeatmap invoices={mockInvoices} />);
144+
const cells = screen.getAllByRole("button");
145+
146+
fireEvent.mouseEnter(cells[0]);
147+
148+
const originalTooltip = document.querySelector('[role="tooltip"]');
149+
const originalRect = originalTooltip?.getBoundingClientRect();
150+
151+
window.innerWidth = 400;
152+
fireEvent.resize(window);
153+
154+
const resizedTooltip = document.querySelector('[role="tooltip"]');
155+
const resizedRect = resizedTooltip?.getBoundingClientRect();
156+
157+
if (resizedTooltip) {
158+
expect(resizedRect?.right).toBeLessThanOrEqual(400);
159+
}
160+
});
161+
162+
it("shows tooltip content with activity count", async () => {
163+
render(<ActivityHeatmap invoices={mockInvoices} />);
164+
const cells = screen.getAllByRole("button");
165+
166+
fireEvent.mouseEnter(cells[0]);
167+
168+
await waitFor(() => {
169+
const tooltip = document.querySelector('[role="tooltip"]');
170+
expect(tooltip?.textContent).toMatch(/\d+ payment/i);
171+
});
172+
});
173+
174+
it("positions tooltip relative to heatmap container coordinates", () => {
175+
const { container } = render(<ActivityHeatmap invoices={mockInvoices} />);
176+
const heatmap = container.querySelector('svg');
177+
const cells = screen.getAllByRole("button");
178+
179+
fireEvent.mouseEnter(cells[0]);
180+
181+
const tooltip = document.querySelector('[role="tooltip"]');
182+
if (tooltip && heatmap) {
183+
const heatmapRect = heatmap.getBoundingClientRect();
184+
const tooltipRect = tooltip.getBoundingClientRect();
185+
186+
expect(tooltipRect.left).toBeGreaterThanOrEqual(heatmapRect.left - 100);
187+
expect(tooltipRect.right).toBeLessThanOrEqual(heatmapRect.right + 100);
188+
}
189+
});
190+
});

0 commit comments

Comments
 (0)