This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/web/src/components/Board.test.tsx
Adam Moussa 8530074551 ci(agent-team-web): wire Prettier + ESLint + Playwright and enable the CI steps (#65)
- Prettier 3.6.2: .prettierrc.json + .prettierignore + format/format:check
  scripts; formatted the web/ tree.
- ESLint 9 flat config (@eslint/js + typescript-eslint + react-hooks +
  react-refresh, eslint-config-prettier last so Prettier owns formatting);
  lint script; src/components/ui/** opts out of react-refresh (shadcn primitives
  co-export their cva variants). 0 problems.
- Playwright 1.61.1: playwright.config.ts (build+preview webServer) + an e2e
  Board-renders smoke against a mocked /api; test:e2e script. Reusable workflow
  installs chromium itself.
- ci-web.yaml: required-scripts -> format:check,lint,build,test,test:e2e;
  run-format-check/lint/e2e -> true; e2e-browser chromium.

All five scripts verified green locally.
2026-06-25 10:56:48 -04:00

296 lines
7.3 KiB
TypeScript

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(<TooltipProvider>{ui}</TooltipProvider>);
}
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(
<Board
state={makeState()}
topology={TOPO}
selectedId={null}
nodeFilter={null}
onSelect={() => {}}
onClearNodeFilter={() => {}}
/>,
);
expect(screen.getByLabelText("Stage: Intake")).toBeInTheDocument();
expect(screen.getByLabelText("Stage: Clarify")).toBeInTheDocument();
});
it("shows task count badge in each column header", () => {
renderWithProviders(
<Board
state={makeState()}
topology={TOPO}
selectedId={null}
nodeFilter={null}
onSelect={() => {}}
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(
<Board
state={makeState()}
topology={TOPO}
selectedId={null}
nodeFilter={null}
onSelect={() => {}}
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(
<Board
state={makeState()}
topology={TOPO}
selectedId={null}
nodeFilter={null}
onSelect={() => {}}
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(
<Board
state={makeState()}
topology={TOPO}
selectedId={null}
nodeFilter={null}
onSelect={() => {}}
onClearNodeFilter={() => {}}
/>,
);
expect(screen.getByLabelText("human gate")).toBeInTheDocument();
});
it("clicking a card fires onSelect with the thread_id", () => {
const onSelect = vi.fn();
renderWithProviders(
<Board
state={makeState()}
topology={TOPO}
selectedId={null}
nodeFilter={null}
onSelect={onSelect}
onClearNodeFilter={() => {}}
/>,
);
fireEvent.click(screen.getByText("remediate CVE"));
expect(onSelect).toHaveBeenCalledWith("aaaa1111");
});
it("selected card gets aria-selected=true", () => {
renderWithProviders(
<Board
state={makeState()}
topology={TOPO}
selectedId="aaaa1111"
nodeFilter={null}
onSelect={() => {}}
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(
<Board
state={makeState()}
topology={TOPO}
selectedId={null}
nodeFilter="intake"
onSelect={() => {}}
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(
<Board
state={stateNoStages}
topology={TOPO}
selectedId={null}
nodeFilter={null}
onSelect={() => {}}
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(
<Board
state={null}
topology={null}
selectedId={null}
nodeFilter={null}
onSelect={() => {}}
onClearNodeFilter={() => {}}
/>,
);
expect(screen.getByText("Loading…")).toBeInTheDocument();
});
});