import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import fs from "node:fs"; import path from "node:path"; import { AgentGenerationModal } from "../AgentGenerationModal"; import * as api from "../../api"; import type { AgentGenerationSpec } from "../../api"; vi.mock("../../api", () => ({ startAgentGeneration: vi.fn(), generateAgentSpec: vi.fn(), cancelAgentGeneration: vi.fn(), })); const mockStartAgentGeneration = vi.mocked(api.startAgentGeneration); const mockGenerateAgentSpec = vi.mocked(api.generateAgentSpec); const mockCancelAgentGeneration = vi.mocked(api.cancelAgentGeneration); import { loadAllAppCss } from "../../test/cssFixture"; const readStyles = () => loadAllAppCss(); const generatedSpec: AgentGenerationSpec = { title: "Accessibility Reviewer", icon: "♿", role: "reviewer", description: "Reviews React code for accessibility compliance", systemPrompt: "You are an expert accessibility reviewer...", thinkingLevel: "high", maxTurns: 12, }; describe("AgentGenerationModal", () => { const onClose = vi.fn(); const onGenerated = vi.fn(); beforeEach(() => { vi.clearAllMocks(); mockStartAgentGeneration.mockResolvedValue({ sessionId: "session-1", roleDescription: "review accessibility", }); mockGenerateAgentSpec.mockResolvedValue({ spec: generatedSpec, }); mockCancelAgentGeneration.mockResolvedValue({ success: true }); }); afterEach(() => { vi.restoreAllMocks(); }); function renderModal(isOpen = true) { return render( , ); } async function startGeneration(description = "Build an accessibility-focused reviewer") { const user = userEvent.setup(); await user.type(screen.getByLabelText("Role Description"), description); await user.click(screen.getByRole("button", { name: "Generate" })); await waitFor(() => { expect(screen.getByRole("button", { name: "Use This" })).toBeInTheDocument(); }); } it("returns null when isOpen=false", () => { renderModal(false); expect(screen.queryByText("Generate Agent")).not.toBeInTheDocument(); }); it("renders title and role description textarea when open", () => { renderModal(true); expect(screen.getByText("Generate Agent")).toBeInTheDocument(); expect(screen.getByLabelText("Role Description")).toBeInTheDocument(); }); it("uses token-based agent dialog styles in styles.css", () => { const styles = readStyles(); expect(styles).toContain('.agent-dialog-overlay'); expect(styles).toContain('background: color-mix(in srgb, var(--bg) 60%, transparent);'); expect(styles).toContain('.btn--ai-generate'); expect(styles).toContain('background: var(--cta-bg, var(--todo));'); expect(styles).toContain('.agent-dialog-field label'); expect(styles).toContain('margin-bottom: calc(var(--space-sm) - var(--space-xs) * 0.5);'); }); it("focuses role description textarea on open", () => { renderModal(true); expect(screen.getByLabelText("Role Description")).toHaveFocus(); }); it("disables Generate when description has fewer than 3 chars and enables at 3+", async () => { renderModal(true); const user = userEvent.setup(); const generateButton = screen.getByRole("button", { name: "Generate" }); expect(generateButton).toBeDisabled(); await user.type(screen.getByLabelText("Role Description"), "ab"); expect(generateButton).toBeDisabled(); await user.type(screen.getByLabelText("Role Description"), "c"); expect(generateButton).toBeEnabled(); }); it("shows character counter for role description", async () => { renderModal(true); const user = userEvent.setup(); await user.type(screen.getByLabelText("Role Description"), "abcd"); expect(screen.getByText("4/1000")).toBeInTheDocument(); }); it("handleGenerate calls startAgentGeneration then generateAgentSpec and transitions to preview", async () => { renderModal(true); await startGeneration(); expect(mockStartAgentGeneration).toHaveBeenCalledWith( "Build an accessibility-focused reviewer", undefined, ); expect(mockGenerateAgentSpec).toHaveBeenCalledWith("session-1", undefined); }); it("supports Enter key shortcut to generate when valid", async () => { renderModal(true); const textarea = screen.getByLabelText("Role Description"); fireEvent.change(textarea, { target: { value: "Generate with enter" } }); fireEvent.keyDown(textarea, { key: "Enter" }); await waitFor(() => { expect(mockStartAgentGeneration).toHaveBeenCalledWith("Generate with enter", undefined); }); }); it("preview shows title/icon/role/description/thinking/maxTurns with system prompt collapsed", async () => { renderModal(true); await startGeneration(); expect(screen.getByText(/Accessibility Reviewer/)).toBeInTheDocument(); expect(screen.getByText("reviewer")).toBeInTheDocument(); expect(screen.getByText("Reviews React code for accessibility compliance")).toBeInTheDocument(); expect(screen.getByText("high")).toBeInTheDocument(); expect(screen.getByText("12")).toBeInTheDocument(); expect(screen.getByText("System Prompt")).toBeInTheDocument(); expect(screen.getByText("Expand")).toBeInTheDocument(); }); it("Use This calls onGenerated with spec and closes modal", async () => { renderModal(true); await startGeneration(); const user = userEvent.setup(); await user.click(screen.getByRole("button", { name: "Use This" })); expect(onGenerated).toHaveBeenCalledWith(generatedSpec); expect(onClose).toHaveBeenCalledTimes(1); }); it("Regenerate cancels existing session and re-runs generation", async () => { mockStartAgentGeneration .mockResolvedValueOnce({ sessionId: "session-1", roleDescription: "first" }) .mockResolvedValueOnce({ sessionId: "session-2", roleDescription: "first" }); mockGenerateAgentSpec .mockResolvedValueOnce({ spec: generatedSpec }) .mockResolvedValueOnce({ spec: { ...generatedSpec, title: "Second Spec" } }); renderModal(true); await startGeneration(); const user = userEvent.setup(); await user.click(screen.getByRole("button", { name: "Regenerate" })); await waitFor(() => { expect(mockCancelAgentGeneration).toHaveBeenCalledWith("session-1", undefined); expect(screen.getByText(/Second Spec/)).toBeInTheDocument(); }); }); it("Cancel button calls onClose and cancels active session", async () => { renderModal(true); await startGeneration(); const user = userEvent.setup(); await user.click(screen.getByRole("button", { name: "Cancel" })); expect(mockCancelAgentGeneration).toHaveBeenCalledWith("session-1", undefined); expect(onClose).toHaveBeenCalledTimes(1); }); it("Escape key triggers cancel", async () => { renderModal(true); await startGeneration(); fireEvent.keyDown(document, { key: "Escape" }); await waitFor(() => { expect(mockCancelAgentGeneration).toHaveBeenCalledWith("session-1", undefined); expect(onClose).toHaveBeenCalled(); }); }); it("shows error and returns to input when startAgentGeneration fails", async () => { mockStartAgentGeneration.mockRejectedValueOnce(new Error("start failed")); renderModal(true); const user = userEvent.setup(); await user.type(screen.getByLabelText("Role Description"), "some role"); await user.click(screen.getByRole("button", { name: "Generate" })); await waitFor(() => { expect(screen.getByText("start failed")).toBeInTheDocument(); expect(screen.getByLabelText("Role Description")).toBeInTheDocument(); }); }); it("shows friendly message for 429/rate-limit errors", async () => { mockStartAgentGeneration.mockRejectedValueOnce(new Error("429 Too Many Requests")); renderModal(true); const user = userEvent.setup(); await user.type(screen.getByLabelText("Role Description"), "rate limit role"); await user.click(screen.getByRole("button", { name: "Generate" })); await waitFor(() => { expect(screen.getByText("Too many requests. Please wait a moment and try again.")).toBeInTheDocument(); }); }); it("cancels active session when modal closes via prop change", async () => { const { rerender } = render( , ); await startGeneration(); rerender(); await waitFor(() => { expect(mockCancelAgentGeneration).toHaveBeenCalledWith("session-1", undefined); }); }); });