Files
fusion/packages/dashboard/app/components/__tests__/WorkflowColumnPanel.test.tsx
gsxdsm fcb7444e0e FN-6400: theme workflow column controls
Theme the workflow editor column panel with tokenized controls and coverage.

- Add tokenized button, empty-state, trait, agent selector, badge, and mode styles for workflow columns.
- Preserve disabled, hover, focus, stale, and mobile touch target states across column controls.
- Add WorkflowColumnPanel tests for themed classes, read-only states, empty states, and CSS token usage.

Files changed:
 .../app/components/WorkflowNodeEditor.css          | 192 ++++++++++++++++++++-
 .../__tests__/WorkflowColumnPanel.test.tsx         | 142 +++++++++++++++
 2 files changed, 333 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-6400

Fusion-Task-Lineage: 278f0db8-ef41-4278-8db6-8b17e27d5d71
2026-06-13 16:30:28 -07:00

143 lines
5.5 KiB
TypeScript

import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { cleanup, render, screen, waitFor, within } from "@testing-library/react";
import type { ComponentProps } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Agent, TraitCatalogEntry } from "../../api";
import { fetchAgents, fetchTraits } from "../../api";
import { WorkflowColumnPanel } from "../WorkflowColumnPanel";
vi.mock("../../api", () => ({
fetchAgents: vi.fn(),
fetchTraits: vi.fn(),
}));
const traitCatalog: TraitCatalogEntry[] = [
{ id: "intake", name: "Intake", builtin: true, flags: { intake: true } },
{ id: "complete", name: "Complete", builtin: true, flags: { complete: true } },
];
const agents = [
{ id: "agent-1", name: "Column Bot" },
] as Agent[];
function renderPanel({
columns = [
{ id: "triage", name: "Triage", traits: [{ trait: "intake" }], agent: { agentId: "agent-1", mode: "defer" as const } },
{ id: "done", name: "Done", traits: [{ trait: "complete" }] },
],
readOnly = false,
}: {
columns?: ComponentProps<typeof WorkflowColumnPanel>["columns"];
readOnly?: boolean;
} = {}) {
return render(
<WorkflowColumnPanel
columns={columns}
onChange={vi.fn()}
violations={[]}
readOnly={readOnly}
addToast={vi.fn()}
columnAgentsEnabled
/>,
);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function selectorMatches(selectorList: string, selector: string): boolean {
return selectorList
.split(",")
.map((part) => part.trim())
.some((part) => part === selector || part.startsWith(`${selector}:`));
}
function themedRuleBlocks(css: string, selector: string): string[] {
return [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
.filter((match) => selectorMatches(match[1], selector))
.map((match) => match[2]);
}
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("WorkflowColumnPanel", () => {
beforeEach(() => {
vi.mocked(fetchTraits).mockResolvedValue(traitCatalog);
vi.mocked(fetchAgents).mockResolvedValue(agents);
});
it("renders populated column controls with the themed column-panel classes", async () => {
const { container } = renderPanel();
expect(screen.getByRole("button", { name: /Add column/i })).toHaveClass("wf-column-add");
const triageRow = screen.getByTestId("wf-column-triage");
const moveButtons = within(triageRow).getAllByRole("button", { name: /Move column (up|down)/i });
expect(moveButtons).toHaveLength(2);
expect(moveButtons[0]).toHaveClass("wf-column-move");
expect(within(triageRow).getByRole("button", { name: /Remove column/i })).toHaveClass("wf-column-remove");
expect(screen.getByTestId("wf-column-agent-select-triage")).toHaveClass("wf-column-agent-select");
expect(screen.getByTestId("wf-column-agent-badge-triage")).toHaveClass("wf-column-agent-badge");
expect(container.querySelector(".wf-column-traits")).toBeTruthy();
expect(container.querySelector(".wf-column-agent-mode-option")).toBeTruthy();
await waitFor(() => expect(fetchTraits).toHaveBeenCalled());
await waitFor(() => expect(fetchAgents).toHaveBeenCalled());
});
it("renders the themed empty-state class when no columns exist", () => {
renderPanel({ columns: [] });
expect(screen.getByText(/No columns yet/i)).toHaveClass("wf-column-panel-empty");
});
it("keeps read-only controls visible with their themed classes and disabled state", () => {
renderPanel({ readOnly: true });
expect(screen.getByRole("button", { name: /Add column/i })).toHaveClass("wf-column-add");
expect(screen.getByRole("button", { name: /Add column/i })).toBeDisabled();
expect(screen.getAllByRole("button", { name: /Move column (up|down)/i })[0]).toHaveClass("wf-column-move");
expect(screen.getAllByRole("button", { name: /Move column (up|down)/i })[0]).toBeDisabled();
expect(screen.getAllByRole("button", { name: /Remove column/i })[0]).toHaveClass("wf-column-remove");
expect(screen.getAllByRole("button", { name: /Remove column/i })[0]).toBeDisabled();
expect(screen.getByTestId("wf-column-agent-select-triage")).toHaveClass("wf-column-agent-select");
expect(screen.getByTestId("wf-column-agent-select-triage")).toBeDisabled();
});
it("defines tokenized CSS rules for every column-panel selector themed by FN-6400", () => {
const css = readFileSync(resolve(__dirname, "../WorkflowNodeEditor.css"), "utf8");
const selectors = [
".wf-column-add",
".wf-column-move",
".wf-column-remove",
".wf-column-panel-empty",
".wf-column-panel-errors",
".wf-column-traits",
".wf-column-agent",
".wf-column-agent-label",
".wf-column-agent-select",
".wf-column-agent-badge",
".wf-column-agent-badge--stale",
".wf-column-agent-error",
".wf-column-agent-stale",
".wf-column-agent-mode",
".wf-column-agent-mode-option",
];
for (const selector of selectors) {
const blocks = themedRuleBlocks(css, selector);
expect(blocks.length, `${selector} should have a CSS rule`).toBeGreaterThan(0);
expect(blocks.some((block) => block.includes("var(--")), `${selector} should use design tokens`).toBe(true);
for (const block of blocks) {
expect(block, `${selector} should not use raw hex or rgba()`).not.toMatch(/#[0-9a-fA-F]{3,8}\b|rgba\(/);
}
}
expect(css.match(new RegExp(escapeRegExp(".wf-column-agent-select"), "g"))?.length ?? 0).toBeGreaterThan(0);
});
});