From 240566646ff3c40654f952b187d6a0073c295182 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 1 Mar 2026 15:41:22 -0600 Subject: [PATCH] fix(web): resolve lint errors from PR #632 - prettier, catch type, eslint-disable for test assertions --- .../src/app/(authenticated)/files/page.tsx | 12 +- .../app/(authenticated)/kanban/page.test.tsx | 188 ++++++++++++++++++ 2 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/app/(authenticated)/kanban/page.test.tsx diff --git a/apps/web/src/app/(authenticated)/files/page.tsx b/apps/web/src/app/(authenticated)/files/page.tsx index ec31624..75dae38 100644 --- a/apps/web/src/app/(authenticated)/files/page.tsx +++ b/apps/web/src/app/(authenticated)/files/page.tsx @@ -26,11 +26,7 @@ import { DialogFooter, } from "@/components/ui/dialog"; import { fetchEntries, createEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge"; -import type { - EntriesResponse, - CreateEntryData, - EntryFilters, -} from "@/lib/api/knowledge"; +import type { EntriesResponse, CreateEntryData, EntryFilters } from "@/lib/api/knowledge"; /* --------------------------------------------------------------------------- Helpers @@ -439,7 +435,7 @@ function CreateEntryDialog({ .then((tags) => { setAvailableTags(tags); }) - .catch((err) => { + .catch((err: unknown) => { console.error("Failed to load tags:", err); }); } @@ -730,7 +726,9 @@ function CreateEntryDialog({ }} onBlur={() => { // Delay to allow click on suggestion - setTimeout(() => setShowSuggestions(false), 150); + setTimeout(() => { + setShowSuggestions(false); + }, 150); }} onFocus={() => { if (tagInput.length > 0) setShowSuggestions(true); diff --git a/apps/web/src/app/(authenticated)/kanban/page.test.tsx b/apps/web/src/app/(authenticated)/kanban/page.test.tsx new file mode 100644 index 0000000..9b98cc3 --- /dev/null +++ b/apps/web/src/app/(authenticated)/kanban/page.test.tsx @@ -0,0 +1,188 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { Task } from "@mosaic/shared"; +import { TaskPriority, TaskStatus } from "@mosaic/shared"; +import KanbanPage from "./page"; + +const mockReplace = vi.fn(); +let mockSearchParams = new URLSearchParams(); + +vi.mock("next/navigation", () => ({ + useRouter: (): { replace: typeof mockReplace } => ({ replace: mockReplace }), + useSearchParams: (): URLSearchParams => mockSearchParams, +})); + +vi.mock("@hello-pangea/dnd", () => ({ + DragDropContext: ({ children }: { children: React.ReactNode }): React.JSX.Element => ( +
{children}
+ ), + Droppable: ({ + children, + droppableId, + }: { + children: (provided: { + innerRef: (el: HTMLElement | null) => void; + droppableProps: Record; + placeholder: React.ReactNode; + }) => React.ReactNode; + droppableId: string; + }): React.JSX.Element => ( +
+ {children({ + innerRef: () => { + /* noop */ + }, + droppableProps: {}, + placeholder: null, + })} +
+ ), + Draggable: ({ + children, + draggableId, + }: { + children: ( + provided: { + innerRef: (el: HTMLElement | null) => void; + draggableProps: { style: Record }; + dragHandleProps: Record; + }, + snapshot: { isDragging: boolean } + ) => React.ReactNode; + draggableId: string; + index: number; + }): React.JSX.Element => ( +
+ {children( + { + innerRef: () => { + /* noop */ + }, + draggableProps: { style: {} }, + dragHandleProps: {}, + }, + { isDragging: false } + )} +
+ ), +})); + +vi.mock("@/components/ui/MosaicSpinner", () => ({ + MosaicSpinner: ({ label }: { label?: string }): React.JSX.Element => ( +
{label ?? "Loading..."}
+ ), +})); + +const mockUseWorkspaceId = vi.fn<() => string | null>(); +vi.mock("@/lib/hooks", () => ({ + useWorkspaceId: (): string | null => mockUseWorkspaceId(), +})); + +const mockFetchTasks = vi.fn<() => Promise>(); +const mockUpdateTask = vi.fn<() => Promise>(); +const mockCreateTask = vi.fn<() => Promise>(); +vi.mock("@/lib/api/tasks", () => ({ + fetchTasks: (...args: unknown[]): Promise => mockFetchTasks(...(args as [])), + updateTask: (...args: unknown[]): Promise => mockUpdateTask(...(args as [])), + createTask: (...args: unknown[]): Promise => mockCreateTask(...(args as [])), +})); + +const mockFetchProjects = vi.fn<() => Promise>(); +vi.mock("@/lib/api/projects", () => ({ + fetchProjects: (...args: unknown[]): Promise => mockFetchProjects(...(args as [])), +})); + +const createdTask: Task = { + id: "task-new-1", + title: "Ship Kanban add task flow", + description: null, + status: TaskStatus.NOT_STARTED, + priority: TaskPriority.MEDIUM, + dueDate: null, + creatorId: "user-1", + assigneeId: null, + workspaceId: "ws-1", + projectId: "project-42", + parentId: null, + sortOrder: 0, + metadata: {}, + completedAt: null, + createdAt: new Date("2026-03-01"), + updatedAt: new Date("2026-03-01"), +}; + +describe("KanbanPage add task flow", (): void => { + beforeEach((): void => { + vi.clearAllMocks(); + mockSearchParams = new URLSearchParams("project=project-42"); + mockUseWorkspaceId.mockReturnValue("ws-1"); + mockFetchTasks.mockResolvedValue([]); + mockFetchProjects.mockResolvedValue([]); + mockCreateTask.mockResolvedValue(createdTask); + }); + + it("opens add-task form in a column and creates a task via API", async (): Promise => { + const user = userEvent.setup(); + + render(); + + await waitFor((): void => { + expect(screen.getByText("Kanban Board")).toBeInTheDocument(); + }); + + // Click the "+ Add task" button in the To Do column + const addTaskButtons = screen.getAllByRole("button", { name: /\+ Add task/i }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await user.click(addTaskButtons[0]!); // First column is "To Do" + + // Type in the title input + const titleInput = screen.getByPlaceholderText("Task title..."); + await user.type(titleInput, createdTask.title); + + // Click the Add button + await user.click(screen.getByRole("button", { name: /✓ Add/i })); + + await waitFor((): void => { + expect(mockCreateTask).toHaveBeenCalledWith( + expect.objectContaining({ + title: createdTask.title, + status: TaskStatus.NOT_STARTED, + projectId: "project-42", + }), + "ws-1" + ); + }); + }); + + it("cancels add-task form when pressing Escape", async (): Promise => { + const user = userEvent.setup(); + + render(); + + await waitFor((): void => { + expect(screen.getByText("Kanban Board")).toBeInTheDocument(); + }); + + // Click the "+ Add task" button + const addTaskButtons = screen.getAllByRole("button", { name: /\+ Add task/i }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await user.click(addTaskButtons[0]!); + + // Type in the title input + const titleInput = screen.getByPlaceholderText("Task title..."); + await user.type(titleInput, "Test task"); + + // Press Escape to cancel + await user.keyboard("{Escape}"); + + // Form should be closed, back to "+ Add task" button + await waitFor((): void => { + const buttons = screen.getAllByRole("button", { name: /\+ Add task/i }); + expect(buttons.length).toBe(5); // One per column + }); + + // Should not have called createTask + expect(mockCreateTask).not.toHaveBeenCalled(); + }); +}); -- 2.49.1