Skip to content
Closed
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
17 changes: 17 additions & 0 deletions src/components/SettlementTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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 {
Expand Down Expand Up @@ -31,7 +32,7 @@
it("shows the amount and fee totals for the visible rows", () => {
render(<SettlementTable settlements={settlements} />);

const totalRow = screen.getByText("Total (visible rows)").closest("tr");

Check failure on line 35 in src/components/SettlementTable.test.tsx

View workflow job for this annotation

GitHub Actions / build-test

src/components/SettlementTable.test.tsx > SettlementTable sorting > shows the amount and fee totals for the visible rows

TestingLibraryElementError: Found multiple elements with the text: Total (visible rows) Here are the matching elements: Ignored nodes: comments, script, style <td class="py-2 text-zinc-300" colspan="3" > Total (visible rows) </td> Ignored nodes: comments, script, style <div class="font-medium text-zinc-400 mb-1" > Total (visible rows) </div> (If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)). Ignored nodes: comments, script, style <body> <div> <table class="hidden sm:table w-full text-left text-sm" > <thead> <tr class="border-b border-zinc-800 text-zinc-400" > <th class="py-2 font-medium" > # </th> <th aria-sort="none" class="py-2 font-medium" > <button aria-label="Sort by Anchor" class="flex items-center gap-1 rounded-sm px-1 py-0.5 hover:text-zinc-200 focus-visible:border focus-visible:border-zinc-600 focus-visible:outline-none" type="button" > Anchor <span class="w-2 text-[10px] text-zinc-500" /> </button> </th> <th class="py-2 font-medium" > Asset </th> <th aria-sort="none" class="py-2 font-medium" > <button aria-label="Sort by Amount" class="flex items-center gap-1 rounded-sm px-1 py-0.5 hover:text-zinc-200 focus-visible:border focus-visible:border-zinc-600 focus-visible:outline-none" type="button" > Amount <span class="w-2 text-[10px] text-zinc-500" /> </button> </th> <th class="py-2 font-medium" > Fee </th> <th aria-sort="none" class="py-2 font-medium" > <button aria-label="Sort by Status" class="flex items-center gap-1 rounded-sm px-1 py-0.5 hover:text-zinc-200 focus-visible:border focus-visible:border-zinc-600 focus-visible:outline-none" type="button" > Status <span class="w-2 text-[10px] text-zinc-500" /> </button> </th> </tr> </thead> <tbody> <tr class="border-b border-zinc-900" > <td class="py-2 text-zinc-500" > <a class="hover:underline" href="/settlements/1" > 1 </a> </td> <td class="py-2 font-mono text-xs text-zinc-300" > a </td> <td class="py-2 font-mono text-zinc-100" > USDC </td> <td class="py-2 text-zinc-200" > 300 </td> <td class="py-2 text-zinc-400" > 3 </td> <td class="py-2" > <span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset bg-amber-500/15 text-amber-300 ring-amber-500/30" > Pending </span> <span aria-atomic="true" aria-live="polite" class="sr-only" /> </td> </tr> <tr class="border-b border-zinc-900" > <td class="py-2 text-zinc-500" > <a class="hover:underline" href="/settlements/2" > 2 </a> </td> <td class="py-2 font-mono text-xs text-zinc-300" > a
expect(totalRow).not.toBeNull();
expect(within(totalRow!).getAllByRole("cell").map((cell) => cell.textContent)).toEqual([
"Total (visible rows)",
Expand Down Expand Up @@ -77,3 +78,19 @@
expect(header).toHaveAttribute("aria-sort", "descending");
});
});

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) => {
const card = screen.getByText(`Settlement #${s.id}`).closest("div");
expect(card).toBeInTheDocument();
expect(within(card!).getByText(s.anchor)).toBeInTheDocument();

Check failure on line 90 in src/components/SettlementTable.test.tsx

View workflow job for this annotation

GitHub Actions / build-test

src/components/SettlementTable.test.tsx > SettlementTable mobile layout > renders a card for each settlement with correct data

TestingLibraryElementError: Unable to find an element with the text: a. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <div class="flex justify-between items-center mb-2" > <a class="text-zinc-500 hover:underline font-medium" href="/settlements/1" > Settlement # 1 </a> <span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset bg-amber-500/15 text-amber-300 ring-amber-500/30" > Pending </span> <span aria-atomic="true" aria-live="polite" class="sr-only" /> </div> ❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19 ❯ src/components/SettlementTable.test.tsx:90:28 ❯ src/components/SettlementTable.test.tsx:87:17
expect(within(card!).getByText(s.asset)).toBeInTheDocument();
expect(within(card!).getByText(formatAmount(s.amount))).toBeInTheDocument();
expect(within(card!).getByText(formatAmount(s.fee))).toBeInTheDocument();
});
});
});
211 changes: 134 additions & 77 deletions src/components/SettlementTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,86 +43,143 @@ export function SettlementTable({
);

return (
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-zinc-800 text-zinc-400">
<th className="py-2 font-medium">#</th>
<SortableHeader
label="Anchor"
sortKey="anchor"
sort={sort}
onSort={requestSort}
/>
<th className="py-2 font-medium">Asset</th>
<SortableHeader
label="Amount"
sortKey="amount"
sort={sort}
onSort={requestSort}
/>
<th className="py-2 font-medium">Fee</th>
<SortableHeader
label="Status"
sortKey="status"
sort={sort}
onSort={requestSort}
/>
{actionable ? <th className="py-2" /> : null}
</tr>
</thead>
<tbody>
<>
{/* Table view for larger screens */}
<table className="hidden sm:table w-full text-left text-sm">
<thead>
<tr className="border-b border-zinc-800 text-zinc-400">
<th className="py-2 font-medium">#</th>
<SortableHeader
label="Anchor"
sortKey="anchor"
sort={sort}
onSort={requestSort}
/>
<th className="py-2 font-medium">Asset</th>
<SortableHeader
label="Amount"
sortKey="amount"
sort={sort}
onSort={requestSort}
/>
<th className="py-2 font-medium">Fee</th>
<SortableHeader
label="Status"
sortKey="status"
sort={sort}
onSort={requestSort}
/>
{actionable ? <th className="py-2" /> : null}
</tr>
</thead>
<tbody>
{sorted.map((s) => (
<tr key={s.id} className="border-b border-zinc-900">
<td className="py-2 text-zinc-500">
<Link href={`/settlements/${s.id}`} className="hover:underline">
{s.id}
</Link>
</td>
<td className="py-2 font-mono text-xs text-zinc-300">{s.anchor}</td>
<td className="py-2 font-mono text-zinc-100">{s.asset}</td>
<td className="py-2 text-zinc-200">{formatAmount(s.amount)}</td>
<td className="py-2 text-zinc-400">{formatAmount(s.fee)}</td>
<td className="py-2">
<StatusBadge status={s.status} />
</td>
{actionable ? (
<td className="py-2 text-right">
{s.status === "pending" ? (
<span className="flex justify-end gap-2">
{onExecute ? (
<button
onClick={() => onExecute(s.id)}
className="rounded-md px-2 py-1 text-xs text-emerald-400 hover:text-emerald-300"
>
Execute
</button>
) : null}
{onCancel ? (
<button
onClick={() => onCancel(s.id)}
className="rounded-md px-2 py-1 text-xs text-red-400 hover:text-red-300"
>
Cancel
</button>
) : null}
</span>
) : null}
</td>
) : null}
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t border-zinc-800 font-medium">
<td className="py-2 text-zinc-300" colSpan={3}>
Total (visible rows)
</td>
<td className="py-2 text-zinc-100">{formatAmount(totals.amount)}</td>
<td className="py-2 text-zinc-300">{formatAmount(totals.fee)}</td>
<td />
{actionable ? <td /> : null}
</tr>
</tfoot>
</table>

{/* Card view for small screens */}
<div className="block sm:hidden space-y-4">
{sorted.map((s) => (
<tr key={s.id} className="border-b border-zinc-900">
<td className="py-2 text-zinc-500">
<Link href={`/settlements/${s.id}`} className="hover:underline">
{s.id}
<div key={s.id} data-testid="settlement-card" className="border border-zinc-800 rounded p-4 bg-zinc-900">
<div className="flex justify-between items-center mb-2">
<Link href={`/settlements/${s.id}`} className="text-zinc-500 hover:underline font-medium">
Settlement #{s.id}
</Link>
</td>
<td className="py-2 font-mono text-xs text-zinc-300">{s.anchor}</td>
<td className="py-2 font-mono text-zinc-100">{s.asset}</td>
<td className="py-2 text-zinc-200">{formatAmount(s.amount)}</td>
<td className="py-2 text-zinc-400">{formatAmount(s.fee)}</td>
<td className="py-2">
<StatusBadge status={s.status} />
</td>
{actionable ? (
<td className="py-2 text-right">
{s.status === "pending" ? (
<span className="flex justify-end gap-2">
{onExecute ? (
<button
onClick={() => onExecute(s.id)}
className="rounded-md px-2 py-1 text-xs text-emerald-400 hover:text-emerald-300"
>
Execute
</button>
) : null}
{onCancel ? (
<button
onClick={() => onCancel(s.id)}
className="rounded-md px-2 py-1 text-xs text-red-400 hover:text-red-300"
>
Cancel
</button>
) : null}
</span>
) : null}
</td>
) : null}
</tr>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="font-medium text-zinc-400">Anchor</div>
<div className="font-mono text-zinc-300">{s.anchor}</div>
<div className="font-medium text-zinc-400">Asset</div>
<div className="font-mono text-zinc-100">{s.asset}</div>
<div className="font-medium text-zinc-400">Amount</div>
<div className="text-zinc-200">{formatAmount(s.amount)}</div>
<div className="font-medium text-zinc-400">Fee</div>
<div className="text-zinc-400">{formatAmount(s.fee)}</div>
</div>
{actionable && s.status === "pending" && (
<div className="mt-2 flex justify-end gap-2">
{onExecute && (
<button
onClick={() => onExecute(s.id)}
className="rounded-md px-2 py-1 text-xs text-emerald-400 hover:text-emerald-300"
>
Execute
</button>
)}
{onCancel && (
<button
onClick={() => onCancel(s.id)}
className="rounded-md px-2 py-1 text-xs text-red-400 hover:text-red-300"
>
Cancel
</button>
)}
</div>
)}
</div>
))}
</tbody>
<tfoot>
<tr className="border-t border-zinc-800 font-medium">
<td className="py-2 text-zinc-300" colSpan={3}>
Total (visible rows)
</td>
<td className="py-2 text-zinc-100">{formatAmount(totals.amount)}</td>
<td className="py-2 text-zinc-300">{formatAmount(totals.fee)}</td>
<td />
{actionable ? <td /> : null}
</tr>
</tfoot>
</table>
{/* Totals card */}
<div className="border border-zinc-800 rounded p-4 bg-zinc-900">
<div className="font-medium text-zinc-400 mb-1">Total (visible rows)</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="font-medium text-zinc-400">Amount</div>
<div className="text-zinc-100">{formatAmount(totals.amount)}</div>
<div className="font-medium text-zinc-400">Fee</div>
<div className="text-zinc-300">{formatAmount(totals.fee)}</div>
</div>
</div>
</div>
</>
);
}
Loading