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();
});
});