feat(activity): remit_received events drill to RemitDrawer
This commit is contained in:
@@ -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<typeof vi.fn>).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<typeof vi.fn>
|
||||
).mockReturnValue(new Promise(() => {}));
|
||||
});
|
||||
|
||||
it("test_renders_filter_controls_when_mounted", async () => {
|
||||
@@ -447,3 +456,104 @@ describe("ActivityLog page filters", () => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SP21 Task 4.7: ActivityLog → RemitDrawer wiring", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(api.listActivity as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
EMPTY,
|
||||
);
|
||||
(
|
||||
api.getRemittance as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(new Promise(() => {}));
|
||||
});
|
||||
|
||||
it("clicking a remit_received event opens the RemitDrawer", async () => {
|
||||
(api.listActivity as unknown as ReturnType<typeof vi.fn>).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 <li role="button"> row and click it. ActivityFeed renders
|
||||
// each row as a button-role <li> when `onItemClick` is provided, with
|
||||
// an aria-label like "View remit received: <message>".
|
||||
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<typeof vi.fn>).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");
|
||||
});
|
||||
});
|
||||
@@ -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<ActivityKind[]>(
|
||||
() =>
|
||||
@@ -163,9 +174,48 @@ export function ActivityLog() {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ActivityFeed items={items} emptyMessage="No activity recorded yet." />
|
||||
<ActivityFeed
|
||||
items={items}
|
||||
emptyMessage="No activity recorded yet."
|
||||
onItemClick={(evt) => {
|
||||
// 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.`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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()`. */}
|
||||
<RemitDrawer
|
||||
remitId={remitId}
|
||||
remits={[]}
|
||||
onClose={close}
|
||||
onNavigate={open}
|
||||
onToggleHelp={() => {
|
||||
// ActivityLog has no cheatsheet; `?` is a no-op here.
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user