Skip to content
Merged
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
49 changes: 43 additions & 6 deletions src/components/AnchorDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,19 @@
});

it("shows a not‑found message when the anchor returns 404", async () => {
vi.mocked(fetchAnchor).mockRejectedValue(new ApiRequestError(404, "NOT_FOUND", "Not found"));
vi.mocked(fetchAnchor).mockRejectedValue(
new ApiRequestError(404, "NOT_FOUND", "Not found"),
);

renderDetail();

// Expect the distinct not‑found text
expect(await screen.findByText(/anchor not found/i)).toBeInTheDocument();
const backLink = screen.getByRole("link", { name: /back to anchors/i });
const backLink = screen.getByRole("link", { name: /back to anchors/i });
expect(backLink).toBeInTheDocument();
expect(backLink).toHaveAttribute("href", "/anchors");
});


it("hides the deactivate action for an already-inactive anchor", async () => {
vi.mocked(fetchAnchor).mockResolvedValue({
id: "anchorA",
Expand Down Expand Up @@ -119,8 +120,40 @@
);
});

it("cancels deactivation when Cancel is clicked in the confirm dialog", async () => {
vi.mocked(fetchAnchor).mockResolvedValue({
id: "anchorA",
name: "Anchor A",
registeredAt: "",
active: true,
});

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

// Click "Deactivate" to open the dialog
fireEvent.click(screen.getByRole("button", { name: "Deactivate" }));
expect(deregisterAnchor).not.toHaveBeenCalled();

// Dialog should be open
const dialog = screen.getByRole("alertdialog");
expect(dialog).toBeInTheDocument();

// Click the Cancel button in the dialog
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));

// Verify deregisterAnchor was not called
expect(deregisterAnchor).not.toHaveBeenCalled();

// Dialog should be closed (either not in the document or queryByRole returns null)
expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument();

// Anchor status is unchanged (remains Active)
expect(screen.getByText("Active")).toBeInTheDocument();
});

it("deactivates an anchor without flashing a loading spinner", async () => {
let resolveFetch: (value: any) => void = () => {};

Check failure on line 156 in src/components/AnchorDetail.test.tsx

View workflow job for this annotation

GitHub Actions / build-test

Unexpected any. Specify a different type
const fetchPromise = new Promise((resolve) => {
resolveFetch = resolve;
});
Expand All @@ -132,7 +165,7 @@
registeredAt: "",
active: true,
})
.mockReturnValueOnce(fetchPromise as any);

Check failure on line 168 in src/components/AnchorDetail.test.tsx

View workflow job for this annotation

GitHub Actions / build-test

Unexpected any. Specify a different type

vi.mocked(deregisterAnchor).mockResolvedValue({
id: "anchorA",
Expand Down Expand Up @@ -167,7 +200,9 @@
});

// Wait for the newly fetched status to be reflected
await waitFor(() => expect(screen.getByText("Inactive")).toBeInTheDocument());
await waitFor(() =>
expect(screen.getByText("Inactive")).toBeInTheDocument(),
);
});

it("handles deactivation error gracefully", async () => {
Expand All @@ -177,7 +212,9 @@
registeredAt: "",
active: true,
});
vi.mocked(deregisterAnchor).mockRejectedValue(new Error("Deactivation failed test error"));
vi.mocked(deregisterAnchor).mockRejectedValue(
new Error("Deactivation failed test error"),
);

renderDetail();
expect(await screen.findByText("Anchor A")).toBeInTheDocument();
Expand All @@ -189,7 +226,7 @@
await waitFor(() =>
expect(deregisterAnchor).toHaveBeenCalledWith("anchorA"),
);

