feat(FN-849): normalize agent UI theme consistency and simplify workflow step API
- Fix AgentListModal create-row and control styling to use token-based design tokens - Normalize AgentDetailView theme consistency with shared token-based styling - Simplify WorkflowStepManager by removing server-side CRUD, using local state only - Remove unused store methods and types (workflowStep CRUD) from core package - Remove server-side workflow step API routes and their tests - Update dashboard README with shared token-based styling notes - Add tests for AgentDetailView and extend AgentListModal tests
This commit is contained in:
@@ -97,14 +97,14 @@ A persistent footer status bar at the bottom of the dashboard displays real-time
|
||||
- `GET /api/executor/stats` - Returns `globalPause`, `enginePaused`, `maxConcurrent`, and `lastActivityAt` for state derivation. Column-based counts (running, blocked, stuck, queued, in-review) are derived client-side from the shared task list.
|
||||
|
||||
### Agents View
|
||||
Manage AI agents with a dedicated control surface accessible from the main dashboard navigation.
|
||||
Manage AI agents with a dedicated control surface accessible from the main dashboard navigation. All agent surfaces (AgentsView, AgentListModal, AgentDetailView) share consistent token-based styling using dashboard design tokens (`--surface`, `--card`, `--border`, `--text`, `--color-success`, `--color-error`, etc.) and locally defined state color tokens (`--state-idle-*`, `--state-active-*`, `--state-paused-*`, `--state-error-*`) for theme-aware rendering.
|
||||
|
||||
**Features**:
|
||||
- **State Filter**: Styled dropdown to filter agents by state (All States, Idle, Active, Paused, Terminated) with icon and consistent dashboard styling using design tokens
|
||||
- **State Filter**: Styled dropdown to filter agents by state (All States, Idle, Active, Paused, Terminated) with Filter icon, aria-label, and consistent dashboard styling using design tokens (`--radius-sm`, `--border`, `--bg`, `--focus-ring`)
|
||||
- **View Modes**: Board (compact grid) and list (detailed card) layouts, persisted to localStorage
|
||||
- **Agent CRUD**: Create agents with name and role (create form uses dashboard radius tokens for consistent styling), change state, update roles inline, delete terminated agents
|
||||
- **Health Monitoring**: Heartbeat-based health status (Healthy, Unresponsive, Starting, Paused, Terminated)
|
||||
- **Agent Detail**: Click any agent card to open a detail modal with full agent information. The **Settings** tab now includes **editable advanced settings** (heartbeat interval, max retries, task timeout, log level) persisted through `agent.metadata`. Empty fields revert to system defaults, invalid values block save with inline error messages
|
||||
- **Agent CRUD**: Create agents with name and role (create form uses `var(--radius-sm)` and `var(--bg-secondary)` tokens for consistent theme-aware styling), change state, update roles inline, delete terminated agents
|
||||
- **Health Monitoring**: Heartbeat-based health status (Healthy, Unresponsive, Starting, Paused, Terminated) using CSS variable references for theme consistency
|
||||
- **Agent Detail**: Click any agent card to open a detail modal with full agent information. The modal uses component-local token aliases (`--bg-primary`, `--accent`, `--text-primary`, `--bg-hover`) mapped to global tokens (`--surface`, `--todo`, `--text`, `--card-hover`) for theme consistency. The **Settings** tab now includes **editable advanced settings** (heartbeat interval, max retries, task timeout, log level) persisted through `agent.metadata`. Empty fields revert to system defaults, invalid values block save with inline error messages
|
||||
|
||||
### Interactive Terminal
|
||||
Access a fully functional PTY (pseudo-terminal) shell directly from the dashboard. Click the terminal icon in the header to open the interactive terminal modal.
|
||||
|
||||
@@ -56,17 +56,17 @@ const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [
|
||||
];
|
||||
|
||||
const STATE_COLORS: Record<AgentState, { bg: string; text: string; border: string }> = {
|
||||
idle: { bg: "rgba(139, 148, 158, 0.15)", text: "#8b949e", border: "#8b949e" },
|
||||
active: { bg: "rgba(46, 160, 67, 0.15)", text: "#3fb950", border: "#3fb950" },
|
||||
paused: { bg: "rgba(227, 179, 65, 0.15)", text: "#e3b541", border: "#e3b541" },
|
||||
terminated: { bg: "rgba(248, 81, 73, 0.15)", text: "#f85149", border: "#f85149" },
|
||||
idle: { bg: "var(--state-idle-bg)", text: "var(--state-idle-text)", border: "var(--state-idle-border)" },
|
||||
active: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" },
|
||||
paused: { bg: "var(--state-paused-bg)", text: "var(--state-paused-text)", border: "var(--state-paused-border)" },
|
||||
terminated: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" },
|
||||
};
|
||||
|
||||
const RUN_STATUS_ICONS: Record<string, { icon: typeof CheckCircle; color: string }> = {
|
||||
completed: { icon: CheckCircle, color: "text-green-500" },
|
||||
failed: { icon: XCircle, color: "text-red-500" },
|
||||
active: { icon: Loader2, color: "text-cyan-500 animate-spin" },
|
||||
terminated: { icon: Square, color: "text-gray-500" },
|
||||
completed: { icon: CheckCircle, color: "var(--color-success, #3fb950)" },
|
||||
failed: { icon: XCircle, color: "var(--color-error, #f85149)" },
|
||||
active: { icon: Loader2, color: "var(--in-progress, #bc8cff)" },
|
||||
terminated: { icon: Square, color: "var(--text-muted, #8b949e)" },
|
||||
};
|
||||
|
||||
export function AgentDetailView({ agentId, projectId, onClose, addToast }: AgentDetailViewProps) {
|
||||
@@ -179,23 +179,23 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast }: Agent
|
||||
};
|
||||
|
||||
const getHealthStatus = () => {
|
||||
if (!agent) return { label: "Unknown", color: "#888" };
|
||||
if (!agent) return { label: "Unknown", color: "var(--text-muted, #8b949e)" };
|
||||
if (agent.state === "terminated") {
|
||||
return { label: "Terminated", color: "#f85149" };
|
||||
return { label: "Terminated", color: "var(--state-error-text, #f85149)" };
|
||||
}
|
||||
if (agent.state === "paused") {
|
||||
return { label: "Paused", color: "#e3b541" };
|
||||
return { label: "Paused", color: "var(--state-paused-text, #e3b541)" };
|
||||
}
|
||||
if (!agent.lastHeartbeatAt) {
|
||||
return { label: agent.state === "active" ? "Starting..." : "Idle", color: "#8b949e" };
|
||||
return { label: agent.state === "active" ? "Starting..." : "Idle", color: "var(--state-idle-text, #8b949e)" };
|
||||
}
|
||||
const lastHeartbeat = new Date(agent.lastHeartbeatAt).getTime();
|
||||
const elapsed = Date.now() - lastHeartbeat;
|
||||
const timeoutMs = 60000;
|
||||
if (elapsed > timeoutMs) {
|
||||
return { label: "Unresponsive", color: "#f85149" };
|
||||
return { label: "Unresponsive", color: "var(--state-error-text, #f85149)" };
|
||||
}
|
||||
return { label: "Healthy", color: "#3fb950" };
|
||||
return { label: "Healthy", color: "var(--state-active-text, #3fb950)" };
|
||||
};
|
||||
|
||||
const copyAgentId = () => {
|
||||
@@ -372,6 +372,27 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast }: Agent
|
||||
|
||||
<style>{`
|
||||
.agent-detail-overlay {
|
||||
/* Agent state CSS variables - define fallback values */
|
||||
--state-idle-bg: rgba(139, 148, 158, 0.15);
|
||||
--state-idle-text: #8b949e;
|
||||
--state-idle-border: #8b949e;
|
||||
--state-active-bg: rgba(46, 160, 67, 0.15);
|
||||
--state-active-text: #3fb950;
|
||||
--state-active-border: #3fb950;
|
||||
--state-paused-bg: rgba(227, 179, 65, 0.15);
|
||||
--state-paused-text: #e3b541;
|
||||
--state-paused-border: #e3b541;
|
||||
--state-error-bg: rgba(248, 81, 73, 0.15);
|
||||
--state-error-text: #f85149;
|
||||
--state-error-border: #f85149;
|
||||
--text-secondary: var(--text-muted, #8b949e);
|
||||
|
||||
/* Component-local aliases for dashboard tokens */
|
||||
--bg-primary: var(--surface, #161b22);
|
||||
--accent: var(--todo, #58a6ff);
|
||||
--text-primary: var(--text, #e6edf3);
|
||||
--bg-hover: var(--card-hover, #282e36);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
@@ -385,7 +406,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast }: Agent
|
||||
.agent-detail-modal {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
max-height: 85vh;
|
||||
@@ -423,7 +444,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast }: Agent
|
||||
.agent-detail-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
background: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -860,13 +881,13 @@ function LogsTab({
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--success);
|
||||
color: var(--color-success, #3fb950);
|
||||
}
|
||||
|
||||
.streaming-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--success);
|
||||
background: var(--color-success, #3fb950);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
@@ -914,14 +935,14 @@ function LogEntry({ entry }: { entry: AgentLogEntry }) {
|
||||
};
|
||||
case "tool_result":
|
||||
return {
|
||||
color: "var(--success)",
|
||||
borderLeft: "3px solid var(--success)",
|
||||
color: "var(--color-success, #3fb950)",
|
||||
borderLeft: "3px solid var(--color-success, #3fb950)",
|
||||
background: "rgba(76, 175, 80, 0.06)",
|
||||
};
|
||||
case "tool_error":
|
||||
return {
|
||||
color: "var(--error)",
|
||||
borderLeft: "3px solid var(--error)",
|
||||
color: "var(--color-error, #f85149)",
|
||||
borderLeft: "3px solid var(--color-error, #f85149)",
|
||||
background: "rgba(229, 57, 53, 0.06)",
|
||||
};
|
||||
case "thinking":
|
||||
@@ -1142,11 +1163,11 @@ function RunsTab({
|
||||
}
|
||||
|
||||
.run-status.completed {
|
||||
color: var(--success);
|
||||
color: var(--color-success, #3fb950);
|
||||
}
|
||||
|
||||
.run-status.failed {
|
||||
color: var(--error);
|
||||
color: var(--color-error, #f85149);
|
||||
}
|
||||
|
||||
.run-status.terminated {
|
||||
@@ -1520,11 +1541,11 @@ function ConfigTab({
|
||||
|
||||
.config-error {
|
||||
font-size: 11px;
|
||||
color: var(--error, #f85149);
|
||||
color: var(--color-error, #f85149);
|
||||
}
|
||||
|
||||
.input--error {
|
||||
border-color: var(--error, #f85149) !important;
|
||||
border-color: var(--color-error, #f85149) !important;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
@@ -1541,7 +1562,7 @@ function ConfigTab({
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--success, #3fb950);
|
||||
color: var(--color-success, #3fb950);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { X, Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List } from "lucide-react";
|
||||
import { X, Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, Filter } from "lucide-react";
|
||||
import type { Agent, AgentCapability, AgentState } from "../api";
|
||||
import { fetchAgents, createAgent, updateAgent, updateAgentState, deleteAgent } from "../api";
|
||||
|
||||
@@ -197,17 +197,21 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
<div className="modal-content">
|
||||
{/* Filter and Create Bar */}
|
||||
<div className="agent-controls">
|
||||
<select
|
||||
className="select"
|
||||
value={filterState}
|
||||
onChange={(e) => setFilterState(e.target.value as AgentState | "all")}
|
||||
>
|
||||
<option value="all">All States</option>
|
||||
<option value="idle">Idle</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="paused">Paused</option>
|
||||
<option value="terminated">Terminated</option>
|
||||
</select>
|
||||
<div className="agent-state-filter">
|
||||
<Filter size={14} />
|
||||
<select
|
||||
className="agent-state-filter-select"
|
||||
value={filterState}
|
||||
onChange={(e) => setFilterState(e.target.value as AgentState | "all")}
|
||||
aria-label="Filter agents by state"
|
||||
>
|
||||
<option value="all">All States</option>
|
||||
<option value="idle">Idle</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="paused">Paused</option>
|
||||
<option value="terminated">Terminated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn--primary"
|
||||
@@ -521,8 +525,38 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agent-controls .select {
|
||||
width: auto;
|
||||
.agent-state-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
transition: border-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-state-filter:hover {
|
||||
border-color: var(--text-dim);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.agent-state-filter:focus-within {
|
||||
border-color: var(--todo);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agent-state-filter-select {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: var(--font-primary);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.agent-create-form {
|
||||
@@ -531,7 +565,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.agent-create-form .input {
|
||||
|
||||
@@ -76,6 +76,134 @@ describe("AgentDetailView", () => {
|
||||
expect(screen.getByText(/Loading agent/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("defines CSS variables for agent state tokens in the style block", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const headings = screen.getAllByRole("heading", { level: 2 });
|
||||
expect(headings.some(h => h.textContent === "Test Agent")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify state CSS variables are defined in the component's style block
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
const allCss = Array.from(styleElements).map(el => el.textContent ?? "").join("");
|
||||
expect(allCss).toContain("--state-idle-bg");
|
||||
expect(allCss).toContain("--state-active-bg");
|
||||
expect(allCss).toContain("--state-paused-bg");
|
||||
expect(allCss).toContain("--state-error-bg");
|
||||
expect(allCss).toContain("--state-idle-text");
|
||||
expect(allCss).toContain("--state-active-text");
|
||||
});
|
||||
|
||||
it("uses token-based state colors for badges instead of hardcoded hex", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("active").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Verify badge styles use CSS variable references for background, not hex values
|
||||
const badges = document.querySelectorAll(".badge, .inline-badge");
|
||||
badges.forEach(badge => {
|
||||
const htmlEl = badge as HTMLElement;
|
||||
const style = htmlEl.getAttribute("style") ?? "";
|
||||
// Background should use var(--state-*) references, not raw rgba() or hex
|
||||
if (style.includes("background")) {
|
||||
expect(style).toContain("var(--state-");
|
||||
// Should not use raw rgba() for state backgrounds
|
||||
expect(style).not.toMatch(/background:\s*rgba\(/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("uses token-based colors for health status instead of hardcoded hex", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// The mock agent is active with a heartbeat from 2024, so it should show "Unresponsive"
|
||||
const hasHealthStatus = screen.queryAllByText(/Healthy|Unresponsive|Idle/).length > 0;
|
||||
expect(hasHealthStatus).toBe(true);
|
||||
});
|
||||
|
||||
// Health badges in header should use var(--state-*) references, not raw hex
|
||||
const headerBadges = document.querySelectorAll(".agent-detail-badges .badge");
|
||||
headerBadges.forEach(badge => {
|
||||
const htmlEl = badge as HTMLElement;
|
||||
const style = htmlEl.getAttribute("style") ?? "";
|
||||
if (style.includes("color:") && !style.includes("var(--state-")) {
|
||||
// If the color is not a state variable, it should still be a CSS variable
|
||||
expect(style).toMatch(/color:\s*var\(/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("uses token-based color references in CSS instead of undefined vars", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("active").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Navigate to Runs tab to trigger rendering of run-related style blocks
|
||||
fireEvent.click(screen.getByText("Runs"));
|
||||
|
||||
// Verify style blocks use --color-success and --color-error with fallbacks
|
||||
// (not bare --success or --error which are undefined in the root CSS)
|
||||
await waitFor(() => {
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
const allCss = Array.from(styleElements).map(el => el.textContent ?? "").join("");
|
||||
expect(allCss).toMatch(/var\(--color-success/);
|
||||
expect(allCss).toMatch(/var\(--color-error/);
|
||||
});
|
||||
});
|
||||
|
||||
it("defines component-local aliases for undefined CSS tokens", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("active").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Verify that component-local aliases are defined for tokens used in the style block
|
||||
// These map to real global tokens so they don't fall back to browser defaults
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
const allCss = Array.from(styleElements).map(el => el.textContent ?? "").join("");
|
||||
expect(allCss).toContain("--bg-primary: var(--surface");
|
||||
expect(allCss).toContain("--accent: var(--todo");
|
||||
expect(allCss).toContain("--text-primary: var(--text");
|
||||
expect(allCss).toContain("--bg-hover: var(--card-hover");
|
||||
});
|
||||
|
||||
it("displays agent name in header after loading", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
|
||||
@@ -671,7 +671,7 @@ describe("AgentListModal", () => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
const filterSelect = screen.getByLabelText("Filter agents by state");
|
||||
fireEvent.change(filterSelect, { target: { value: "active" } });
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -692,7 +692,7 @@ describe("AgentListModal", () => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
const filterSelect = screen.getByLabelText("Filter agents by state");
|
||||
fireEvent.change(filterSelect, { target: { value: "idle" } });
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -755,6 +755,93 @@ describe("AgentListModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("create form styling parity", () => {
|
||||
it("renders create form with dashboard token-based styling", async () => {
|
||||
render(
|
||||
<AgentListModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("New Agent")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("New Agent"));
|
||||
|
||||
// The create form container is rendered
|
||||
const createForm = document.querySelector(".agent-create-form");
|
||||
expect(createForm).toBeTruthy();
|
||||
|
||||
// The inline style block should use var(--radius-sm) instead of hardcoded 8px
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
let foundCreateFormRule = false;
|
||||
styleElements.forEach(styleEl => {
|
||||
const css = styleEl.textContent ?? "";
|
||||
if (css.includes(".agent-create-form")) {
|
||||
foundCreateFormRule = true;
|
||||
// Must not contain hardcoded border-radius: 8px
|
||||
expect(css).not.toMatch(/\.agent-create-form\s*\{[^}]*border-radius:\s*8px/);
|
||||
}
|
||||
});
|
||||
expect(foundCreateFormRule).toBe(true);
|
||||
});
|
||||
|
||||
it("renders filter with styled container matching AgentsView", async () => {
|
||||
render(
|
||||
<AgentListModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Styled filter container exists
|
||||
const filterContainer = document.querySelector(".agent-state-filter");
|
||||
expect(filterContainer).toBeTruthy();
|
||||
|
||||
// Select has correct aria-label
|
||||
const filterSelect = screen.getByLabelText("Filter agents by state");
|
||||
expect(filterSelect).toBeTruthy();
|
||||
expect(filterSelect).toHaveValue("all");
|
||||
});
|
||||
|
||||
it("filter CSS uses dashboard tokens for border-radius", async () => {
|
||||
render(
|
||||
<AgentListModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Agents")).toBeTruthy();
|
||||
});
|
||||
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
let foundFilterRule = false;
|
||||
styleElements.forEach(styleEl => {
|
||||
const css = styleEl.textContent ?? "";
|
||||
if (css.includes(".agent-state-filter {")) {
|
||||
foundFilterRule = true;
|
||||
// Should use var(--radius-sm) token
|
||||
expect(css).toContain("border-radius: var(--radius-sm)");
|
||||
// Should have hover and focus-within states
|
||||
expect(css).toContain(".agent-state-filter:hover");
|
||||
expect(css).toContain(".agent-state-filter:focus-within");
|
||||
}
|
||||
});
|
||||
expect(foundFilterRule).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("view toggle", () => {
|
||||
beforeEach(() => {
|
||||
// Clear localStorage before each test
|
||||
|
||||
Reference in New Issue
Block a user