test(FN-686): fix pre-existing dashboard test failures

- App.test.tsx: Add mocks for useProjects/useCurrentProject, update localStorage keys for view-mode
- SetupWizard.test.tsx: Remove tests for non-existent features (auto-detect, review step), match simple wizard
- TaskDetailModal.test.tsx: Update tab count to 7, change 'Steering Comments' to 'Comments', add Changes tab
- WorkspaceSelector.test.tsx: Fix task prefix from KB- to FN-
This commit is contained in:
gsxdsm
2026-04-01 09:08:23 -07:00
parent 41927b4995
commit efc49dee29
4 changed files with 146 additions and 242 deletions

View File

@@ -62,6 +62,27 @@ vi.mock("../../hooks/useTasks", () => ({
}),
}));
vi.mock("../../hooks/useProjects", () => ({
useProjects: () => ({
projects: [],
loading: false,
error: null,
refresh: vi.fn(),
register: vi.fn(),
update: vi.fn(),
unregister: vi.fn(),
}),
}));
vi.mock("../../hooks/useCurrentProject", () => ({
useCurrentProject: () => ({
currentProject: { id: "proj_123", name: "Test Project", path: "/test", status: "active", isolationMode: "in-process", createdAt: "", updatedAt: "" },
setCurrentProject: vi.fn(),
clearCurrentProject: vi.fn(),
loading: false,
}),
}));
import { fetchAuthStatus, fetchSettings, fetchTaskDetail, updateSettings } from "../../api";
beforeEach(() => {
@@ -355,18 +376,24 @@ describe("App engine pause (soft pause)", () => {
describe("App view switching", () => {
it("renders Board view by default", async () => {
// Set project mode so board view is available
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
// Wait for the app to render and check that the board is visible
await waitFor(() => {
expect(screen.getByRole("main")).toBeTruthy();
expect(document.querySelector(".board")).toBeTruthy();
});
// Board should be rendered
expect(screen.getByRole("main").className).toContain("board");
// Cleanup
localStorage.removeItem("kb-dashboard-view-mode");
});
it("renders ListView when view is switched to list", async () => {
// Set project mode so board/list view is available
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
// Wait for the header to render with view toggle
@@ -381,9 +408,15 @@ describe("App view switching", () => {
await waitFor(() => {
expect(document.querySelector(".list-view")).toBeTruthy();
});
// Cleanup
localStorage.removeItem("kb-dashboard-view-mode");
});
it("switches back to Board view from list view", async () => {
// Set project mode so board/list view is available
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
// Wait for the header to render
@@ -402,9 +435,15 @@ describe("App view switching", () => {
await waitFor(() => {
expect(document.querySelector(".board")).toBeTruthy();
});
// Cleanup
localStorage.removeItem("kb-dashboard-view-mode");
});
it("opens the NewTaskModal from the list view new-task button", async () => {
// Set project mode so board/list view is available
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
await waitFor(() => {
@@ -424,11 +463,15 @@ describe("App view switching", () => {
expect(screen.getByText("New Task")).toBeTruthy();
expect(screen.getByPlaceholderText("What needs to be done?")).toBeTruthy();
});
// Cleanup
localStorage.removeItem("kb-dashboard-view-mode");
});
it("persists view preference to localStorage", async () => {
// Clear any previous value
localStorage.removeItem("kb-dashboard-view");
// Clear any previous value and set project mode
localStorage.removeItem("kb-dashboard-task-view");
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
@@ -442,13 +485,17 @@ describe("App view switching", () => {
// Should have saved to localStorage
await waitFor(() => {
expect(localStorage.getItem("kb-dashboard-view")).toBe("list");
expect(localStorage.getItem("kb-dashboard-task-view")).toBe("list");
});
// Cleanup
localStorage.removeItem("kb-dashboard-view-mode");
});
it("initializes view from localStorage if available", async () => {
// Set localStorage to list view
localStorage.setItem("kb-dashboard-view", "list");
// Set localStorage to list view and project mode
localStorage.setItem("kb-dashboard-task-view", "list");
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
@@ -461,7 +508,8 @@ describe("App view switching", () => {
expect(screen.getByTitle("List view").className).toContain("active");
// Cleanup
localStorage.removeItem("kb-dashboard-view");
localStorage.removeItem("kb-dashboard-task-view");
localStorage.removeItem("kb-dashboard-view-mode");
});
it("shows view toggle buttons in header", async () => {

View File

@@ -2,36 +2,19 @@ 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();
// Mock the API
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 = () => {};
@@ -40,196 +23,91 @@ 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 () => {
it("renders the wizard with manual step by default", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
// Wait for the effect to run
await waitFor(() => {
expect(mockFetchFirstRunStatus).toHaveBeenCalled();
});
// Should show welcome/manual screen
expect(await screen.findByText("Welcome to kb")).toBeDefined();
// 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();
});
// Should show manual entry form
expect(screen.getByLabelText("Project Path")).toBeDefined();
expect(screen.getByLabelText("Project Name")).toBeDefined();
expect(screen.getByLabelText("Isolation Mode")).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");
const pathInput = await screen.findByLabelText("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",
});
it("calls onClose when close button is clicked", async () => {
const onClose = vi.fn();
render(
<SetupWizardModal
onProjectRegistered={onProjectRegistered}
onProjectsRegistered={noop}
onProjectRegistered={noop}
onClose={onClose}
/>
);
const closeButton = await screen.findByLabelText("Close wizard");
fireEvent.click(closeButton);
expect(onClose).toHaveBeenCalled();
});
it("disables register button when form is incomplete", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Add Manually")).toBeDefined();
// Wait for form to render
await screen.findByLabelText("Project Path");
const registerButton = screen.getByRole("button", { name: /register project/i });
expect(registerButton).toBeDisabled();
// Fill only path
fireEvent.change(screen.getByLabelText("Project Path"), {
target: { value: "/path/to/project" },
});
fireEvent.click(screen.getByText("Add Manually"));
// Button should still be disabled
expect(registerButton).toBeDisabled();
});
await waitFor(() => {
expect(screen.getByLabelText("Project Path")).toBeDefined();
});
it("enables register button when form is complete", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onClose={noop}
/>
);
// Wait for form to render
await screen.findByLabelText("Project Path");
// Fill the form
fireEvent.change(screen.getByLabelText("Project Path"), {
target: { value: "/path/to/project" },
});
@@ -237,74 +115,48 @@ describe("SetupWizardModal", () => {
target: { value: "test-project" },
});
fireEvent.click(screen.getByText("Register Project"));
await waitFor(() => {
expect(onProjectRegistered).toHaveBeenCalled();
});
// Wait for button to be enabled
const registerButton = screen.getByRole("button", { name: /register project/i });
await waitFor(() => expect(registerButton).not.toBeDisabled());
});
it("persists wizard state to localStorage", async () => {
mockDetectProjects.mockResolvedValue({
projects: [{ path: "/home/user/project1", suggestedName: "project1", existing: false }],
});
it("has isolation mode selector with correct options", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={noop}
/>
);
await waitFor(() => {
expect(screen.getByText("Auto-detect Projects")).toBeDefined();
});
const select = await screen.findByLabelText("Isolation Mode") as HTMLSelectElement;
expect(select.value).toBe("in-process");
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");
});
// Check options exist
const options = Array.from(select.options);
expect(options.some(opt => opt.value === "in-process")).toBe(true);
expect(options.some(opt => opt.value === "child-process")).toBe(true);
});
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 });
it("shows form hint for project path", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onProjectsRegistered={noop}
onClose={onClose}
onClose={noop}
/>
);
// Wait for modal to open
await waitFor(() => {
expect(screen.getByText("Review Detected Projects")).toBeDefined();
});
expect(await screen.findByText("Absolute path to your project directory")).toBeDefined();
});
// Close the modal
fireEvent.click(screen.getByLabelText("Close wizard"));
it("has correct isolation mode default value", async () => {
render(
<SetupWizardModal
onProjectRegistered={noop}
onClose={noop}
/>
);
expect(localStorage.getItem("kb-setup-wizard-state")).toBeNull();
expect(onClose).toHaveBeenCalled();
const select = await screen.findByLabelText("Isolation Mode") as HTMLSelectElement;
expect(select.value).toBe("in-process");
});
});

View File

@@ -731,7 +731,7 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".detail-activity-list")).toBeNull();
});
it("can switch between all four tabs", () => {
it("can switch between all tabs", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({
@@ -763,14 +763,16 @@ describe("TaskDetailModal", () => {
// Switch to Steering tab
fireEvent.click(screen.getByText("Steering"));
expect(screen.getByText("Steering Comments")).toBeTruthy();
// The SteeringTab renders an h4 with "Comments"
expect(container.querySelector("h4")?.textContent).toBe("Comments");
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
// Switch back to Definition tab
fireEvent.click(screen.getByText("Definition"));
expect(container.querySelector(".markdown-body")).toBeTruthy();
expect(container.querySelector(".detail-activity")).toBeNull();
expect(screen.queryByText("Steering Comments")).toBeNull();
// The Comments heading should not be visible in Definition tab
expect(container.querySelector("h4")?.textContent).not.toBe("Comments");
});
it("switches to Agent Log tab and back", async () => {
@@ -1166,7 +1168,7 @@ describe("TaskDetailModal", () => {
);
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(6); // Definition, Activity, Agent Log, Steering, Comments, Model
expect(tabs.length).toBe(7); // Definition, Activity, Agent Log, Changes, Steering, Comments, Model
// Tabs should use class-based styling, not inline styles
expect(tabs[0].classList.contains("detail-tab")).toBe(true);
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true); // Definition is default active
@@ -1175,6 +1177,7 @@ describe("TaskDetailModal", () => {
expect(tabs[3].classList.contains("detail-tab-active")).toBe(false);
expect(tabs[4].classList.contains("detail-tab-active")).toBe(false);
expect(tabs[5].classList.contains("detail-tab-active")).toBe(false);
expect(tabs[6].classList.contains("detail-tab-active")).toBe(false);
// Verify no inline padding/fontSize (responsive CSS controls this)
expect((tabs[0] as HTMLElement).style.padding).toBe("");
expect((tabs[0] as HTMLElement).style.fontSize).toBe("");
@@ -1652,7 +1655,7 @@ describe("TaskDetailModal", () => {
});
});
it("shows all 6 tabs in correct order with comments", () => {
it("shows all 7 tabs in correct order with comments", () => {
const { container } = render(
<TaskDetailModal
task={makeTask()}
@@ -1666,13 +1669,14 @@ describe("TaskDetailModal", () => {
);
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(6);
expect(tabs.length).toBe(7);
expect(tabs[0].textContent).toBe("Definition");
expect(tabs[1].textContent).toBe("Activity");
expect(tabs[2].textContent).toBe("Agent Log");
expect(tabs[3].textContent).toBe("Steering");
expect(tabs[4].textContent).toBe("Comments");
expect(tabs[5].textContent).toBe("Model");
expect(tabs[3].textContent).toBe("Changes");
expect(tabs[4].textContent).toBe("Steering");
expect(tabs[5].textContent).toBe("Comments");
expect(tabs[6].textContent).toBe("Model");
});
it("shows empty state and Edit button when no prompt", () => {

View File

@@ -46,11 +46,11 @@ describe("WorkspaceSelector", () => {
/>,
);
await user.click(screen.getByRole("button", { name: /KB-200/i }));
await user.click(screen.getByRole("button", { name: /FN-200/i }));
expect(screen.getByText("Project Root")).toBeInTheDocument();
const menu = screen.getByRole("listbox", { name: /select workspace/i });
const activeOption = within(menu).getByRole("button", { name: /KB-200 Short title/i });
const activeOption = within(menu).getByRole("button", { name: /FN-200 Short title/i });
expect(activeOption.className).toContain("active");
});
@@ -67,7 +67,7 @@ describe("WorkspaceSelector", () => {
);
await user.click(screen.getByRole("button", { name: /kb/i }));
await user.click(screen.getByRole("button", { name: /KB-123/i }));
await user.click(screen.getByRole("button", { name: /FN-123/i }));
expect(onSelect).toHaveBeenCalledWith("FN-123");
expect(screen.queryByText("Task Worktrees")).not.toBeInTheDocument();