import React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { Board } from "../Board"; import { COLUMNS } from "@fusion/core"; import type { Task } from "@fusion/core"; const fetchBatchMock = vi.fn(); vi.mock("../../hooks/useBatchBadgeFetch", () => ({ useBatchBadgeFetch: vi.fn(() => ({ fetchBatch: fetchBatchMock, isLoading: false, lastFetchTime: null, getBatchData: vi.fn(), })), })); vi.mock("../../api", () => ({ fetchWorkflowSteps: vi.fn().mockResolvedValue([ { id: "WS-003", name: "Accessibility Audit", enabled: true }, ]), })); const columnRenderCounts: Record = {}; // Mock child components so we only test Board's own rendering vi.mock("../Column", () => ({ Column: React.memo(({ column, tasks, onToggleCollapse, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, workflowStepNameLookup }: { column: string; tasks: Task[]; onToggleCollapse?: () => void; favoriteProviders?: string[]; favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void; isSearchActive?: boolean; workflowStepNameLookup?: ReadonlyMap }) => { columnRenderCounts[column] = (columnRenderCounts[column] ?? 0) + 1; return (
{onToggleCollapse && }
); }), })); const noop = () => {}; const noopAsync = () => Promise.resolve({} as any); beforeEach(() => { fetchBatchMock.mockReset(); for (const key of Object.keys(columnRenderCounts)) { delete columnRenderCounts[key]; } }); function createBoardProps(overrides = {}) { return { tasks: [], maxConcurrent: 2, onMoveTask: noopAsync, onOpenDetail: noop, addToast: noop, onQuickCreate: noopAsync, onNewTask: noop, autoMerge: true, onToggleAutoMerge: noop, globalPaused: false, onUpdateTask: undefined, onArchiveTask: undefined, onUnarchiveTask: undefined, ...overrides, }; } function renderBoard(props = {}) { return render(); } describe("Board", () => { it("renders a
element with class 'board'", () => { renderBoard(); const main = screen.getByRole("main"); expect(main).toBeDefined(); expect(main.className).toContain("board"); }); it("renders with id='board' for scroll targeting", () => { renderBoard(); const main = screen.getByRole("main"); expect(main.id).toBe("board"); }); it("FN-4380: does not eagerly fetch GitHub badge status on board mount", () => { vi.useFakeTimers(); try { const tasksWithBadges: Task[] = [ { id: "FN-PR-1", title: "Task with PR badge", description: "Has prInfo", column: "todo", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2024-01-01T00:00:00.000Z", updatedAt: "2024-01-01T00:00:00.000Z", prInfo: { number: 123, owner: "runfusion", repo: "fusion" } as Task["prInfo"], }, { id: "FN-ISSUE-1", title: "Task with issue badge", description: "Has issueInfo", column: "todo", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2024-01-01T00:00:00.000Z", updatedAt: "2024-01-01T00:00:00.000Z", issueInfo: { number: 456, owner: "runfusion", repo: "fusion" } as Task["issueInfo"], }, ]; renderBoard({ tasks: tasksWithBadges }); vi.runAllTimers(); expect(fetchBatchMock).not.toHaveBeenCalled(); } finally { vi.useRealTimers(); } }); it("renders all 6 columns", () => { renderBoard(); for (const col of COLUMNS) { expect(screen.getByTestId(`column-${col}`)).toBeDefined(); } }); it("falls back malformed task columns to triage instead of crashing", () => { const malformedTask = { id: "FN-404", description: "Malformed", column: "impossible-column", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2024-01-01T00:00:00.000Z", updatedAt: "2024-01-01T00:00:00.000Z", } as unknown as Task; expect(() => renderBoard({ tasks: [malformedTask] })).not.toThrow(); const triageTasks = JSON.parse(screen.getByTestId("column-triage").getAttribute("data-tasks") || "[]") as Task[]; expect(triageTasks).toHaveLength(1); expect(triageTasks[0]?.id).toBe("FN-404"); }); it("forwards board-level workflow name lookup to columns", async () => { renderBoard(); await waitFor(() => { for (const col of COLUMNS) { const columnEl = screen.getByTestId(`column-${col}`); expect(columnEl.getAttribute("data-workflow-lookup-size")).toBe("1"); } }); }); it("renders all 6 columns as direct children of .board (CSS selector target)", () => { renderBoard(); const board = screen.getByRole("main"); // The mock Column renders
, which are direct children const directChildren = Array.from(board.children); expect(directChildren).toHaveLength(COLUMNS.length); // Each direct child should be one of the column test-id elements for (const col of COLUMNS) { const colEl = screen.getByTestId(`column-${col}`); expect(colEl.parentElement).toBe(board); } }); it("renders the board element as a
tag (semantic structure)", () => { renderBoard(); const board = screen.getByRole("main"); expect(board.tagName).toBe("MAIN"); }); describe("search functionality", () => { const createTask = (overrides: Partial & { id: string; description: string }): Task => ({ column: "todo", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2024-01-01T00:00:00.000Z", updatedAt: "2024-01-01T00:00:00.000Z", ...overrides, }); it("renders server-filtered tasks by ID when search query is provided", () => { const tasks: Task[] = [ createTask({ id: "FN-001", description: "First task", column: "todo" }), createTask({ id: "FN-002", description: "Second task", column: "todo" }), createTask({ id: "FN-003", description: "Third task", column: "in-progress" }), ]; // Pre-filtered tasks - only FN-002 matches the search const filteredTasks = [tasks[1]]; renderBoard({ tasks: filteredTasks, searchQuery: "FN-002" }); const todoColumn = screen.getByTestId("column-todo"); const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]"); expect(todoTasks).toHaveLength(1); expect(todoTasks[0].id).toBe("FN-002"); const inProgressColumn = screen.getByTestId("column-in-progress"); const inProgressTasks = JSON.parse(inProgressColumn.getAttribute("data-tasks") || "[]"); expect(inProgressTasks).toHaveLength(0); }); it("renders server-filtered tasks by title when search query is provided", () => { const tasks: Task[] = [ createTask({ id: "FN-001", title: "Fix login bug", description: "First task", column: "todo" }), createTask({ id: "FN-002", title: "Add dashboard feature", description: "Second task", column: "todo" }), createTask({ id: "FN-003", title: "Update documentation", description: "Third task", column: "todo" }), ]; // Pre-filtered tasks - only dashboard matches const filteredTasks = [tasks[1]]; renderBoard({ tasks: filteredTasks, 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("FN-002"); }); it("renders server-filtered tasks by description when search query is provided", () => { const tasks: Task[] = [ createTask({ id: "FN-001", description: "Implement user authentication", column: "todo" }), createTask({ id: "FN-002", description: "Fix database connection issue", column: "todo" }), createTask({ id: "FN-003", description: "Add caching layer", column: "todo" }), ]; // Pre-filtered tasks - only database matches const filteredTasks = [tasks[1]]; renderBoard({ tasks: filteredTasks, 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("FN-002"); }); it("search is case-insensitive (server handles this)", () => { const tasks: Task[] = [ createTask({ id: "FN-001", title: "Fix Login Bug", description: "First task", column: "todo" }), createTask({ id: "FN-002", title: "Add Dashboard Feature", description: "Second task", column: "todo" }), ]; // Pre-filtered tasks - only FN-001 matches const filteredTasks = [tasks[0]]; renderBoard({ tasks: filteredTasks, 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("FN-001"); }); it("search is case-insensitive for lowercase query matching uppercase content (server handles this)", () => { const tasks: Task[] = [ createTask({ id: "FN-UPPER", title: "UPPERCASE TITLE", description: "DESC", column: "todo" }), ]; // Pre-filtered tasks - FN-UPPER matches const filteredTasks = [tasks[0]]; renderBoard({ tasks: filteredTasks, 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("FN-UPPER"); }); it("shows all tasks when search query is empty", () => { const tasks: Task[] = [ createTask({ id: "FN-001", description: "First task", column: "todo" }), createTask({ id: "FN-002", description: "Second task", column: "todo" }), createTask({ id: "FN-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 (server returns empty)", () => { const tasks: Task[] = [ createTask({ id: "FN-001", description: "First task", column: "todo" }), createTask({ id: "FN-002", description: "Second task", column: "todo" }), ]; // Pre-filtered tasks - empty array because server found no matches const filteredTasks: Task[] = []; renderBoard({ tasks: filteredTasks, searchQuery: "nonexistent" }); const todoColumn = screen.getByTestId("column-todo"); const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]"); expect(todoTasks).toHaveLength(0); }); it("keeps unaffected columns stable when archived collapse toggles", () => { const tasks: Task[] = [ createTask({ id: "FN-001", description: "Todo task", column: "todo" }), createTask({ id: "FN-002", description: "Archived task", column: "archived" }), ]; renderBoard({ tasks }); const initialTodoRenders = columnRenderCounts.todo; const initialArchivedRenders = columnRenderCounts.archived; fireEvent.click(screen.getByRole("button", { name: "toggle-archived" })); expect(columnRenderCounts.archived).toBeGreaterThan(initialArchivedRenders); expect(columnRenderCounts.todo).toBe(initialTodoRenders); }); it("only re-renders the affected column when a task updates", () => { const tasks: Task[] = [ createTask({ id: "FN-001", description: "Todo task", column: "todo", title: "Original" }), createTask({ id: "FN-002", description: "Done task", column: "done", title: "Done" }), ]; const { rerender } = renderBoard({ tasks }); const initialTodoRenders = columnRenderCounts.todo; const initialDoneRenders = columnRenderCounts.done; rerender( , ); const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]"); expect(todoTasks[0].title).toBe("Updated"); expect(columnRenderCounts.todo).toBeGreaterThan(initialTodoRenders); expect(columnRenderCounts.done).toBeGreaterThanOrEqual(initialDoneRenders); }); describe("column default ordering priority semantics", () => { it("orders done tasks by most recent completion regardless of priority", () => { const tasks: Task[] = [ createTask({ id: "FN-003", description: "Older urgent done task", column: "done", priority: "urgent", columnMovedAt: "2024-01-01T09:00:00.000Z", }), createTask({ id: "FN-001", description: "Newest low-priority done task", column: "done", priority: "low", columnMovedAt: "2024-01-01T11:00:00.000Z", }), createTask({ id: "FN-002", description: "Middle high-priority done task", column: "done", priority: "high", columnMovedAt: "2024-01-01T10:00:00.000Z", }), ]; renderBoard({ tasks }); const doneTasks = JSON.parse(screen.getByTestId("column-done").getAttribute("data-tasks") || "[]") as Task[]; expect(doneTasks.map((t: Task) => t.id)).toEqual(["FN-001", "FN-002", "FN-003"]); }); it("falls back to updatedAt and createdAt for legacy done tasks missing columnMovedAt", () => { const tasks: Task[] = [ createTask({ id: "FN-010", description: "Has updatedAt fallback", column: "done", updatedAt: "2024-01-01T10:30:00.000Z", }), createTask({ id: "FN-011", description: "Has createdAt fallback", column: "done", createdAt: "2024-01-01T10:45:00.000Z", }), createTask({ id: "FN-012", description: "Has real completion timestamp", column: "done", columnMovedAt: "2024-01-01T11:00:00.000Z", }), ]; const taskWithCreatedAtOnly = tasks[1]; delete taskWithCreatedAtOnly.columnMovedAt; delete taskWithCreatedAtOnly.updatedAt; renderBoard({ tasks }); const doneTasks = JSON.parse(screen.getByTestId("column-done").getAttribute("data-tasks") || "[]") as Task[]; expect(doneTasks.map((t: Task) => t.id)).toEqual(["FN-012", "FN-011", "FN-010"]); }); it("orders todo by priority before age", () => { const tasks: Task[] = [ createTask({ id: "FN-003", description: "Low but oldest", column: "todo", priority: "low", createdAt: "2024-01-01T08:00:00.000Z", }), createTask({ id: "FN-001", description: "Urgent but newer", column: "todo", priority: "urgent", createdAt: "2024-01-01T10:00:00.000Z", }), createTask({ id: "FN-004", description: "Normal", column: "todo", priority: "normal", createdAt: "2024-01-01T09:00:00.000Z", }), createTask({ id: "FN-002", description: "High", column: "todo", priority: "high", createdAt: "2024-01-01T07:00:00.000Z", }), ]; renderBoard({ tasks, searchQuery: "task" }); const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]") as Task[]; expect(todoTasks).toHaveLength(4); expect(todoTasks.map((t: Task) => t.id)).toEqual(["FN-001", "FN-002", "FN-004", "FN-003"]); }); it("orders same-priority todo tasks by oldest createdAt first", () => { const tasks: Task[] = [ createTask({ id: "FN-020", description: "Newest", column: "todo", priority: "high", createdAt: "2024-01-01T12:00:00.000Z" }), createTask({ id: "FN-021", description: "Oldest", column: "todo", priority: "high", createdAt: "2024-01-01T09:00:00.000Z" }), createTask({ id: "FN-022", description: "Middle", column: "todo", priority: "high", createdAt: "2024-01-01T10:00:00.000Z" }), ]; renderBoard({ tasks }); const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]") as Task[]; expect(todoTasks.map((t: Task) => t.id)).toEqual(["FN-021", "FN-022", "FN-020"]); }); it("uses task ID as deterministic tie-breaker when todo createdAt matches", () => { const tasks: Task[] = [ createTask({ id: "FN-050", description: "Fifty", column: "todo", priority: "normal", createdAt: "2024-01-01T10:00:00.000Z" }), createTask({ id: "FN-010", description: "Ten", column: "todo", priority: "normal", createdAt: "2024-01-01T10:00:00.000Z" }), createTask({ id: "FN-030", description: "Thirty", column: "todo", priority: "normal", createdAt: "2024-01-01T10:00:00.000Z" }), ]; renderBoard({ tasks }); const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]") as Task[]; expect(todoTasks.map((t: Task) => t.id)).toEqual(["FN-010", "FN-030", "FN-050"]); }); it("keeps non-todo columns on priority then task ID ordering", () => { const tasks: Task[] = [ createTask({ id: "FN-050", description: "Fifty", column: "in-progress", priority: "normal", createdAt: "2024-01-01T12:00:00.000Z" }), createTask({ id: "FN-010", description: "Ten", column: "in-progress", priority: "normal", createdAt: "2024-01-01T09:00:00.000Z" }), createTask({ id: "FN-030", description: "Thirty", column: "in-progress", priority: "normal", createdAt: "2024-01-01T10:00:00.000Z" }), ]; renderBoard({ tasks }); const ipTasks = JSON.parse(screen.getByTestId("column-in-progress").getAttribute("data-tasks") || "[]") as Task[]; expect(ipTasks.map((t: Task) => t.id)).toEqual(["FN-010", "FN-030", "FN-050"]); }); it("normalizes missing and invalid legacy priority values to normal", () => { const noPriorityTask = createTask({ id: "FN-060", description: "No priority", column: "todo" }); delete noPriorityTask.priority; const legacyPriorityTask = { ...createTask({ id: "FN-059", description: "Legacy priority", column: "todo", priority: "normal" }), priority: "critical" as unknown as Task["priority"], }; const tasks: Task[] = [ noPriorityTask, createTask({ id: "FN-061", description: "Explicit normal", column: "todo", priority: "normal" }), legacyPriorityTask, createTask({ id: "FN-062", description: "Urgent", column: "todo", priority: "urgent" }), ]; renderBoard({ tasks }); const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]") as Task[]; // FN-060 (missing), FN-059 (legacy invalid), and FN-061 (explicit normal) normalize to normal, // so they sort by numeric ID ascending after urgent tasks. expect(todoTasks.map((t: Task) => t.id)).toEqual(["FN-062", "FN-059", "FN-060", "FN-061"]); }); it("uses localeCompare fallback for non-numeric task IDs", () => { const tasks: Task[] = [ createTask({ id: "TASK-002", description: "Task two", column: "todo", priority: "normal" }), createTask({ id: "TASK-001", description: "Task one", column: "todo", priority: "normal" }), ]; renderBoard({ tasks }); const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]") as Task[]; // Both have same priority, numeric parse fails (NaN), localeCompare fallback expect(todoTasks.map((t: Task) => t.id)).toEqual(["TASK-001", "TASK-002"]); }); }); describe("column default ordering merging pinning", () => { it("pins merging tasks to top of in-review even when newer non-merging tasks exist", () => { const tasks: Task[] = [ createTask({ id: "FN-010", column: "in-review", status: "merging", columnMovedAt: "2024-01-01T10:00:00.000Z", }), createTask({ id: "FN-011", column: "in-review", status: "review-ready", columnMovedAt: "2024-01-01T12:00:00.000Z", }), ]; renderBoard({ tasks }); const inReviewTasks = JSON.parse(screen.getByTestId("column-in-review").getAttribute("data-tasks") || "[]") as Task[]; expect(inReviewTasks.map((task) => task.id)).toEqual(["FN-010", "FN-011"]); }); it("pins merging-pr tasks to top of in-review even when newer non-merging tasks exist", () => { const tasks: Task[] = [ createTask({ id: "FN-020", column: "in-review", status: "merging-pr", columnMovedAt: "2024-01-01T10:00:00.000Z", }), createTask({ id: "FN-021", column: "in-review", status: "review-ready", columnMovedAt: "2024-01-01T13:00:00.000Z", }), ]; renderBoard({ tasks }); const inReviewTasks = JSON.parse(screen.getByTestId("column-in-review").getAttribute("data-tasks") || "[]") as Task[]; expect(inReviewTasks.map((task) => task.id)).toEqual(["FN-020", "FN-021"]); }); it("pins merging-fix tasks to top of in-review even when newer non-merging tasks exist", () => { const tasks: Task[] = [ createTask({ id: "FN-060", column: "in-review", status: "merging-fix", columnMovedAt: "2024-01-01T10:00:00.000Z", }), createTask({ id: "FN-061", column: "in-review", status: "review-ready", columnMovedAt: "2024-01-01T13:00:00.000Z", }), ]; renderBoard({ tasks }); const inReviewTasks = JSON.parse(screen.getByTestId("column-in-review").getAttribute("data-tasks") || "[]") as Task[]; expect(inReviewTasks.map((task) => task.id)).toEqual(["FN-060", "FN-061"]); }); it("sorts multiple merging tasks by priority then task ID within the pinned group", () => { const tasks: Task[] = [ createTask({ id: "FN-030", column: "in-review", status: "merging", priority: "high", }), createTask({ id: "FN-031", column: "in-review", status: "merging-pr", priority: "urgent", }), createTask({ id: "FN-032", column: "in-review", status: "review-ready", priority: "urgent", }), ]; renderBoard({ tasks }); const inReviewTasks = JSON.parse(screen.getByTestId("column-in-review").getAttribute("data-tasks") || "[]") as Task[]; // Pinned group (merging): FN-031 urgent, FN-030 high — sorted by priority desc // Non-pinned group: FN-032 urgent expect(inReviewTasks.map((task) => task.id)).toEqual(["FN-031", "FN-030", "FN-032"]); }); it("sorts non-in-review columns by priority then task ID regardless of status", () => { const tasks: Task[] = [ createTask({ id: "FN-040", column: "todo", status: "merging", priority: "high", }), createTask({ id: "FN-041", column: "todo", status: "ready", priority: "urgent", }), createTask({ id: "FN-042", column: "todo", status: "ready", priority: "high", }), ]; renderBoard({ tasks }); const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]") as Task[]; // No merge-pinning outside in-review, so pure priority-then-ID sort // FN-041 urgent, then FN-040 and FN-042 both high (sorted by ID asc) expect(todoTasks.map((task) => task.id)).toEqual(["FN-041", "FN-040", "FN-042"]); }); it("sorts tasks without status by priority then task ID in in-review", () => { const statuslessTask = createTask({ id: "FN-050", column: "in-review", priority: "normal", }); delete statuslessTask.status; const tasks: Task[] = [ statuslessTask, createTask({ id: "FN-051", column: "in-review", status: "review-ready", priority: "urgent", }), ]; renderBoard({ tasks }); const inReviewTasks = JSON.parse(screen.getByTestId("column-in-review").getAttribute("data-tasks") || "[]") as Task[]; // Neither is merging, so sort by priority: FN-051 urgent > FN-050 normal expect(inReviewTasks.map((task) => task.id)).toEqual(["FN-051", "FN-050"]); }); }); it("renders server-filtered tasks matching across multiple fields simultaneously", () => { const tasks: Task[] = [ createTask({ id: "SEARCH-123", title: "Searchable title", description: "Normal description", column: "todo" }), createTask({ id: "FN-999", title: "Other task", description: "This has searchable content", column: "todo" }), createTask({ id: "FN-888", title: "Unrelated", description: "No match here", column: "todo" }), ]; // Pre-filtered tasks - only the two matching tasks const filteredTasks = [tasks[0], tasks[1]]; renderBoard({ tasks: filteredTasks, searchQuery: "search" }); const todoColumn = screen.getByTestId("column-todo"); const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]"); // Should have both matching tasks expect(todoTasks).toHaveLength(2); expect(todoTasks.map((t: Task) => t.id).sort()).toEqual(["FN-999", "SEARCH-123"]); }); it("renders server-filtered branch-target results without additional client filtering", () => { const branchFilteredTasks: Task[] = [ createTask({ id: "FN-3428", description: "Task targeting release branch", column: "todo", branch: "feature/fn-3428", baseBranch: "release/2026-05", }), ]; renderBoard({ tasks: branchFilteredTasks, searchQuery: "release/2026-05" }); const todoColumn = screen.getByTestId("column-todo"); const todoTasks = JSON.parse(todoColumn.getAttribute("data-tasks") || "[]") as Task[]; expect(todoTasks).toHaveLength(1); expect(todoTasks[0]?.id).toBe("FN-3428"); expect(todoTasks[0]?.branch).toBe("feature/fn-3428"); expect(todoTasks[0]?.baseBranch).toBe("release/2026-05"); }); it("shows all tasks for whitespace-only search query (server treats as empty)", () => { const tasks: Task[] = [ createTask({ id: "FN-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); }); it("passes isSearchActive=true to columns when search query is non-empty", () => { const tasks: Task[] = [ createTask({ id: "FN-001", description: "First task", column: "todo" }), ]; renderBoard({ tasks, searchQuery: "first" }); for (const col of COLUMNS) { const columnEl = screen.getByTestId(`column-${col}`); expect(columnEl.getAttribute("data-is-search-active")).toBe("true"); } }); it("passes isSearchActive=false to columns when search query is empty", () => { renderBoard({ searchQuery: "" }); for (const col of COLUMNS) { const columnEl = screen.getByTestId(`column-${col}`); expect(columnEl.getAttribute("data-is-search-active")).toBe("false"); } }); it("passes isSearchActive=false to columns when search query is whitespace-only", () => { renderBoard({ searchQuery: " " }); for (const col of COLUMNS) { const columnEl = screen.getByTestId(`column-${col}`); expect(columnEl.getAttribute("data-is-search-active")).toBe("false"); } }); }); it("does not render a .board-project-context badge", () => { renderBoard(); const badge = document.querySelector(".board-project-context"); expect(badge).toBeNull(); }); describe("favorite model prop forwarding (FN-770)", () => { it("forwards favoriteProviders and favoriteModels to all columns", () => { const favoriteProviders = ["anthropic"]; const favoriteModels = ["claude-sonnet-4-5"]; const onToggleFavorite = vi.fn(); const onToggleModelFavorite = vi.fn(); renderBoard({ favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, }); // Every column should receive the favorite props for (const col of COLUMNS) { const columnEl = screen.getByTestId(`column-${col}`); expect(columnEl.getAttribute("data-favorite-providers")).toBe(JSON.stringify(favoriteProviders)); expect(columnEl.getAttribute("data-favorite-models")).toBe(JSON.stringify(favoriteModels)); expect(columnEl.getAttribute("data-has-toggle-favorite")).toBe("yes"); expect(columnEl.getAttribute("data-has-toggle-model-favorite")).toBe("yes"); } }); it("passes empty arrays for favorites when not provided", () => { renderBoard(); for (const col of COLUMNS) { const columnEl = screen.getByTestId(`column-${col}`); expect(columnEl.getAttribute("data-favorite-providers")).toBe("[]"); expect(columnEl.getAttribute("data-favorite-models")).toBe("[]"); expect(columnEl.getAttribute("data-has-toggle-favorite")).toBe("no"); expect(columnEl.getAttribute("data-has-toggle-model-favorite")).toBe("no"); } }); }); });