import { fireEvent, render, screen, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import type { ReactNode } from "react"; import { Board } from "./Board"; import { TooltipProvider } from "@/components/ui/tooltip"; import type { StateResponse, Topology } from "../api"; /** Wrap in TooltipProvider — the real app mounts this at the root. */ function renderWithProviders(ui: ReactNode) { return render({ui}); } const TOPO: Topology = { trees: [ { id: "core", label: "Core" }, { id: "sdlc", label: "SDLC" }, ], nodes: [ { id: "intake", label: "Intake", agent: "coordinator", tree: "core", kind: "phase", gated: false, }, { id: "clarify", label: "Clarify", agent: "coordinator", tree: "core", kind: "gate", gated: true, }, { id: "plan", label: "Plan", agent: "Claude", tree: "sdlc", kind: "phase", gated: false, }, ], edges: [], }; function makeState(): StateResponse { return { ok: true, error: null, generated_at: "now", db_path: "x", stages: [ { key: "intake", label: "Intake", agent: "coordinator", phases: ["intake"], gated: false, role_node: false, state: "idle", count: 1, tasks: [ { thread_id: "aaaa1111", short_id: "aaaa1111", task: "remediate CVE", current_phase: "intake", status: "active", waiting: false, waiting_since: null, }, ], }, { key: "clarify", label: "Clarify", agent: "coordinator", phases: ["clarify"], gated: true, role_node: false, state: "awaiting_human", count: 1, tasks: [ { thread_id: "bbbb2222", short_id: "bbbb2222", task: "fix login bug", current_phase: "clarify", status: "waiting_human", waiting: true, waiting_since: "2024-01-01T00:00:00Z", }, ], }, ], tasks: [ { thread_id: "aaaa1111", short_id: "aaaa1111", task: "remediate CVE", current_phase: "intake", status: "active", waiting: false, waiting_since: null, }, { thread_id: "bbbb2222", short_id: "bbbb2222", task: "fix login bug", current_phase: "clarify", status: "waiting_human", waiting: true, waiting_since: "2024-01-01T00:00:00Z", }, ], waiting: [], question_counts: {}, summary: { active: 1, parked: 0, waiting: 1, open_questions: 0, total: 2 }, budget: { available: false }, nodes: {}, }; } describe("Board", () => { it("renders a column per stage with correct labels", () => { renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); expect(screen.getByLabelText("Stage: Intake")).toBeInTheDocument(); expect(screen.getByLabelText("Stage: Clarify")).toBeInTheDocument(); }); it("shows task count badge in each column header", () => { renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); const intakeCol = screen.getByLabelText("Stage: Intake"); // Both columns have 1 task each expect(within(intakeCol).getByText("1")).toBeInTheDocument(); const clarifyCol = screen.getByLabelText("Stage: Clarify"); expect(within(clarifyCol).getByText("1")).toBeInTheDocument(); }); it("renders task descriptions inside columns", () => { renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); expect(screen.getByText("remediate CVE")).toBeInTheDocument(); expect(screen.getByText("fix login bug")).toBeInTheDocument(); }); it("shows a waiting affordance (clock icon) for a waiting task", () => { renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); // The Clock icon has aria-label="awaiting human" expect(screen.getByLabelText("awaiting human")).toBeInTheDocument(); }); it("shows lock icon on a gated stage column", () => { renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); expect(screen.getByLabelText("human gate")).toBeInTheDocument(); }); it("clicking a card fires onSelect with the thread_id", () => { const onSelect = vi.fn(); renderWithProviders( {}} />, ); fireEvent.click(screen.getByText("remediate CVE")); expect(onSelect).toHaveBeenCalledWith("aaaa1111"); }); it("selected card gets aria-selected=true", () => { renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); const cards = screen.getAllByRole("option", { selected: true }); expect(cards).toHaveLength(1); }); it("shows node filter chip when nodeFilter is set", () => { const onClearNodeFilter = vi.fn(); renderWithProviders( {}} onClearNodeFilter={onClearNodeFilter} />, ); const chip = screen.getByLabelText("Clear node filter: intake"); expect(chip).toBeInTheDocument(); fireEvent.click(chip); expect(onClearNodeFilter).toHaveBeenCalled(); }); it("falls back to deriving stages from tasks when stages is undefined", () => { const stateNoStages: StateResponse = { ...makeState(), stages: undefined, }; renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); // Should still show the tasks grouped by phase expect(screen.getByText("remediate CVE")).toBeInTheDocument(); expect(screen.getByText("fix login bug")).toBeInTheDocument(); }); it("renders null state gracefully with loading message", () => { renderWithProviders( {}} onClearNodeFilter={() => {}} />, ); expect(screen.getByText("Loading…")).toBeInTheDocument(); }); });