diff --git a/src/pages/ActivityLog.test.tsx b/src/pages/ActivityLog.test.tsx index 1575fe1..0cb7e69 100644 --- a/src/pages/ActivityLog.test.tsx +++ b/src/pages/ActivityLog.test.tsx @@ -21,6 +21,7 @@ vi.mock("@/lib/api", () => ({ api: { isConfigured: true, listActivity: vi.fn(), + getRemittance: vi.fn(), }, })); @@ -158,6 +159,14 @@ describe("ActivityLog page filters", () => { (api.listActivity as unknown as ReturnType).mockResolvedValue( EMPTY, ); + // Default for the per-remit detail fetch — the drawer fetches + // this whenever `?remit=` is in the URL or `remit_received` + // events drill in. Return a never-resolving promise so the + // drawer stays in the loading state; the smoke tests only + // assert the drawer mounts. + ( + api.getRemittance as unknown as ReturnType + ).mockReturnValue(new Promise(() => {})); }); it("test_renders_filter_controls_when_mounted", async () => { @@ -446,4 +455,105 @@ describe("ActivityLog page filters", () => { unmount(); }); +}); + +describe("SP21 Task 4.7: ActivityLog → RemitDrawer wiring", () => { + beforeEach(() => { + vi.clearAllMocks(); + (api.listActivity as unknown as ReturnType).mockResolvedValue( + EMPTY, + ); + ( + api.getRemittance as unknown as ReturnType + ).mockReturnValue(new Promise(() => {})); + }); + + it("clicking a remit_received event opens the RemitDrawer", async () => { + (api.listActivity as unknown as ReturnType).mockResolvedValue({ + items: [ + { + id: "A-1", + kind: "remit_received", + message: "Remit PCN-1 received", + timestamp: "2026-06-20T10:00:00Z", + remittanceId: "REM-1", + }, + ], + total: 1, + returned: 1, + has_more: false, + }); + + const { unmount } = renderActivity(); + + // Wait for the row to render so we can click it. + await settle(() => + document.body.textContent?.includes("Remit PCN-1 received") ?? false, + ); + + // Drawer should not be mounted before the click. + expect(document.body.querySelector('[data-testid="remit-drawer"]')).toBeNull(); + + // Find the
  • row and click it. ActivityFeed renders + // each row as a button-role
  • when `onItemClick` is provided, with + // an aria-label like "View remit received: ". + const row = document.body.querySelector('[aria-label^="View remit received"]') as + | HTMLLIElement + | null; + expect(row).not.toBeNull(); + await act(async () => { + row!.click(); + }); + + // After click, the RemitDrawer should be mounted (with the skeleton, + // since the never-resolving getRemittance keeps it in loading state). + await settle( + () => document.body.querySelector('[data-testid="remit-drawer"]') !== null, + ); + expect( + document.body.querySelector('[data-testid="remit-drawer"]'), + ).not.toBeNull(); + + unmount(); + }); + + it("deep-link ?remit=ID opens the RemitDrawer on mount", async () => { + (api.listActivity as unknown as ReturnType).mockResolvedValue({ + items: [ + { + id: "A-1", + kind: "remit_received", + message: "Remit PCN-1 received", + timestamp: "2026-06-20T10:00:00Z", + remittanceId: "REM-7", + }, + ], + total: 1, + returned: 1, + has_more: false, + }); + + // The hook reads `?remit=` from `window.location.search`, so set + // BOTH the MemoryRouter initial entry (for the page's URL display) + // AND `window.happyDOM.setURL` (for the hook). + (window as unknown as { happyDOM: { setURL: (u: string) => void } }) + .happyDOM.setURL("http://localhost/activity?remit=REM-7"); + + const { unmount } = renderActivity({ + initialEntries: ["/activity?remit=REM-7"], + }); + + await settle( + () => document.body.querySelector('[data-testid="remit-drawer"]') !== null, + ); + expect( + document.body.querySelector('[data-testid="remit-drawer"]'), + ).not.toBeNull(); + + unmount(); + + // Reset URL so any subsequent tests see a clean /activity URL. + (window as unknown as { happyDOM: { setURL: (u: string) => void } }) + .happyDOM.setURL("http://localhost/activity"); + }); }); \ No newline at end of file diff --git a/src/pages/ActivityLog.tsx b/src/pages/ActivityLog.tsx index 360a7b1..7d9dde1 100644 --- a/src/pages/ActivityLog.tsx +++ b/src/pages/ActivityLog.tsx @@ -1,12 +1,16 @@ import { useCallback, useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useSearchParams, useNavigate } from "react-router-dom"; +import { toast } from "sonner"; import { useActivity } from "@/hooks/useActivity"; import { useTailStream } from "@/hooks/useTailStream"; import { useMergedTail } from "@/hooks/useMergedTail"; +import { useRemitDrawerUrlState } from "@/hooks/useRemitDrawerUrlState"; +import { eventKindToUrl } from "@/lib/event-routing"; import { TailStatusPill } from "@/components/TailStatusPill"; import { PageHeader } from "@/components/PageHeader"; import { ActivityFeed } from "@/components/ActivityFeed"; import { ActivityFilters, type SinceValue } from "@/components/ActivityFilters"; +import { RemitDrawer } from "@/components/RemitDrawer"; import { Skeleton } from "@/components/ui/skeleton"; import { EmptyState } from "@/components/ui/empty-state"; import { ErrorState } from "@/components/ui/error-state"; @@ -35,6 +39,13 @@ function useSinceIso(since: SinceValue): string | undefined { export function ActivityLog() { const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + // SP21 Phase 4 Task 4.7: `remit_received` events with a + // remittanceId drill into the RemitDrawer. Calling `open(id)` + // pushes `?remit=ID` onto the current URL (no navigation away + // from `/activity`), so the activity feed stays visible behind + // the drawer and the drawer portals in over it. + const { remitId, open, close } = useRemitDrawerUrlState(); const selectedKinds = useMemo( () => @@ -163,9 +174,48 @@ export function ActivityLog() { } /> ) : ( - + { + // SP21 Phase 4 Task 4.7: drill into the right surface + // based on event kind. `claim_*` and `provider_added` + // navigate away via `eventKindToUrl` (the Dashboard uses + // the same helper). `remit_received` events stay on + // `/activity` and open the RemitDrawer via `open(id)` + // — `eventKindToUrl` still returns `null` for that kind + // because cross-page navigation isn't the right UX here + // (we want to keep the activity feed as context behind + // the drawer). Anything else falls back to the + // "coming soon" toast so the click still gives feedback. + const url = eventKindToUrl(evt); + if (url) navigate(url); + else if (evt.kind === "remit_received" && evt.remittanceId) { + open(evt.remittanceId); + } else { + toast.info( + `Drill for ${evt.kind.replace(/_/g, " ")} coming in a later phase.`, + ); + } + }} + /> )} + + {/* SP21 Phase 4 Task 4.7: RemitDrawer mount. The activity + feed's `remit_received` rows drill into the drawer via + `open()`. `remits` is empty (the activity feed doesn't + keep a flat list of remits around), so j/k is a no-op + here — closing reverts the URL via `close()`. */} + { + // ActivityLog has no cheatsheet; `?` is a no-op here. + }} + /> ); }