// We expect a toast with the error message but we don't strictly test the toast component itself here,
// just the error branch in the catch block of deactivate()
});
Expand Down
11 changes: 10 additions & 1 deletion src/components/AnchorsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
import {
fetchAnchors,
registerAnchor,
Expand Down Expand Up @@ -109,6 +109,15 @@ export function AnchorsPanel() {
const [rawStatus, setStatus] = useQueryState("status", "all");
const filter: StatusFilter = isStatusFilter(rawStatus) ? rawStatus : "all";

const [sortParam, setSortParam] = useQueryState("sort", "");
const [dirParam, setDirParam] = useQueryState("dir", "");

const initialSort = useMemo<SortState<SortKey> | null>(() => {
if (!sortParam || !VALID_SORT_KEYS.has(sortParam)) return null;
const direction: SortDirection = dirParam === "desc" ? "desc" : "asc";
return { key: sortParam as SortKey, direction };
}, [sortParam, dirParam]);

// When the URL carries an invalid status value, correct it to the effective
// fallback ("all") so the address bar always reflects what is displayed.
useEffect(() => {
Expand Down
7 changes: 1 addition & 6 deletions src/components/PoolsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@

const SEARCH_DEBOUNCE_MS = 200;

/**
* Optional props allowing a parent (e.g. DashboardContent) to supply pool data
* it already fetched, avoiding a duplicate request. When omitted, the panel
* fetches its own data.
*/
export interface PoolsPanelProps {
interface PoolsPanelProps {
pools?: Pool[];
isLoading?: boolean;
error?: string;
Expand Down Expand Up @@ -54,7 +49,7 @@
useFocusShortcut("/", searchRef);

// Use external data if provided, otherwise use internal fetch state
const pools =

Check warning on line 52 in src/components/PoolsPanel.tsx

View workflow job for this annotation

GitHub Actions / build-test

The 'pools' conditional could make the dependencies of useMemo Hook (at line 80) change on every render. To fix this, wrap the initialization of 'pools' in its own useMemo() Hook

Check warning on line 52 in src/components/PoolsPanel.tsx

View workflow job for this annotation

GitHub Actions / build-test

The 'pools' conditional could make the dependencies of useMemo Hook (at line 76) change on every render. To fix this, wrap the initialization of 'pools' in its own useMemo() Hook

Check warning on line 52 in src/components/PoolsPanel.tsx

View workflow job for this annotation

GitHub Actions / build-test

The 'pools' conditional could make the dependencies of useMemo Hook (at line 72) change on every render. To fix this, wrap the initialization of 'pools' in its own useMemo() Hook
externalPools !== undefined
? externalPools
: state.status === "ready"
Expand Down
56 changes: 55 additions & 1 deletion src/components/SettlementDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@

// Expect the distinct not‑found text
expect(await screen.findByText(/settlement not found/i)).toBeInTheDocument();
const backLink = screen.getByRole('link', { name: /back to settlements/i });
const backLink = screen.getByRole('link', { name: /back to settlements/i });
expect(backLink).toBeInTheDocument();
expect(backLink).toHaveAttribute('href', '/settlements');
});
Expand Down Expand Up @@ -142,7 +142,7 @@
const pendingPromise = new Promise<void>((res) => {
resolveAction = res;
});
vi.mocked(executeSettlement).mockReturnValue(pendingPromise as any);

Check failure on line 145 in src/components/SettlementDetail.test.tsx

View workflow job for this annotation

GitHub Actions / build-test

Unexpected any. Specify a different type

renderDetail();
await screen.findByText("Settlement #1");
Expand All @@ -161,4 +161,58 @@
expect(cancelBtn).not.toBeDisabled();
});

it("cancels cancellation when Keep settlement is clicked in the confirm dialog", async () => {
vi.mocked(fetchSettlement).mockResolvedValue(pending);

renderDetail();
await screen.findByText("Settlement #1");

// Click "Cancel" to open the dialog
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(cancelSettlement).not.toHaveBeenCalled();

// Dialog should be open
const dialog = screen.getByRole("alertdialog");
expect(dialog).toBeInTheDocument();

// Click the "Keep settlement" button in the dialog
fireEvent.click(within(dialog).getByRole("button", { name: "Keep settlement" }));

// Verify cancelSettlement was not called
expect(cancelSettlement).not.toHaveBeenCalled();

// Dialog should be closed
expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument();

// Settlement status is unchanged (remains Pending)
expect(screen.getByText("Pending")).toBeInTheDocument();
});

it("confirms before cancelling a pending settlement", async () => {
vi.mocked(fetchSettlement).mockResolvedValue(pending);
vi.mocked(cancelSettlement).mockResolvedValue({
...pending,
status: "cancelled",
});

renderDetail();
await screen.findByText("Settlement #1");

// Click "Cancel" to open the dialog
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(cancelSettlement).not.toHaveBeenCalled();

// Dialog should be open
const dialog = screen.getByRole("alertdialog");
expect(dialog).toBeInTheDocument();

// Click the "Cancel settlement" button in the dialog to confirm
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel settlement" }));

// Verify cancelSettlement was called
await waitFor(() =>
expect(cancelSettlement).toHaveBeenCalledWith(1),
);
});

});
Loading