feat(KB-156): add search input to dependency dropdowns
- Add searchable text input to TaskDetailModal dependency dropdown with filtering by id, title, and description - Add matching search input to InlineCreateCard dependency dropdown - Add sticky .dep-dropdown-search CSS with focus styling - Reset search term when dropdown closes and reopens - Add tests for search filtering, case-insensitive matching, empty state, and reset behavior
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { InlineCreateCard } from "../InlineCreateCard";
|
||||
import type { Task, Column } from "@kb/core";
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock("lucide-react", () => ({
|
||||
@@ -12,9 +13,9 @@ vi.mock("../../api", () => ({
|
||||
uploadAttachment: vi.fn(),
|
||||
}));
|
||||
|
||||
function renderCard() {
|
||||
function renderCard(tasks: Task[] = []) {
|
||||
const props = {
|
||||
tasks: [],
|
||||
tasks,
|
||||
onSubmit: vi.fn().mockResolvedValue({ id: "KB-001" }),
|
||||
onCancel: vi.fn(),
|
||||
addToast: vi.fn(),
|
||||
@@ -65,3 +66,30 @@ describe("InlineCreateCard blur-to-cancel", () => {
|
||||
expect(props.onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("InlineCreateCard dependency dropdown search", () => {
|
||||
const testTasks: Task[] = [
|
||||
{ id: "KB-001", title: "Fix login", description: "Login page broken", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z" },
|
||||
{ id: "KB-002", title: "Add dark mode", description: "Theme support", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-02-01T00:00:00Z", updatedAt: "2026-02-01T00:00:00Z" },
|
||||
{ id: "KB-003", title: "Refactor API", description: "Clean up endpoints", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-03-01T00:00:00Z", updatedAt: "2026-03-01T00:00:00Z" },
|
||||
];
|
||||
|
||||
it("shows search input when dropdown is opened", () => {
|
||||
renderCard(testTasks);
|
||||
fireEvent.click(screen.getByText(/Deps/));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
expect(input).toBeTruthy();
|
||||
expect(input.placeholder).toBe("Search tasks…");
|
||||
});
|
||||
|
||||
it("filters tasks by search term", () => {
|
||||
renderCard(testTasks);
|
||||
fireEvent.click(screen.getByText(/Deps/));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "dark" } });
|
||||
|
||||
const items = document.querySelectorAll(".dep-dropdown-item");
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].querySelector(".dep-dropdown-id")?.textContent).toBe("KB-002");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -619,4 +619,98 @@ describe("TaskDetailModal", () => {
|
||||
expect(afterSwitch[1]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dependency dropdown search", () => {
|
||||
const searchTasks: Task[] = [
|
||||
{ id: "KB-010", title: "Fix login bug", description: "Users cannot log in", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z" },
|
||||
{ id: "KB-020", title: "Add dark mode", description: "Theme support", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-02-01T00:00:00Z", updatedAt: "2026-02-01T00:00:00Z" },
|
||||
{ id: "KB-030", title: "Refactor API", description: "Clean up endpoints", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-03-01T00:00:00Z", updatedAt: "2026-03-01T00:00:00Z" },
|
||||
{ id: "KB-099", description: "Self", column: "in-progress" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-03-15T00:00:00Z", updatedAt: "2026-03-15T00:00:00Z" },
|
||||
];
|
||||
|
||||
function renderWithSearch(taskOverrides: Partial<TaskDetail> = {}) {
|
||||
return render(
|
||||
<TaskDetailModal
|
||||
task={makeTask(taskOverrides)}
|
||||
tasks={searchTasks}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
it("shows search input when dropdown is opened", () => {
|
||||
renderWithSearch();
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
expect(input).toBeTruthy();
|
||||
expect(input.placeholder).toBe("Search tasks…");
|
||||
});
|
||||
|
||||
it("filters tasks by search term", () => {
|
||||
renderWithSearch();
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "login" } });
|
||||
|
||||
const items = document.querySelectorAll(".dep-dropdown-item");
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].querySelector(".dep-dropdown-id")?.textContent).toBe("KB-010");
|
||||
});
|
||||
|
||||
it("matches task ID case-insensitively", () => {
|
||||
renderWithSearch();
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "kb-020" } });
|
||||
|
||||
const items = document.querySelectorAll(".dep-dropdown-item");
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].querySelector(".dep-dropdown-id")?.textContent).toBe("KB-020");
|
||||
});
|
||||
|
||||
it("matches task title", () => {
|
||||
renderWithSearch();
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "dark mode" } });
|
||||
|
||||
const items = document.querySelectorAll(".dep-dropdown-item");
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].querySelector(".dep-dropdown-id")?.textContent).toBe("KB-020");
|
||||
});
|
||||
|
||||
it("shows empty state when search matches nothing", () => {
|
||||
renderWithSearch();
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "zzz-nonexistent" } });
|
||||
|
||||
const items = document.querySelectorAll(".dep-dropdown-item");
|
||||
expect(items).toHaveLength(0);
|
||||
expect(document.querySelector(".dep-dropdown-empty")?.textContent).toBe("No available tasks");
|
||||
});
|
||||
|
||||
it("resets search when dropdown closes and reopens", () => {
|
||||
renderWithSearch();
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "login" } });
|
||||
expect(input.value).toBe("login");
|
||||
|
||||
// Close by clicking again
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
expect(document.querySelector(".dep-dropdown")).toBeNull();
|
||||
|
||||
// Reopen
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
const newInput = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
|
||||
expect(newInput.value).toBe("");
|
||||
// All items visible again
|
||||
expect(document.querySelectorAll(".dep-dropdown-item")).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user