diff --git a/src/components/ActivityFeed.test.tsx b/src/components/ActivityFeed.test.tsx new file mode 100644 index 0000000..6af07a1 --- /dev/null +++ b/src/components/ActivityFeed.test.tsx @@ -0,0 +1,50 @@ +// @vitest-environment happy-dom +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { ActivityFeed } from "./ActivityFeed"; +import type { Activity } from "@/types"; + +const baseActivity: Activity = { + id: "a-1", + kind: "claim_submitted", + message: "Claim ABC-1 submitted", + timestamp: "2026-06-20T12:00:00Z", +}; + +describe("ActivityFeed", () => { + it("renders the empty-state copy when items is empty", () => { + render(); + expect(screen.getByText("No activity yet.")).toBeTruthy(); + }); + + it("renders each item's message", () => { + render( + , + ); + expect(screen.getByText("Claim ABC-1 submitted")).toBeTruthy(); + expect(screen.getByText("Remit XYZ received")).toBeTruthy(); + }); + + it("does not crash on an unknown kind", () => { + // Backwards-compat guard: the backend may emit a kind the frontend + // hasn't been updated for yet (manual_match was the original case). + // The feed must fall back to a neutral icon instead of throwing. + const unknown = { + ...baseActivity, + id: "a-3", + message: "future_kind event arrived", + // Cast through `unknown` so the type-checker doesn't widen the + // `kind` literal away from the exhaustive union. + kind: "future_kind" as Activity["kind"], + }; + expect(() => render()).not.toThrow(); + expect(screen.getByText("future_kind event arrived")).toBeTruthy(); + }); +}); diff --git a/src/components/ActivityFeed.tsx b/src/components/ActivityFeed.tsx index 552417b..260afe7 100644 --- a/src/components/ActivityFeed.tsx +++ b/src/components/ActivityFeed.tsx @@ -1,9 +1,11 @@ import { Banknote, CheckCircle2, + Circle, FileText, Pill, UserPlus, + Wrench, XCircle, type LucideIcon, } from "lucide-react"; @@ -21,6 +23,22 @@ const kindConfig: Record< claim_denied: { icon: XCircle, tone: "text-destructive" }, remit_received: { icon: Pill, tone: "text-[hsl(var(--warning))]" }, provider_added: { icon: UserPlus, tone: "text-foreground" }, + // `manual_match` is emitted by the backend when an operator manually + // pairs a remit to a claim from the Reconciliation page — see + // `cyclone.store` `record_manual_match`. Wrench is the closest fit; + // we keep it neutral-tone so it doesn't read as a status. + manual_match: { icon: Wrench, tone: "text-muted-foreground" }, +}; + +// Defensive fallback: if the backend ever emits a kind this UI doesn't +// know about (e.g. a new kind landed server-side before the frontend +// picked it up), render a neutral icon instead of crashing the whole +// Activity Log page. The kindConfig map above stays exhaustive for +// known kinds so TypeScript still catches missing entries at compile +// time; this only protects the runtime path. +const FALLBACK_KIND: { icon: LucideIcon; tone: string } = { + icon: Circle, + tone: "text-muted-foreground", }; export function ActivityFeed({ @@ -40,7 +58,7 @@ export function ActivityFeed({ return (
    {items.map((a) => { - const cfg = kindConfig[a.kind]; + const cfg = kindConfig[a.kind] ?? FALLBACK_KIND; const Icon = cfg.icon; return (
  • { + proxy.on("proxyRes", (proxyRes) => { + proxyRes.headers["x-accel-buffering"] = "no"; + }); + }, + }, + }, }, });