f88a7c7c4f
The 1,156 accepted acks were burying the 5 rejections — the operator had to scroll the whole table to find them. Two changes: 1. Sidebar '999 ACKs' nav item gets a warning-toned badge showing the total rejected segment count from aggregates.rejected_count (mirrors the Reconciliation unmatched badge pattern). One number, visible from any page. 2. Acks page table now sorts rejected rows to the top, then newest-id-first. All 5 rejections fit on page 1 since they're a tiny fraction of the total — no pagination needed. New files: - src/hooks/useAckStats.ts (lightweight aggregates-only fetch) - src/hooks/useAckStats.test.tsx (2 tests) - src/components/Sidebar.test.tsx (3 tests) Pre-existing baseline: 10 frontend failures (api.test.ts exportBatch837, tail-stream.test.ts acks/ta1_acks targeting, Inbox/InboxHeader copy). Unchanged.
96 lines
2.8 KiB
TypeScript
96 lines
2.8 KiB
TypeScript
// @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(
|
|
<QueryClientProvider client={qc}>
|
|
<MemoryRouter>
|
|
<Sidebar />
|
|
</MemoryRouter>
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|
|
}); |