Files
fusion/packages/dashboard/app/components/__tests__/ProjectSelector.test.tsx
gsxdsm a27921a276 fix: address PR review feedback (#1467)
- guard optional project selection callbacks and keep bookmarked search matches visible
- use structured timeout/rate-limit handling in retry backoff
- add focused regressions for selector and retry behavior

Note: pnpm test passed the merge gate but hit pre-existing non-blocking engine mock failures in changed-package tests.
2026-06-07 23:03:38 -07:00

872 lines
29 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { ProjectSelector } from "../ProjectSelector";
import type { ProjectInfo, ProjectStatus } from "@fusion/core";
// Mock useProjectBookmarks
const mockToggleBookmark = vi.fn();
let mockBookmarkedIds: Set<string> = new Set();
vi.mock("../../hooks/useProjectBookmarks", () => ({
useProjectBookmarks: () => ({
bookmarkedIds: mockBookmarkedIds,
toggleBookmark: mockToggleBookmark,
isBookmarked: (id: string) => mockBookmarkedIds.has(id),
}),
}));
// Mock lucide-react
vi.mock("lucide-react", async () => {
const actual = await vi.importActual("lucide-react");
return {
...actual,
ChevronDown: () => <span data-testid="chevron-icon">▼</span>,
Check: () => <span data-testid="check-icon">✓</span>,
Folder: () => <span data-testid="folder-icon">📁</span>,
Grid3X3: () => <span data-testid="grid-icon">⊞</span>,
Search: () => <span data-testid="search-icon">🔍</span>,
Clock: () => <span data-testid="clock-icon">🕐</span>,
Star: ({ fill }: { fill?: string }) => (
<span data-testid="star-icon" data-fill={fill ?? "none"}>★</span>
),
X: () => <span data-testid="x-icon">✕</span>,
Play: () => <span data-testid="play-icon">▶</span>,
Pause: () => <span data-testid="pause-icon">⏸</span>,
AlertCircle: () => <span data-testid="alert-icon">⚠</span>,
Loader2: () => <span data-testid="loader-icon">⟳</span>,
};
});
function makeProject(overrides: Partial<ProjectInfo> = {}): ProjectInfo {
return {
id: "proj_abc123",
name: "Test Project",
path: "/home/user/projects/test",
status: "active",
isolationMode: "in-process",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
lastActivityAt: new Date().toISOString(),
...overrides,
};
}
const noop = () => {};
describe("ProjectSelector", () => {
beforeEach(() => {
vi.clearAllMocks();
mockBookmarkedIds = new Set();
// JSDOM does not implement scrollIntoView — mock it for itemRefs
Element.prototype.scrollIntoView = vi.fn();
});
it("renders without crashing", () => {
render(
<ProjectSelector
projects={[makeProject({ id: "proj_1" }), makeProject({ id: "proj_2" })]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
expect(screen.getByTestId("project-selector-trigger")).toBeDefined();
});
it("does not render when only one project exists", () => {
const { container } = render(
<ProjectSelector
projects={[makeProject()]}
currentProject={makeProject()}
onSelect={noop}
onViewAll={noop}
/>
);
expect(container.firstChild).toBeNull();
});
it("does not render when no projects exist", () => {
const { container } = render(
<ProjectSelector
projects={[]}
currentProject={null}
onSelect={noop}
onViewAll={noop}
/>
);
expect(container.firstChild).toBeNull();
});
it("shows current project name in trigger", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1", name: "Project One" })}
onSelect={noop}
onViewAll={noop}
/>
);
expect(screen.getByText("Project One")).toBeDefined();
});
it("opens dropdown on click", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByTestId("project-selector-dropdown")).toBeDefined();
});
it("shows all projects in dropdown", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByText("Project Two")).toBeDefined();
});
it("renders fallback status icons for unknown or missing project statuses", () => {
expect(() => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One", status: "active" }),
makeProject({ id: "proj_2", name: "Project Two", status: "removing" as ProjectStatus }),
makeProject({ id: "proj_3", name: "Project Three", status: undefined as unknown as ProjectStatus }),
]}
currentProject={makeProject({ id: "proj_1", name: "Project One", status: "active" })}
onSelect={noop}
onViewAll={noop}
/>
);
}).not.toThrow();
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByText("Project Two")).toBeDefined();
expect(screen.getByText("Project Three")).toBeDefined();
expect(screen.getAllByTestId("alert-icon").length).toBeGreaterThanOrEqual(2);
});
it("calls onSelect when project is clicked", () => {
const onSelect = vi.fn();
const projectTwo = makeProject({ id: "proj_2", name: "Project Two" });
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
projectTwo,
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={onSelect}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
fireEvent.click(screen.getByText("Project Two"));
expect(onSelect).toHaveBeenCalledWith(projectTwo);
});
it("does not throw when clicking a project without onSelect", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(() => fireEvent.click(screen.getByText("Project Two"))).not.toThrow();
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
it("closes dropdown after selection", () => {
const onSelect = vi.fn();
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={onSelect}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
fireEvent.click(screen.getByText("Project Two"));
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
it("calls onViewAll when 'View All Projects' is clicked", () => {
const onViewAll = vi.fn();
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={onViewAll}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
fireEvent.click(screen.getByText("View All Projects"));
expect(onViewAll).toHaveBeenCalled();
});
it("shows search input when 5+ projects", () => {
render(
<ProjectSelector
projects={Array.from({ length: 5 }, (_, i) =>
makeProject({ id: `proj_${i}`, name: `Project ${i}` })
)}
currentProject={makeProject({ id: "proj_0" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByPlaceholderText("Search projects...")).toBeDefined();
});
it("shows search input even with only 2 projects (autocomplete always available)", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Alpha" }),
makeProject({ id: "proj_2", name: "Beta" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
// Search input should always be present for autocomplete
expect(screen.getByTestId("project-selector-search-input")).toBeDefined();
});
it("filters projects based on search query", () => {
render(
<ProjectSelector
projects={Array.from({ length: 5 }, (_, i) =>
makeProject({ id: `proj_${i}`, name: `Project ${i}` })
)}
currentProject={makeProject({ id: "proj_0" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "Project 2" } });
expect(screen.getByText("Project 2")).toBeDefined();
expect(screen.queryByText("Project 1")).toBeNull();
});
it("shows recent projects section", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
makeProject({ id: "proj_3", name: "Project Three" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
recentProjectIds={["proj_2", "proj_3"]}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByText("Recent")).toBeDefined();
expect(screen.getByText("Project Two")).toBeDefined();
});
it("closes dropdown on escape key", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1" }),
makeProject({ id: "proj_2" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByTestId("project-selector-dropdown")).toBeDefined();
fireEvent.keyDown(document, { key: "Escape" });
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
it("closes dropdown on outside click", () => {
render(
<>
<div data-testid="outside">Outside element</div>
<ProjectSelector
projects={[
makeProject({ id: "proj_1" }),
makeProject({ id: "proj_2" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
</>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByTestId("project-selector-dropdown")).toBeDefined();
fireEvent.mouseDown(screen.getByTestId("outside"));
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
it("trigger has correct aria attributes", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1" }),
makeProject({ id: "proj_2" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
const trigger = screen.getByTestId("project-selector-trigger");
expect(trigger.getAttribute("aria-haspopup")).toBe("listbox");
expect(trigger.getAttribute("aria-expanded")).toBe("false");
fireEvent.click(trigger);
expect(trigger.getAttribute("aria-expanded")).toBe("true");
});
it("shows checkmark for current project", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1", name: "Project One" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
// Should have checkmark for current project (though in dropdown it might not be visible due to filtering)
});
// === Bookmark-specific tests ===
describe("bookmarks", () => {
it("shows bookmark toggle on each project item", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const bookmarkToggle = screen.getByTestId("bookmark-toggle-proj_2");
expect(bookmarkToggle).toBeDefined();
});
it("calls toggleBookmark when star is clicked", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const bookmarkToggle = screen.getByTestId("bookmark-toggle-proj_2");
fireEvent.click(bookmarkToggle);
expect(mockToggleBookmark).toHaveBeenCalledWith("proj_2");
});
it("does not trigger onSelect when star is clicked", () => {
const onSelect = vi.fn();
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={onSelect}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const bookmarkToggle = screen.getByTestId("bookmark-toggle-proj_2");
fireEvent.click(bookmarkToggle);
expect(onSelect).not.toHaveBeenCalled();
});
it("shows Bookmarked section when projects are bookmarked", () => {
mockBookmarkedIds = new Set(["proj_2"]);
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
makeProject({ id: "proj_3", name: "Project Three" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.getByText("Bookmarked")).toBeDefined();
expect(screen.getByText("Project Two")).toBeDefined();
});
it("displays bookmarked projects at top of list", () => {
mockBookmarkedIds = new Set(["proj_3"]);
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
makeProject({ id: "proj_3", name: "Project Three" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
// "Bookmarked" section header should exist
expect(screen.getByText("Bookmarked")).toBeDefined();
// The "All Projects" section should also exist since we have non-bookmarked items
expect(screen.getByText("All Projects")).toBeDefined();
});
it("bookmark toggle shows filled star for bookmarked projects", () => {
mockBookmarkedIds = new Set(["proj_2"]);
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const bookmarkedStar = screen.getByTestId("bookmark-toggle-proj_2");
// Star inside should have data-fill="currentColor" for bookmarked
const starIcon = bookmarkedStar.querySelector('[data-testid="star-icon"]');
expect(starIcon?.getAttribute("data-fill")).toBe("currentColor");
});
it("does not show Bookmarked section when no projects are bookmarked", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
expect(screen.queryByText("Bookmarked")).toBeNull();
});
it("does not show current project in bookmarked section", () => {
mockBookmarkedIds = new Set(["proj_1"]);
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
// Current project should not appear in Bookmarked section
// (it's excluded from all lists)
expect(screen.queryByText("Bookmarked")).toBeNull();
});
it("bookmark toggle has correct aria-label", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const toggle = screen.getByTestId("bookmark-toggle-proj_2");
expect(toggle.getAttribute("aria-label")).toBe("Bookmark project");
});
it("bookmarked project toggle shows remove aria-label", () => {
mockBookmarkedIds = new Set(["proj_2"]);
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const toggle = screen.getByTestId("bookmark-toggle-proj_2");
expect(toggle.getAttribute("aria-label")).toBe("Remove bookmark");
});
it("dropdown stays open when bookmark is toggled", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project One" }),
makeProject({ id: "proj_2", name: "Project Two" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const bookmarkToggle = screen.getByTestId("bookmark-toggle-proj_2");
fireEvent.click(bookmarkToggle);
// Dropdown should still be open
expect(screen.getByTestId("project-selector-dropdown")).toBeDefined();
});
});
// === Autocomplete-specific tests ===
describe("autocomplete", () => {
it("highlights matching text in project names", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Project Alpha" }),
makeProject({ id: "proj_2", name: "Project Beta" }),
makeProject({ id: "proj_3", name: "Unrelated" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "Beta" } });
// Should show the matched project with highlighted text
const mark = screen.getByText("Beta");
expect(mark.tagName).toBe("MARK");
expect(mark.closest(".project-selector__item")).toBeDefined();
});
it("detects exact match and shows indicator", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "My App" }),
makeProject({ id: "proj_2", name: "My Application" }),
makeProject({ id: "proj_3", name: "Other" }),
]}
currentProject={makeProject({ id: "proj_3" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "My App" } });
// Should show exact match indicator
expect(screen.getByTestId("project-selector-exact-match")).toBeDefined();
// Should show "Exact" badge on the matching item
expect(screen.getByText("Exact")).toBeDefined();
});
it("does not show exact match indicator for partial match", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "My App" }),
makeProject({ id: "proj_2", name: "My Application" }),
makeProject({ id: "proj_3", name: "Other" }),
]}
currentProject={makeProject({ id: "proj_3" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
// "My" matches multiple projects — not an exact match
fireEvent.change(searchInput, { target: { value: "My" } });
expect(screen.queryByTestId("project-selector-exact-match")).toBeNull();
});
it("auto-selects exact match on Enter when nothing is highlighted", () => {
const onSelect = vi.fn();
const exactProject = makeProject({ id: "proj_1", name: "Unique Project" });
render(
<ProjectSelector
projects={[
exactProject,
makeProject({ id: "proj_2", name: "Other Project" }),
]}
currentProject={makeProject({ id: "proj_2" })}
onSelect={onSelect}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "Unique Project" } });
// Press Enter without highlighting anything first
fireEvent.keyDown(screen.getByTestId("project-selector-dropdown"), { key: "Enter" });
expect(onSelect).toHaveBeenCalledWith(exactProject);
});
it("shows no results message when search matches nothing", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Alpha" }),
makeProject({ id: "proj_2", name: "Beta" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "zzznonexistent" } });
expect(screen.getByTestId("project-selector-no-results")).toBeDefined();
});
it("shows bookmarked projects that match an active search", () => {
mockBookmarkedIds = new Set(["proj_2"]);
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Alpha" }),
makeProject({ id: "proj_2", name: "Starred Project" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
fireEvent.change(screen.getByPlaceholderText("Search projects..."), {
target: { value: "Starred" },
});
expect(screen.getByText("Starred")).toBeDefined();
expect(screen.getByText("Project")).toBeDefined();
expect(screen.queryByTestId("project-selector-no-results")).toBeNull();
});
it("does not throw when pressing Enter on a highlighted project without onSelect", async () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Alpha" }),
makeProject({ id: "proj_2", name: "Beta" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "Beta" } });
await waitFor(() => {
expect(screen.getByTestId("project-selector-item-proj_2").className).toContain("highlighted");
});
expect(() => fireEvent.keyDown(searchInput, { key: "Enter" })).not.toThrow();
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
it("clear button clears search query", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Alpha" }),
makeProject({ id: "proj_2", name: "Beta" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "Alpha" } });
// Clear button should appear
const clearButton = screen.getByLabelText("Clear search");
expect(clearButton).toBeDefined();
fireEvent.click(clearButton);
expect(searchInput).toHaveValue("");
});
it("filters by project path as well as name", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Alpha", path: "/home/user/projects/frontend" }),
makeProject({ id: "proj_2", name: "Beta", path: "/home/user/projects/backend" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "backend" } });
// Beta project should be visible (matched by path)
expect(screen.getByText("Beta")).toBeDefined();
// Alpha should be filtered out
expect(screen.queryByText("Alpha")).toBeNull();
});
it("resets search query when dropdown is closed and reopened", () => {
render(
<ProjectSelector
projects={[
makeProject({ id: "proj_1", name: "Alpha" }),
makeProject({ id: "proj_2", name: "Beta" }),
]}
currentProject={makeProject({ id: "proj_1" })}
onSelect={noop}
onViewAll={noop}
/>
);
// Open, type something, close with escape
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const searchInput = screen.getByPlaceholderText("Search projects...");
fireEvent.change(searchInput, { target: { value: "Alpha" } });
fireEvent.keyDown(document, { key: "Escape" });
// Reopen — search should be cleared
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const reopenedSearchInput = screen.getByPlaceholderText("Search projects...");
expect(reopenedSearchInput).toHaveValue("");
});
});
});