Files
fusion/packages/dashboard/app/components/__tests__/SetupWizard.test.tsx
gsxdsm d8a591bd7f feat(KB-502): add dashboard multi-project UX with overview, drill-down, and setup wizard
- Add Project Overview page with responsive grid and health metrics

- Add Project selector dropdown for quick context switching

- Add Project drill-down with back navigation to overview

- Add Setup wizard with auto-detection and manual entry flows

- Add Global activity feed with project attribution badges

- Add Project health polling (active tasks, agents, completion counts)

- Add Empty states and loading skeletons for better UX

- Add Keyboard navigation support (arrow keys, enter, escape)

- Add LocalStorage persistence for wizard state and view preferences
2026-04-01 08:04:02 -07:00

311 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { SetupWizardModal } from "../SetupWizardModal";
// Mock the API and utils
const mockFetchFirstRunStatus = vi.fn();
const mockDetectProjects = vi.fn();
const mockRegisterProject = vi.fn();
vi.mock("../api", () => ({
fetchFirstRunStatus: (...args: unknown[]) => mockFetchFirstRunStatus(...args),
detectProjects: (...args: unknown[]) => mockDetectProjects(...args),
registerProject: (...args: unknown[]) => mockRegisterProject(...args),
}));
vi.mock("../utils/projectDetection", () => ({
scanForProjects: (...args: unknown[]) => mockDetectProjects(...args),
suggestProjectName: (path: string) => path.split("/").pop() || "",
isValidProjectName: (name: string) => /^[a-zA-Z0-9_-]+$/.test(name),
}));
// Mock lucide-react icons
vi.mock("lucide-react", () => ({
X: () => <span data-testid="x-icon">×</span>,
Loader2: () => <span data-testid="loader-icon"></span>,
FolderPlus: () => <span data-testid="folder-icon">📁</span>,
Search: () => <span data-testid="search-icon">🔍</span>,
CheckCircle: () => <span data-testid="check-icon"></span>,
ArrowRight: () => <span data-testid="arrow-right"></span>,
ArrowLeft: () => <span data-testid="arrow-left"></span>,
Folder: () => <span data-testid="folder-small">📂</span>,
Check: () => <span data-testid="check-small"></span>,
AlertCircle: () => <span data-testid="alert-icon"></span>,
Pencil: () => <span data-testid="pencil-icon"></span>,
}));
const noop = () => {};
describe("SetupWizardModal", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
// Default: no projects, so wizard should auto-open
mockFetchFirstRunStatus.mockResolvedValue({ hasProjects: false });
});
it("auto-opens when no projects exist", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
// Wait for the effect to run
await waitFor(() => {
expect(mockFetchFirstRunStatus).toHaveBeenCalled();
});
// Should show welcome screen
await waitFor(() => {
expect(screen.getByText("Welcome to kb")).toBeDefined();
});
});
it("does not auto-open when projects exist", async () => {
mockFetchFirstRunStatus.mockResolvedValue({ hasProjects: true });
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(mockFetchFirstRunStatus).toHaveBeenCalled();
});
// Should not show welcome screen
expect(screen.queryByText("Welcome to kb")).toBeNull();
});
it("shows welcome step with auto-detect and manual options", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Welcome to kb")).toBeDefined();
});
expect(screen.getByText("Auto-detect Projects")).toBeDefined();
expect(screen.getByText("Add Manually")).toBeDefined();
});
it("transitions to detecting step when auto-detect clicked", async () => {
mockDetectProjects.mockResolvedValue({ projects: [] });
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Auto-detect Projects")).toBeDefined();
});
fireEvent.click(screen.getByText("Auto-detect Projects"));
await waitFor(() => {
expect(screen.getByText("Detecting Projects...")).toBeDefined();
});
});
it("shows review step with detected projects", async () => {
mockDetectProjects.mockResolvedValue({
projects: [
{ path: "/home/user/project1", suggestedName: "project1", existing: false },
{ path: "/home/user/project2", suggestedName: "project2", existing: false },
],
});
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Auto-detect Projects")).toBeDefined();
});
fireEvent.click(screen.getByText("Auto-detect Projects"));
await waitFor(() => {
expect(screen.getByText("Review Detected Projects")).toBeDefined();
});
// Should show detected projects
await waitFor(() => {
expect(screen.getByText("project1")).toBeDefined();
expect(screen.getByText("project2")).toBeDefined();
});
});
it("transitions to manual step when manual option clicked", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Add Manually")).toBeDefined();
});
fireEvent.click(screen.getByText("Add Manually"));
await waitFor(() => {
expect(screen.getByText("Add Project Manually")).toBeDefined();
});
});
it("allows entering project details in manual step", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Add Manually")).toBeDefined();
});
fireEvent.click(screen.getByText("Add Manually"));
await waitFor(() => {
expect(screen.getByLabelText("Project Path")).toBeDefined();
});
const pathInput = screen.getByLabelText("Project Path");
fireEvent.change(pathInput, { target: { value: "/path/to/project" } });
expect(pathInput).toHaveValue("/path/to/project");
});
it("calls onProjectRegistered when manual registration succeeds", async () => {
const onProjectRegistered = vi.fn();
mockRegisterProject.mockResolvedValue({
id: "proj_123",
name: "Test Project",
path: "/path/to/project",
status: "active",
isolationMode: "in-process",
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
});
render(
<SetupWizardModal
onProjectRegistered={onProjectRegistered}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Add Manually")).toBeDefined();
});
fireEvent.click(screen.getByText("Add Manually"));
await waitFor(() => {
expect(screen.getByLabelText("Project Path")).toBeDefined();
});
fireEvent.change(screen.getByLabelText("Project Path"), {
target: { value: "/path/to/project" },
});
fireEvent.change(screen.getByLabelText("Project Name"), {
target: { value: "test-project" },
});
fireEvent.click(screen.getByText("Register Project"));
await waitFor(() => {
expect(onProjectRegistered).toHaveBeenCalled();
});
});
it("persists wizard state to localStorage", async () => {
mockDetectProjects.mockResolvedValue({
projects: [{ path: "/home/user/project1", suggestedName: "project1", existing: false }],
});
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Auto-detect Projects")).toBeDefined();
});
fireEvent.click(screen.getByText("Auto-detect Projects"));
await waitFor(() => {
expect(screen.getByText("Review Detected Projects")).toBeDefined();
});
// Check localStorage was updated
await waitFor(() => {
const saved = localStorage.getItem("kb-setup-wizard-state");
expect(saved).toBeTruthy();
const parsed = JSON.parse(saved!);
expect(parsed.inProgress).toBe(true);
expect(parsed.step).toBe("review");
});
});
it("clears localStorage when closed", async () => {
const onClose = vi.fn();
// Pre-populate localStorage
localStorage.setItem(
"kb-setup-wizard-state",
JSON.stringify({ inProgress: true, step: "review", detectedProjects: [] })
);
mockFetchFirstRunStatus.mockResolvedValue({ hasProjects: false });
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={onClose}
/>
);
// Wait for modal to open
await waitFor(() => {
expect(screen.getByText("Review Detected Projects")).toBeDefined();
});
// Close the modal
fireEvent.click(screen.getByLabelText("Close wizard"));
expect(localStorage.getItem("kb-setup-wizard-state")).toBeNull();
expect(onClose).toHaveBeenCalled();
});
});