forked from AnchorNet-Org/AnchorNet-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSettlementTable.test.tsx
More file actions
273 lines (222 loc) · 10.7 KB
/
Copy pathSettlementTable.test.tsx
File metadata and controls
273 lines (222 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, within } from "@testing-library/react";
import { SettlementTable } from "./SettlementTable";
import { Settlement } from "@/lib/types";
import { formatAmount } from "@/lib/format";
function settlement(overrides: Partial<Settlement>): Settlement {
return {
id: 1,
anchor: "a",
asset: "USDC",
amount: 0,
fee: 0,
status: "pending",
createdAt: "",
...overrides,
};
}
const settlements: Settlement[] = [
settlement({ id: 1, amount: 300, fee: 3 }),
settlement({ id: 2, amount: 100, fee: 1 }),
settlement({ id: 3, amount: 200, fee: 2 }),
];
function amountCells() {
const rows = within(document.querySelector("tbody")!).getAllByRole("row");
return rows.map((row) => within(row).getAllByRole("cell")[3].textContent);
}
function statusCells() {
const rows = within(document.querySelector("tbody")!).getAllByRole("row");
return rows.map((row) => within(row).getAllByRole("cell")[5].textContent);
}
describe("SettlementTable sorting", () => {
it("makes the full first cell a settlement detail link", () => {
render(<SettlementTable settlements={[settlements[0]]} />);
const row = within(document.querySelector("tbody")!).getByRole("row");
const firstCell = within(row).getAllByRole("cell")[0];
const link = within(firstCell).getByRole("link", { name: "1" });
expect(link).toHaveAttribute("href", "/settlements/1");
expect(link).toHaveClass("block", "hover:underline");
});
it("keeps pending-row actions independently clickable", () => {
const onExecute = vi.fn();
const onCancel = vi.fn();
render(
<SettlementTable
settlements={[settlements[0]]}
onExecute={onExecute}
onCancel={onCancel}
/>,
);
const table = within(document.querySelector("table")!);
fireEvent.click(table.getByRole("button", { name: "Execute" }));
fireEvent.click(table.getByRole("button", { name: "Cancel" }));
expect(onExecute).toHaveBeenCalledWith(1);
expect(onCancel).toHaveBeenCalledWith(1);
});
it("shows the amount and fee totals for the visible rows", () => {
render(<SettlementTable settlements={settlements} />);
const totalRow = screen.getByRole("row", { name: /Total \(visible rows\)/ });
expect(within(totalRow).getAllByRole("cell").map((cell) => cell.textContent)).toEqual([
"Total (visible rows)",
"600",
"6",
"",
]);
});
it("renders rows in their original order by default", () => {
render(<SettlementTable settlements={settlements} />);
expect(amountCells()).toEqual(["300", "100", "200"]);
});
it("sorts ascending by amount on the first click", () => {
render(<SettlementTable settlements={settlements} />);
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(amountCells()).toEqual(["100", "200", "300"]);
});
it("resets an active sort directly to the original row order", () => {
render(<SettlementTable settlements={settlements} />);
const header = screen.getByLabelText("Sort by Amount").closest("th");
expect(screen.queryByRole("button", { name: "Reset sort" })).not.toBeInTheDocument();
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(amountCells()).toEqual(["100", "200", "300"]);
fireEvent.click(screen.getByRole("button", { name: "Reset sort" }));
expect(amountCells()).toEqual(["300", "100", "200"]);
expect(header).toHaveAttribute("aria-sort", "none");
});
it("sorts descending by amount on a second click", () => {
render(<SettlementTable settlements={settlements} />);
fireEvent.click(screen.getByLabelText("Sort by Amount"));
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(amountCells()).toEqual(["300", "200", "100"]);
});
it("sorts by status following canonical lifecycle order (pending -> executed -> cancelled)", () => {
const statusSettlements: Settlement[] = [
settlement({ id: 1, status: "executed" }),
settlement({ id: 2, status: "cancelled" }),
settlement({ id: 3, status: "pending" }),
];
render(<SettlementTable settlements={statusSettlements} />);
fireEvent.click(screen.getByLabelText("Sort by Status"));
expect(statusCells()).toEqual(["Pending", "Executed", "Cancelled"]);
fireEvent.click(screen.getByLabelText("Sort by Status"));
expect(statusCells()).toEqual(["Cancelled", "Executed", "Pending"]);
});
it("applies a visible focus style to sortable header buttons", () => {
render(<SettlementTable settlements={settlements} />);
const sortButton = screen.getByLabelText("Sort by Amount");
expect(sortButton).toHaveClass("focus-visible:border", "focus-visible:border-zinc-600");
});
it("exposes the current sort direction via aria-sort", () => {
render(<SettlementTable settlements={settlements} />);
const header = screen.getByLabelText("Sort by Amount").closest("th");
expect(header).toHaveAttribute("aria-sort", "none");
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(header).toHaveAttribute("aria-sort", "ascending");
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(header).toHaveAttribute("aria-sort", "descending");
});
it("announces the new sort key and direction via a live region when clicked", () => {
const { container } = render(<SettlementTable settlements={settlements} />);
// No announcement on initial render
const liveRegion = container.querySelector('[aria-live="polite"].sr-only');
expect(liveRegion).toBeInTheDocument();
expect(liveRegion).toHaveTextContent("");
// Click to sort by Amount
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(liveRegion).toHaveTextContent("Sorted by Amount, ascending");
// Click again to cycle to descending
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(liveRegion).toHaveTextContent("Sorted by Amount, descending");
// Click again to cycle to unsorted
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(liveRegion).toHaveTextContent("Sorting cleared");
});
describe("initial aria-sort accessibility", () => {
it("announces all column headers as aria-sort=none on initial render", () => {
render(<SettlementTable settlements={settlements} />);
const anchorHeader = screen.getByLabelText("Sort by Anchor").closest("th");
const amountHeader = screen.getByLabelText("Sort by Amount").closest("th");
const statusHeader = screen.getByLabelText("Sort by Status").closest("th");
expect(anchorHeader).toHaveAttribute("aria-sort", "none");
expect(amountHeader).toHaveAttribute("aria-sort", "none");
expect(statusHeader).toHaveAttribute("aria-sort", "none");
});
it("updates aria-sort to ascending on first column click", () => {
render(<SettlementTable settlements={settlements} />);
const header = screen.getByLabelText("Sort by Amount").closest("th");
expect(header).toHaveAttribute("aria-sort", "none");
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(header).toHaveAttribute("aria-sort", "ascending");
});
it("updates aria-sort to descending on second column click", () => {
render(<SettlementTable settlements={settlements} />);
const header = screen.getByLabelText("Sort by Amount").closest("th");
fireEvent.click(screen.getByLabelText("Sort by Amount"));
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(header).toHaveAttribute("aria-sort", "descending");
});
it("resets aria-sort to none on third column click", () => {
render(<SettlementTable settlements={settlements} />);
const header = screen.getByLabelText("Sort by Amount").closest("th");
fireEvent.click(screen.getByLabelText("Sort by Amount"));
fireEvent.click(screen.getByLabelText("Sort by Amount"));
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(header).toHaveAttribute("aria-sort", "none");
});
it("switches aria-sort between columns when clicking different headers", () => {
render(<SettlementTable settlements={settlements} />);
const anchorHeader = screen.getByLabelText("Sort by Anchor").closest("th");
const amountHeader = screen.getByLabelText("Sort by Amount").closest("th");
// Click Anchor (first sort)
fireEvent.click(screen.getByLabelText("Sort by Anchor"));
expect(anchorHeader).toHaveAttribute("aria-sort", "ascending");
expect(amountHeader).toHaveAttribute("aria-sort", "none");
// Click Amount (switches to that column, ascending)
fireEvent.click(screen.getByLabelText("Sort by Amount"));
expect(anchorHeader).toHaveAttribute("aria-sort", "none");
expect(amountHeader).toHaveAttribute("aria-sort", "ascending");
});
});
});
describe("SettlementTable per-row in-flight actions", () => {
it("disables Execute and Cancel buttons only for pending settlement IDs", () => {
const onExecute = () => {};
const onCancel = () => {};
render(
<SettlementTable
settlements={settlements}
onExecute={onExecute}
onCancel={onCancel}
pendingIds={new Set([2])}
/>,
);
const rows = within(document.querySelector("tbody")!).getAllByRole("row");
const row1Execute = within(rows[0]).getByRole("button", { name: "Execute" });
const row1Cancel = within(rows[0]).getByRole("button", { name: "Cancel" });
const row2Execute = within(rows[1]).getByRole("button", { name: "Execute" });
const row2Cancel = within(rows[1]).getByRole("button", { name: "Cancel" });
const row3Execute = within(rows[2]).getByRole("button", { name: "Execute" });
const row3Cancel = within(rows[2]).getByRole("button", { name: "Cancel" });
expect(row1Execute).not.toBeDisabled();
expect(row1Cancel).not.toBeDisabled();
expect(row2Execute).toBeDisabled();
expect(row2Cancel).toBeDisabled();
expect(row3Execute).not.toBeDisabled();
expect(row3Cancel).not.toBeDisabled();
});
});
describe("SettlementTable mobile layout", () => {
it("renders a card for each settlement with correct data", () => {
render(<SettlementTable settlements={settlements} />);
const cards = screen.getAllByTestId("settlement-card");
expect(cards).toHaveLength(settlements.length);
settlements.forEach((s, index) => {
const card = cards[index];
expect(card).toBeInTheDocument();
expect(within(card).getByRole("link", { name: `Settlement #${s.id}` })).toBeInTheDocument();
expect(within(card).getByText(s.anchor)).toBeInTheDocument();
expect(within(card).getByText(s.asset)).toBeInTheDocument();
expect(within(card).getByText(formatAmount(s.amount))).toBeInTheDocument();
expect(within(card).getByText(formatAmount(s.fee))).toBeInTheDocument();
});
});
});