- feat(KB-503): complete Step 5 other command project support - fix(KB-503): align task resolution with shared project context - fix(KB-503): preserve local task command fallback - test(KB-503): complete Step 4 task command coverage - test(KB-503): harden Step 3 CLI parser coverage - fix(KB-503): keep settings set scope explicit - fix(KB-503): allow implicit project resolution for project settings - fix(KB-503): enforce explicit settings scope rules - fix(KB-503): restore shared settings resolution behavior - fix(KB-503): align settings scope and project flag behavior - feat(KB-503): complete Step 5 — add project-aware settings, git, and backup behavior - fix(KB-503): enforce settings scope for global and project modes - fix(KB-503): preserve project-scoped settings and cwd-aware git helpers - fix(KB-503): add scoped settings behavior and git project tests - feat(KB-503): complete Step 5 — add project-aware git and backup coverage - test(KB-503): cover remaining project-aware task command paths - fix(KB-503): use shared resolution flow for all task commands - fix(KB-503): restore legacy task fallback and correct task mocks - fix(KB-503): align task resolution order and kb storage paths - fix(KB-503): restore local task store fallback without project flag - test(KB-503): cover remaining project-aware task handlers - fix(KB-503): preserve branch naming and avoid duplicate log resolution - test(KB-503): expand project-aware task command coverage - fix(KB-503): restore cwd fallback for path-based task commands - fix(KB-503): add project-aware task output and tests - fix(KB-503): harden project command output and coverage - fix(KB-503): preserve legacy task resolution without project flag - test(KB-503): add bin routing coverage for project flag parsing - feat(KB-503): complete Step 3 — CLI argument parsing updates - fix(KB-503): add defaultProjectId to GlobalSettings type and fix TypeScript errors - test(KB-503): fix resolveProject mock in task tests for runTaskLogs follow mode - docs(KB-503): add multi-project CLI documentation and changeset - test(KB-503): add project-context mock to task tests - test(KB-503): update git tests for new cwd parameter - feat(KB-503): Step 3 — CLI argument parsing updates with --project flag support - feat(KB-503): Steps 1-2 — project context utilities and project subcommands
285 lines
7.8 KiB
TypeScript
285 lines
7.8 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||
import { SetupWizard } from "../SetupWizard";
|
||
import type { ProjectInfo, ProjectCreateInput } from "../../api";
|
||
|
||
// Mock lucide-react
|
||
vi.mock("lucide-react", async () => {
|
||
const actual = await vi.importActual("lucide-react");
|
||
return {
|
||
...actual,
|
||
X: () => <span data-testid="close-icon">×</span>,
|
||
ChevronRight: () => <span data-testid="next-icon">→</span>,
|
||
ChevronLeft: () => <span data-testid="back-icon">←</span>,
|
||
Folder: () => <span data-testid="folder-icon">📁</span>,
|
||
Check: () => <span data-testid="check-icon">✓</span>,
|
||
Loader2: () => <span data-testid="loader-icon">⟳</span>,
|
||
AlertCircle: () => <span data-testid="alert-icon">⚠</span>,
|
||
};
|
||
});
|
||
|
||
describe("SetupWizard", () => {
|
||
it("does not render when isOpen is false", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={false}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
expect(screen.queryByText("Add New Project")).toBeNull();
|
||
});
|
||
|
||
it("renders when isOpen is true", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
expect(screen.getByText("Add New Project")).toBeDefined();
|
||
});
|
||
|
||
it("starts at directory step", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
expect(screen.getByText("Select Project Directory")).toBeDefined();
|
||
});
|
||
|
||
it("shows step indicator with 5 steps", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
expect(screen.getByText("Directory")).toBeDefined();
|
||
expect(screen.getByText("Name")).toBeDefined();
|
||
expect(screen.getByText("Mode")).toBeDefined();
|
||
expect(screen.getByText("Validate")).toBeDefined();
|
||
expect(screen.getByText("Confirm")).toBeDefined();
|
||
});
|
||
|
||
it("disables Next button when directory is empty", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
const nextButton = screen.getByRole("button", { name: /Next/i });
|
||
expect(nextButton).toBeDisabled();
|
||
});
|
||
|
||
it("enables Next button when directory is filled", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
const input = screen.getByPlaceholderText("/path/to/your/project");
|
||
fireEvent.change(input, { target: { value: "/home/user/project" } });
|
||
|
||
const nextButton = screen.getByRole("button", { name: /Next/i });
|
||
expect(nextButton).not.toBeDisabled();
|
||
});
|
||
|
||
it("navigates to next step when Next is clicked", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
const input = screen.getByPlaceholderText("/path/to/your/project");
|
||
fireEvent.change(input, { target: { value: "/home/user/project" } });
|
||
|
||
// Find the primary button (Next) in the actions area
|
||
const nextButton = screen.getByRole("button", { name: /Next/i });
|
||
fireEvent.click(nextButton);
|
||
|
||
expect(screen.getByText("Project Name")).toBeDefined();
|
||
});
|
||
|
||
it("auto-suggests name from directory path", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
const input = screen.getByPlaceholderText("/path/to/your/project");
|
||
fireEvent.change(input, { target: { value: "/home/user/my-awesome-project" } });
|
||
|
||
const nextButton = screen.getByRole("button", { name: /Next/i });
|
||
fireEvent.click(nextButton);
|
||
|
||
const nameInput = screen.getByPlaceholderText("My Project") as HTMLInputElement;
|
||
expect(nameInput.value).toBe("my-awesome-project");
|
||
});
|
||
|
||
it("allows navigation back to previous step", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
// Go to step 2
|
||
const input = screen.getByPlaceholderText("/path/to/your/project");
|
||
fireEvent.change(input, { target: { value: "/home/user/project" } });
|
||
const nextButton = screen.getByRole("button", { name: /Next/i });
|
||
fireEvent.click(nextButton);
|
||
|
||
// Go back
|
||
const backButton = screen.getByRole("button", { name: /Back/i });
|
||
fireEvent.click(backButton);
|
||
|
||
expect(screen.getByText("Select Project Directory")).toBeDefined();
|
||
});
|
||
|
||
it("shows isolation mode options", () => {
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
// Navigate to step 3 (isolation)
|
||
const dirInput = screen.getByPlaceholderText("/path/to/your/project");
|
||
fireEvent.change(dirInput, { target: { value: "/home/user/project" } });
|
||
|
||
// Go to name step
|
||
fireEvent.click(screen.getByRole("button", { name: /Next/i }));
|
||
|
||
// Go to isolation step
|
||
fireEvent.click(screen.getByRole("button", { name: /Next/i }));
|
||
|
||
expect(screen.getByText("In-Process (Default)")).toBeDefined();
|
||
expect(screen.getByText("Child Process (Isolated)")).toBeDefined();
|
||
});
|
||
|
||
it("calls onClose when Cancel is clicked", () => {
|
||
const onClose = vi.fn();
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={onClose}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
const cancelButton = screen.getByRole("button", { name: /Cancel/i });
|
||
fireEvent.click(cancelButton);
|
||
|
||
expect(onClose).toHaveBeenCalled();
|
||
});
|
||
|
||
it("calls onClose when close icon is clicked", () => {
|
||
const onClose = vi.fn();
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={onClose}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
const closeButton = screen.getByLabelText("Close");
|
||
fireEvent.click(closeButton);
|
||
|
||
expect(onClose).toHaveBeenCalled();
|
||
});
|
||
|
||
it("submits project data when created", async () => {
|
||
const mockRegisterProject = vi.fn().mockResolvedValue({
|
||
id: "proj_123",
|
||
name: "My Project",
|
||
path: "/home/user/project",
|
||
status: "active",
|
||
isolationMode: "in-process",
|
||
} as ProjectInfo);
|
||
|
||
const onProjectCreated = vi.fn();
|
||
|
||
render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={onProjectCreated}
|
||
onRegisterProject={mockRegisterProject}
|
||
/>
|
||
);
|
||
|
||
// Fill directory
|
||
const dirInput = screen.getByPlaceholderText("/path/to/your/project");
|
||
fireEvent.change(dirInput, { target: { value: "/home/user/project" } });
|
||
|
||
// The wizard should be in directory step with a Next button
|
||
expect(screen.getByRole("button", { name: /Next/i })).toBeDefined();
|
||
|
||
// Note: Full wizard flow testing would require more complex setup
|
||
// including mocking the validation API call
|
||
});
|
||
|
||
it("resets state when reopened", () => {
|
||
const { rerender } = render(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
// Fill some data
|
||
const input = screen.getByPlaceholderText("/path/to/your/project");
|
||
fireEvent.change(input, { target: { value: "/home/user/project" } });
|
||
|
||
// Close and reopen
|
||
rerender(
|
||
<SetupWizard
|
||
isOpen={false}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
rerender(
|
||
<SetupWizard
|
||
isOpen={true}
|
||
onClose={vi.fn()}
|
||
onProjectCreated={vi.fn()}
|
||
/>
|
||
);
|
||
|
||
// Should be back at step 1 with empty fields
|
||
expect(screen.getByText("Select Project Directory")).toBeDefined();
|
||
const newInput = screen.getByPlaceholderText("/path/to/your/project") as HTMLInputElement;
|
||
expect(newInput.value).toBe("");
|
||
});
|
||
});
|