Split app/styles.css from ~40k lines down to ~4.5k. Created 56 co-located
component CSS files in app/components/, each imported by its owning .tsx.
The remainder of styles.css holds genuinely global rules (design tokens,
.btn/.card/.modal/.form-input primitives, cross-component @media overrides).
- Lazy-load 13 heavy views (AgentsView, RoadmapsView, NodesView, etc.) via
React.lazy + Suspense; prefetch all chunks on idle so first navigation is
instant. Initial JS bundle: 1.58 MB → 1.16 MB (-26%). Initial CSS bundle:
635 kB → 471 kB (-26%); the rest splits into 13 per-view chunks.
- Add app/test/cssFixture.ts exposing loadAllAppCss() + loadAllAppCssBaseOnly()
so CSS regression tests load the full per-component bundle (mirroring Vite
source order). Migrate 30+ tests off direct readFileSync('../styles.css').
- Enable test.css: { include: [/.+/] } in vitest.config.ts so component CSS
imports actually inject styles in jsdom (fixes getComputedStyle assertions).
- Add ESLint rule (no-restricted-syntax) banning direct styles.css reads in
dashboard test files; points at loadAllAppCss() instead.
- Restore lost utility classes (.text-muted, .text-secondary, .text-dim,
.form-input) and rescue dropped chat tool-call rules into QuickChatFAB.css.
- Mobile fixes along the way: scroll containment for view containers
(min-height:0 + -webkit-overflow-scrolling), QuickChatFAB full-screen on
mobile (with safe-area-inset for iOS home bar), AgentsView single-row
header layout, ActivityLogModal close button on right, model-combobox
z-index above the mobile quick-chat panel.
- Bug fix: SkillsView toggle was display:none which hid the input from the
accessibility tree; replaced with the visually-hidden pattern so screen
readers + getByRole still find the checkbox.
- Bug fix: standalone Delete button in TaskDetailModal for triage-column
tasks (Actions dropdown is hidden in triage state, so previously no way
to delete a freshly-created task without status change first).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
265 lines
8.8 KiB
TypeScript
265 lines
8.8 KiB
TypeScript
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(
|
|
<AgentGenerationModal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
onGenerated={onGenerated}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
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(
|
|
<AgentGenerationModal isOpen={true} onClose={onClose} onGenerated={onGenerated} />,
|
|
);
|
|
|
|
await startGeneration();
|
|
|
|
rerender(<AgentGenerationModal isOpen={false} onClose={onClose} onGenerated={onGenerated} />);
|
|
|
|
await waitFor(() => {
|
|
expect(mockCancelAgentGeneration).toHaveBeenCalledWith("session-1", undefined);
|
|
});
|
|
});
|
|
});
|