Files
cyclone/src/lib/event-routing.test.ts
T
Tyler 65d98cf674 feat(dashboard): Recent activity events route to entity by kind
SP21 Task 2.5 — the Dashboard's 'Recent activity' card now routes clicks
to the matching entity drawer / page by event kind:

  - claim_*       → /claims?claim=<id>  (drawer in Phase 5)
  - provider_added → /providers?provider=<npi>  (ProviderDrawer)
  - remit_received → toast 'coming in a later phase' (RemitDrawer in Phase 4)
  - anything else  → toast (manual_match, unknown kinds)

Implementation:
  - New src/lib/event-routing.ts with the eventKindToUrl() helper,
    plus a unit test covering all 6 + default branches.
  - src/components/ActivityFeed.tsx gains an optional onItemClick
    prop; when set, each row gets role='button', tabIndex=0, the
    drillable hover affordance (chevron + tint), and an Enter/Space
    keybinding. e.stopPropagation() is called before the handler so a
    parent row click can't double-fire (same fix as Task 2.4).
  - src/pages/Dashboard.tsx wires the handler on the 'Recent activity'
    card via eventKindToUrl + sonner toast for unhandled kinds.
  - Backend: CycloneStore.recent_activity() now exposes claimId and
    remittanceId on each row (read from ActivityEvent.claim_id /
    remittance_id) so the routing helper has the entity ids it needs.
  - The frontend Activity interface gains optional claimId /
    remittanceId fields; the in-memory sample data and the
    addClaim store action populate them so the dashboard works in
    both API-configured and sample-data modes.
2026-06-21 16:41:15 -06:00

87 lines
3.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { eventKindToUrl, type RoutableEvent } from "./event-routing";
// A minimal event factory keeps the assertions short and keeps the
// focus on what the helper actually inspects (kind + the relevant
// entity-id field for that kind).
function evt(overrides: Partial<RoutableEvent> & { kind: RoutableEvent["kind"] }): RoutableEvent {
return {
claimId: null,
remittanceId: null,
npi: undefined,
...overrides,
};
}
describe("eventKindToUrl", () => {
// ----- claim_* → /claims?claim=ID -------------------------------
it("routes claim_submitted to /claims?claim=ID", () => {
expect(
eventKindToUrl(evt({ kind: "claim_submitted", claimId: "CLM-1" })),
).toBe("/claims?claim=CLM-1");
});
it("routes claim_paid to /claims?claim=ID", () => {
expect(
eventKindToUrl(evt({ kind: "claim_paid", claimId: "CLM-2" })),
).toBe("/claims?claim=CLM-2");
});
it("routes claim_denied to /claims?claim=ID", () => {
expect(
eventKindToUrl(evt({ kind: "claim_denied", claimId: "CLM-3" })),
).toBe("/claims?claim=CLM-3");
});
it("routes claim_accepted to /claims?claim=ID", () => {
expect(
eventKindToUrl(evt({ kind: "claim_accepted", claimId: "CLM-4" })),
).toBe("/claims?claim=CLM-4");
});
it("percent-encodes the claim id when it contains special chars", () => {
expect(
eventKindToUrl(evt({ kind: "claim_paid", claimId: "CLM/1+2" })),
).toBe("/claims?claim=CLM%2F1%2B2");
});
it("returns null for claim_* when claimId is missing", () => {
expect(
eventKindToUrl(evt({ kind: "claim_paid", claimId: null })),
).toBeNull();
expect(
eventKindToUrl(evt({ kind: "claim_paid", claimId: undefined })),
).toBeNull();
});
// ----- remit_received → null (Phase 4) --------------------------
it("returns null for remit_received (Phase 4 drawer not built yet)", () => {
expect(
eventKindToUrl(evt({ kind: "remit_received", remittanceId: "REM-1" })),
).toBeNull();
});
// ----- provider_added → /providers?provider=NPI -----------------
it("routes provider_added to /providers?provider=NPI", () => {
expect(
eventKindToUrl(evt({ kind: "provider_added", npi: "1730187395" })),
).toBe("/providers?provider=1730187395");
});
it("returns null for provider_added when npi is missing", () => {
expect(eventKindToUrl(evt({ kind: "provider_added", npi: undefined }))).toBeNull();
});
// ----- default branch ------------------------------------------
it("returns null for unhandled kinds (manual_match)", () => {
expect(eventKindToUrl(evt({ kind: "manual_match" }))).toBeNull();
});
it("returns null for unknown kinds (defensive default)", () => {
// Cast through unknown so the type-checker doesn't widen the
// literal away from the exhaustive union.
const unknown = { kind: "future_kind" } as unknown as RoutableEvent;
expect(eventKindToUrl(unknown)).toBeNull();
});
});