// @vitest-environment happy-dom (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { ActivityFeed } from "./ActivityFeed"; import type { Activity } from "@/types"; // happy-dom keeps `document.body` between tests; without cleanup, // `screen.getByRole("button")` finds buttons from earlier renders. afterEach(() => cleanup()); 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(); }); // ------------------------------------------------------------------- // SP21 Task 2.5: optional `onItemClick` prop makes each row a // drillable target. Default behavior (no handler) must stay unchanged. // ------------------------------------------------------------------- it("does not attach row-level click attrs when onItemClick is omitted", () => { render(); const li = screen.getByRole("listitem"); expect(li.getAttribute("role")).not.toBe("button"); expect(li.getAttribute("tabindex")).toBeNull(); expect(li.classList.contains("drillable")).toBe(false); }); it("attaches role/tabIndex/drillable class when onItemClick is provided", () => { render( {}} />, ); const li = screen.getByRole("button", { name: /View claim submitted/ }); expect(li.tagName).toBe("LI"); expect(li.getAttribute("tabindex")).toBe("0"); expect(li.classList.contains("drillable")).toBe(true); }); it("calls onItemClick with the event on click", () => { const onItemClick = vi.fn(); render( , ); const li = screen.getByRole("button"); fireEvent.click(li); expect(onItemClick).toHaveBeenCalledTimes(1); expect(onItemClick).toHaveBeenCalledWith(baseActivity); }); it("calls onItemClick on Enter keydown", () => { const onItemClick = vi.fn(); render( , ); const li = screen.getByRole("button"); fireEvent.keyDown(li, { key: "Enter" }); expect(onItemClick).toHaveBeenCalledTimes(1); expect(onItemClick).toHaveBeenCalledWith(baseActivity); }); it("calls onItemClick on Space keydown", () => { const onItemClick = vi.fn(); render( , ); fireEvent.keyDown(screen.getByRole("button"), { key: " " }); expect(onItemClick).toHaveBeenCalledTimes(1); }); it("does not call onItemClick for unrelated keys", () => { const onItemClick = vi.fn(); render( , ); fireEvent.keyDown(screen.getByRole("button"), { key: "a" }); expect(onItemClick).not.toHaveBeenCalled(); }); // Regression for the Task 2.4 event-bubbling pattern: clicks on a // drillable row must not bubble to a parent row click. Without // stopPropagation a Dashboard parent handler could fire alongside // the row click and corrupt history. it("stops click propagation so a parent row handler does not also fire", () => { const onItemClick = vi.fn(); const parentClick = vi.fn(); render(
, ); fireEvent.click(screen.getByRole("button")); expect(onItemClick).toHaveBeenCalledTimes(1); expect(parentClick).not.toHaveBeenCalled(); }); });