diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx new file mode 100644 index 0000000..184739f --- /dev/null +++ b/src/components/Sidebar.test.tsx @@ -0,0 +1,96 @@ +// @vitest-environment happy-dom +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { render, waitFor, cleanup } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +// Stub the auth hook so Sidebar doesn't try to read a token. +vi.mock("@/auth/useAuth", () => ({ + useAuth: () => ({ user: { id: 1, username: "admin", role: "admin" }, logout: vi.fn() }), +})); + +// Mutable state so each test can flip the rejected-acks count without +// re-importing the module. `vi.hoisted` ensures the mock factory closes +// over the same `state` object across calls. +const state = vi.hoisted(() => ({ + unmatchedClaims: 0, + unmatchedRemits: 0, + rejectedAcks: 0, +})); + +vi.mock("@/hooks/useReconciliation", () => ({ + useReconciliation: () => ({ + unmatched: { + data: { + claims: Array.from({ length: state.unmatchedClaims }), + remittances: Array.from({ length: state.unmatchedRemits }), + }, + }, + }), +})); + +vi.mock("@/hooks/useAckStats", () => ({ + useAckStats: () => ({ + data: state.rejectedAcks > 0 + ? { aggregates: { accepted: 1000, rejected: state.rejectedAcks, received: 1005 } } + : undefined, + }), +})); + +import { Sidebar } from "./Sidebar"; + +function renderSidebar() { + const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + + + , + ); +} + +describe("Sidebar — 999 ACKs rejected badge", () => { + beforeEach(() => { + cleanup(); + state.unmatchedClaims = 0; + state.unmatchedRemits = 0; + state.rejectedAcks = 0; + }); + + it("shows a '5' badge when 5 segments are rejected", async () => { + state.rejectedAcks = 5; + const { container } = renderSidebar(); + await waitFor(() => { + const badge = container.querySelector( + '[data-testid="sidebar-acks-rejected-badge"]', + ); + expect(badge).not.toBeNull(); + expect(badge?.textContent).toBe("5"); + }); + }); + + it("shows '99+' when rejected segments exceed 99", async () => { + state.rejectedAcks = 250; + const { container } = renderSidebar(); + await waitFor(() => { + const badge = container.querySelector( + '[data-testid="sidebar-acks-rejected-badge"]', + ); + expect(badge?.textContent).toBe("99+"); + }); + }); + + it("hides the badge when there are no rejected acks", async () => { + state.rejectedAcks = 0; + const { container } = renderSidebar(); + // Allow the synchronous mock to settle. + await waitFor(() => { + expect( + container.querySelector('[data-testid="sidebar-acks-rejected-badge"]'), + ).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 7d4be7c..fbcc07d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; import { useReconciliation } from "@/hooks/useReconciliation"; +import { useAckStats } from "@/hooks/useAckStats"; import { useAuth } from "@/auth/useAuth"; const nav = [ @@ -43,6 +44,13 @@ export function Sidebar() { (unmatched.data?.claims.length ?? 0) + (unmatched.data?.remittances.length ?? 0); + // 999 ACKs — total rejected segments across all ack files. Drives + // the badge on the "999 ACKs" nav item so the operator can see + // there are rejections to review from any page (mirrors the + // Reconciliation unmatched badge above). + const { data: ackStats } = useAckStats(); + const rejectedAckCount = ackStats?.aggregates.rejected ?? 0; + return (