test(KB-120): add search filter tests for Header and Board
- Add Header search tests for filter functionality (48 lines) - Add Board search filter tests for task filtering (203 lines) - Remove unused code from TaskDetailModal component - Cover search input rendering and filter behavior for both components
This commit is contained in:
@@ -190,4 +190,52 @@ describe("Header", () => {
|
|||||||
expect(screen.getByTestId("planning-btn")).toBeDefined();
|
expect(screen.getByTestId("planning-btn")).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("search functionality", () => {
|
||||||
|
it("does not render search input when onSearchChange is not provided", () => {
|
||||||
|
renderHeader({ view: "board" });
|
||||||
|
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders search input when onSearchChange and view='board' are provided", () => {
|
||||||
|
renderHeader({ onSearchChange: vi.fn(), view: "board" });
|
||||||
|
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render search input when view is 'list'", () => {
|
||||||
|
renderHeader({ onSearchChange: vi.fn(), view: "list" });
|
||||||
|
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onSearchChange when typing in search input", () => {
|
||||||
|
const onSearchChange = vi.fn();
|
||||||
|
renderHeader({ onSearchChange, view: "board" });
|
||||||
|
const input = screen.getByPlaceholderText("Search tasks...");
|
||||||
|
fireEvent.change(input, { target: { value: "test query" } });
|
||||||
|
expect(onSearchChange).toHaveBeenCalledWith("test query");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows clear button when search query is not empty", () => {
|
||||||
|
renderHeader({ onSearchChange: vi.fn(), view: "board", searchQuery: "test" });
|
||||||
|
expect(screen.getByLabelText("Clear search")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show clear button when search query is empty", () => {
|
||||||
|
renderHeader({ onSearchChange: vi.fn(), view: "board", searchQuery: "" });
|
||||||
|
expect(screen.queryByLabelText("Clear search")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onSearchChange with empty string when clear button is clicked", () => {
|
||||||
|
const onSearchChange = vi.fn();
|
||||||
|
renderHeader({ onSearchChange, view: "board", searchQuery: "test" });
|
||||||
|
fireEvent.click(screen.getByLabelText("Clear search"));
|
||||||
|
expect(onSearchChange).toHaveBeenCalledWith("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("search input has correct placeholder text", () => {
|
||||||
|
renderHeader({ onSearchChange: vi.fn(), view: "board" });
|
||||||
|
const input = screen.getByPlaceholderText("Search tasks...");
|
||||||
|
expect(input).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ import { render, screen } from "@testing-library/react";
|
|||||||
import { Board } from "../Board";
|
import { Board } from "../Board";
|
||||||
import { COLUMNS } from "@kb/core";
|
import { COLUMNS } from "@kb/core";
|
||||||
|
|
||||||
|
import type { Task } from "@kb/core";
|
||||||
|
|
||||||
// Mock child components so we only test Board's own rendering
|
// Mock child components so we only test Board's own rendering
|
||||||
vi.mock("../Column", () => ({
|
vi.mock("../Column", () => ({
|
||||||
Column: ({ column }: { column: string }) => (
|
Column: ({ column, tasks }: { column: string; tasks: Task[] }) => (
|
||||||
<div data-testid={`column-${column}`} />
|
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)} />
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
const noopAsync = () => Promise.resolve({} as any);
|
const noopAsync = () => Promise.resolve({} as any);
|
||||||
|
|
||||||
function renderBoard() {
|
function renderBoard(props = {}) {
|
||||||
return render(
|
return render(
|
||||||
<Board
|
<Board
|
||||||
tasks={[]}
|
tasks={[]}
|
||||||
@@ -26,6 +28,7 @@ function renderBoard() {
|
|||||||
autoMerge={true}
|
autoMerge={true}
|
||||||
onToggleAutoMerge={noop}
|
onToggleAutoMerge={noop}
|
||||||
globalPaused={false}
|
globalPaused={false}
|
||||||
|
{...props}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,4 +72,198 @@ describe("Board", () => {
|
|||||||
const board = screen.getByRole("main");
|
const board = screen.getByRole("main");
|
||||||
expect(board.tagName).toBe("MAIN");
|
expect(board.tagName).toBe("MAIN");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("search functionality", () => {
|
||||||
|
const createTask = (overrides: Partial<Task> & { id: string; description: string }): Task => ({
|
||||||
|
id: overrides.id,
|
||||||
|
title: overrides.title,
|
||||||
|
description: overrides.description,
|
||||||
|
column: overrides.column ?? "todo",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
createdAt: "2024-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters tasks by ID when search query is provided", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-001", description: "First task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-002", description: "Second task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-003", description: "Third task", column: "in-progress" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "KB-002" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(todoTasks).toHaveLength(1);
|
||||||
|
expect(todoTasks[0].id).toBe("KB-002");
|
||||||
|
|
||||||
|
const inProgressColumn = screen.getByTestId("column-in-progress");
|
||||||
|
const inProgressTasks = JSON.parse(inProgressColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(inProgressTasks).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters tasks by title when search query is provided", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-001", title: "Fix login bug", description: "First task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-002", title: "Add dashboard feature", description: "Second task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-003", title: "Update documentation", description: "Third task", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "dashboard" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(todoTasks).toHaveLength(1);
|
||||||
|
expect(todoTasks[0].id).toBe("KB-002");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters tasks by description when search query is provided", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-001", description: "Implement user authentication", column: "todo" }),
|
||||||
|
createTask({ id: "KB-002", description: "Fix database connection issue", column: "todo" }),
|
||||||
|
createTask({ id: "KB-003", description: "Add caching layer", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "database" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(todoTasks).toHaveLength(1);
|
||||||
|
expect(todoTasks[0].id).toBe("KB-002");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("search is case-insensitive", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-001", title: "Fix Login Bug", description: "First task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-002", title: "Add Dashboard Feature", description: "Second task", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "login" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(todoTasks).toHaveLength(1);
|
||||||
|
expect(todoTasks[0].id).toBe("KB-001");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("search is case-insensitive for lowercase query matching uppercase content", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-UPPER", title: "UPPERCASE TITLE", description: "DESC", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "upper" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(todoTasks).toHaveLength(1);
|
||||||
|
expect(todoTasks[0].id).toBe("KB-UPPER");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows all tasks when search query is empty", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-001", description: "First task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-002", description: "Second task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-003", description: "Third task", column: "in-progress" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(todoTasks).toHaveLength(2);
|
||||||
|
|
||||||
|
const inProgressColumn = screen.getByTestId("column-in-progress");
|
||||||
|
const inProgressTasks = JSON.parse(inProgressColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(inProgressTasks).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows no tasks when search query matches nothing", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-001", description: "First task", column: "todo" }),
|
||||||
|
createTask({ id: "KB-002", description: "Second task", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "nonexistent" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
expect(todoTasks).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filtered tasks are sorted correctly (columnMovedAt, createdAt)", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({
|
||||||
|
id: "KB-001",
|
||||||
|
description: "Old task with move time",
|
||||||
|
column: "todo",
|
||||||
|
columnMovedAt: "2024-01-01T10:00:00.000Z",
|
||||||
|
createdAt: "2024-01-01T08:00:00.000Z",
|
||||||
|
}),
|
||||||
|
createTask({
|
||||||
|
id: "KB-002",
|
||||||
|
description: "Newer task with move time",
|
||||||
|
column: "todo",
|
||||||
|
columnMovedAt: "2024-01-01T12:00:00.000Z",
|
||||||
|
createdAt: "2024-01-01T08:00:00.000Z",
|
||||||
|
}),
|
||||||
|
createTask({
|
||||||
|
id: "KB-003",
|
||||||
|
description: "Legacy task no move time",
|
||||||
|
column: "todo",
|
||||||
|
createdAt: "2024-01-01T09:00:00.000Z",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "task" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]") as Task[];
|
||||||
|
|
||||||
|
// Should have all 3 tasks
|
||||||
|
expect(todoTasks).toHaveLength(3);
|
||||||
|
|
||||||
|
// Tasks with columnMovedAt should come first, sorted by columnMovedAt descending (newest first)
|
||||||
|
// So KB-002 (12:00) should be first, KB-001 (10:00) second
|
||||||
|
// Legacy tasks (no columnMovedAt) come last, sorted by createdAt ascending
|
||||||
|
expect(todoTasks[0].id).toBe("KB-002");
|
||||||
|
expect(todoTasks[1].id).toBe("KB-001");
|
||||||
|
expect(todoTasks[2].id).toBe("KB-003");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches tasks across multiple fields simultaneously", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "SEARCH-123", title: "Searchable title", description: "Normal description", column: "todo" }),
|
||||||
|
createTask({ id: "KB-999", title: "Other task", description: "This has searchable content", column: "todo" }),
|
||||||
|
createTask({ id: "KB-888", title: "Unrelated", description: "No match here", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: "search" });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
|
||||||
|
// Should match both tasks with "search" in ID, title, or description
|
||||||
|
expect(todoTasks).toHaveLength(2);
|
||||||
|
expect(todoTasks.map((t: Task) => t.id).sort()).toEqual(["KB-999", "SEARCH-123"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims whitespace from search query", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
createTask({ id: "KB-001", description: "First task", column: "todo" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderBoard({ tasks, searchQuery: " " });
|
||||||
|
|
||||||
|
const todoColumn = screen.getByTestId("column-todo");
|
||||||
|
const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]");
|
||||||
|
|
||||||
|
// Whitespace-only query should be treated as empty, showing all tasks
|
||||||
|
expect(todoTasks).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user