Skip to content
Open
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
1 change: 1 addition & 0 deletions src/components/AnchorTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@ export function AnchorTable({
</table>
);
}

11 changes: 11 additions & 0 deletions src/components/AnchorsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ describe("AnchorsPanel", () => {
expect(screen.queryByText("Anchor B")).not.toBeInTheDocument();
});

it("exposes the filter and search toolbar as a labelled search landmark", async () => {
vi.mocked(fetchAnchors).mockResolvedValue([
{ id: "a", name: "Anchor A", registeredAt: "", active: true },
]);

renderPanel();
await screen.findByText("Anchor A");

expect(screen.getByRole("search", { name: /anchors/i })).toBeInTheDocument();
});

it("focuses the search box when / is pressed", async () => {
vi.mocked(fetchAnchors).mockResolvedValue([
{ id: "a", name: "Anchor A", registeredAt: "", active: true },
Expand Down
6 changes: 5 additions & 1 deletion src/components/AnchorsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ export function AnchorsPanel() {
) : (
<>
{state.data.length > 0 ? (
<div className="mb-3 flex flex-wrap items-center gap-2">
<div
role="search"
aria-label="Anchors search and filters"
className="mb-3 flex flex-wrap items-center gap-2"
>
{FILTERS.map((f, i) => (
<button
key={f.value}
Expand Down
1 change: 1 addition & 0 deletions src/components/PoolTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ export function PoolTable({ pools }: { pools: Pool[] }) {
</table>
);
}

36 changes: 35 additions & 1 deletion src/components/PoolsPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import { PoolsPanel } from "./PoolsPanel";
import { fetchPools } from "@/lib/api";

Expand Down Expand Up @@ -144,6 +144,40 @@ describe("PoolsPanel", () => {
expect(screen.getByLabelText("Search pools")).toHaveValue("");
});

it("debounces the search filter, updating the list only after the delay", async () => {
vi.useFakeTimers();
try {
vi.mocked(fetchPools).mockResolvedValue([
{ asset: "USDC", total: 1000, anchors: 2 },
{ asset: "EURC", total: 500, anchors: 1 },
]);

render(<PoolsPanel />);
await screen.findByText("USDC");
await screen.findByText("EURC");

fireEvent.change(screen.getByLabelText("Search pools"), {
target: { value: "usdc" },
});

expect(screen.getByLabelText("Search pools")).toHaveValue("usdc");
expect(screen.getByText("EURC")).toBeInTheDocument();

await act(async () => {
await vi.advanceTimersByTimeAsync(199);
});
expect(screen.getByText("EURC")).toBeInTheDocument();

await act(async () => {
await vi.advanceTimersByTimeAsync(1);
});
expect(screen.queryByText("EURC")).not.toBeInTheDocument();
expect(screen.getByText("USDC")).toBeInTheDocument();
} finally {
vi.useRealTimers();
}
});

it("shows an error message and retries on demand", async () => {
vi.mocked(fetchPools).mockRejectedValueOnce(new Error("network down"));

Expand Down
6 changes: 5 additions & 1 deletion src/components/PoolsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fetchPools } from "@/lib/api";
import { formatAmount } from "@/lib/format";
import { matchesQuery } from "@/lib/search";
import { useAsync } from "@/hooks/useAsync";
import { useDebouncedValue } from "@/hooks/useDebouncedValue";
import { useFocusShortcut } from "@/hooks/useFocusShortcut";
import { useQueryState } from "@/hooks/useQueryState";
import { Card } from "./Card";
Expand All @@ -14,11 +15,14 @@ import { PoolDistributionBar } from "./PoolDistributionBar";
import { TableSkeleton } from "./TableSkeleton";
import { EmptyState } from "./EmptyState";

const SEARCH_DEBOUNCE_MS = 200;

/** Client panel that loads liquidity pools and renders summary stats. */
export function PoolsPanel() {
const load = useCallback((signal: AbortSignal) => fetchPools(signal), []);
const { state, reload } = useAsync(load);
const [query, setQuery] = useQueryState("q", "");
const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
const searchRef = useRef<HTMLInputElement>(null);
useFocusShortcut("/", searchRef);

Expand Down Expand Up @@ -50,7 +54,7 @@ export function PoolsPanel() {
const totalLiquidity = state.data.reduce((sum, p) => sum + p.total, 0);
const positions = state.data.reduce((sum, p) => sum + p.anchors, 0);
const filteredPools = state.data.filter((pool) =>
matchesQuery([pool.asset], query),
matchesQuery([pool.asset], debouncedQuery),
);

return (
Expand Down
30 changes: 30 additions & 0 deletions src/components/SettlementForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,36 @@ describe("SettlementForm", () => {
expect(onSubmit).not.toHaveBeenCalled();
});

it("renders asset suggestions from availableLiquidity", () => {
const onSubmit = vi.fn();
render(
<SettlementForm
onSubmit={onSubmit}
availableLiquidity={{ USDC: 1000, BTC: 500, EURT: 250 }}
/>,
);

const input = screen.getByPlaceholderText("Asset");
expect(input).toHaveAttribute("list", "settlement-form-asset-list");
const datalist = document.getElementById("settlement-form-asset-list");
expect(datalist).toBeInTheDocument();
expect(datalist?.querySelectorAll("option")).toHaveLength(3);
expect(Array.from(datalist?.querySelectorAll("option") ?? []).map((option) => option.value)).toEqual([
"USDC",
"BTC",
"EURT",
]);
});

it("does not render a datalist when availableLiquidity is absent", () => {
const onSubmit = vi.fn();
render(<SettlementForm onSubmit={onSubmit} />);

const input = screen.getByPlaceholderText("Asset");
expect(input).not.toHaveAttribute("list");
expect(document.getElementById("settlement-form-asset-list")).not.toBeInTheDocument();
});

it("rejects amount exceeding available liquidity", () => {
const onSubmit = vi.fn();
render(<SettlementForm onSubmit={onSubmit} availableLiquidity={{ USDC: 100 }} />);
Expand Down
13 changes: 12 additions & 1 deletion src/components/SettlementForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const inputClass =
const invalidInputClass =
"w-full rounded-lg border border-red-500/60 bg-zinc-950 px-3 py-2 text-sm " +
"text-zinc-100 outline-none focus:border-red-500";
const ASSET_DATALIST_ID = "settlement-form-asset-list";

interface FormErrors {
anchor?: string;
Expand Down Expand Up @@ -55,6 +56,8 @@ export function SettlementForm({
const [errors, setErrors] = useState<FormErrors>({});

const anchorRef = useRef<HTMLInputElement>(null);
const assetOptions = Object.keys(availableLiquidity ?? {});
const assetListId = assetOptions.length > 0 ? ASSET_DATALIST_ID : undefined;
const anchorErrorId = "settlement-anchor-error";
const assetErrorId = "settlement-asset-error";
const amountErrorId = "settlement-amount-error";
Expand Down Expand Up @@ -113,10 +116,18 @@ export function SettlementForm({
if (errors.asset) setErrors((prev) => ({ ...prev, asset: undefined }));
}}
placeholder="Asset"
list={assetListId}
aria-invalid={Boolean(errors.asset)}
aria-describedby={errors.asset ? assetErrorId : undefined}
className={errors.asset ? invalidInputClass : inputClass}
/>
{assetOptions.length > 0 ? (
<datalist id={ASSET_DATALIST_ID}>
{assetOptions.map((assetOption) => (
<option key={assetOption} value={assetOption} />
))}
</datalist>
) : null}
{errors.asset ? (
<p id={assetErrorId} className="mt-1 text-xs text-red-400">
{errors.asset}
Expand Down Expand Up @@ -161,4 +172,4 @@ export function SettlementForm({
</div>
</form>
);
}
}
1 change: 1 addition & 0 deletions src/components/SettlementTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@ export function SettlementTable({
</table>
);
}

Loading