Compare commits

...

1 Commits

Author SHA1 Message Date
240566646f fix(web): resolve lint errors from PR #632 - prettier, catch type, eslint-disable for test assertions
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-01 15:41:22 -06:00
2 changed files with 193 additions and 7 deletions

View File

@@ -26,11 +26,7 @@ import {
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { fetchEntries, createEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge"; import { fetchEntries, createEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge";
import type { import type { EntriesResponse, CreateEntryData, EntryFilters } from "@/lib/api/knowledge";
EntriesResponse,
CreateEntryData,
EntryFilters,
} from "@/lib/api/knowledge";
/* --------------------------------------------------------------------------- /* ---------------------------------------------------------------------------
Helpers Helpers
@@ -439,7 +435,7 @@ function CreateEntryDialog({
.then((tags) => { .then((tags) => {
setAvailableTags(tags); setAvailableTags(tags);
}) })
.catch((err) => { .catch((err: unknown) => {
console.error("Failed to load tags:", err); console.error("Failed to load tags:", err);
}); });
} }
@@ -730,7 +726,9 @@ function CreateEntryDialog({
}} }}
onBlur={() => { onBlur={() => {
// Delay to allow click on suggestion // Delay to allow click on suggestion
setTimeout(() => setShowSuggestions(false), 150); setTimeout(() => {
setShowSuggestions(false);
}, 150);
}} }}
onFocus={() => { onFocus={() => {
if (tagInput.length > 0) setShowSuggestions(true); if (tagInput.length > 0) setShowSuggestions(true);

View File

@@ -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 => (
<div data-testid="mock-dnd-context">{children}</div>
),
Droppable: ({
children,
droppableId,
}: {
children: (provided: {
innerRef: (el: HTMLElement | null) => void;
droppableProps: Record<string, never>;
placeholder: React.ReactNode;
}) => React.ReactNode;
droppableId: string;
}): React.JSX.Element => (
<div data-testid={`mock-droppable-${droppableId}`}>
{children({
innerRef: () => {
/* noop */
},
droppableProps: {},
placeholder: null,
})}
</div>
),
Draggable: ({
children,
draggableId,
}: {
children: (
provided: {
innerRef: (el: HTMLElement | null) => void;
draggableProps: { style: Record<string, string> };
dragHandleProps: Record<string, string>;
},
snapshot: { isDragging: boolean }
) => React.ReactNode;
draggableId: string;
index: number;
}): React.JSX.Element => (
<div data-testid={`mock-draggable-${draggableId}`}>
{children(
{
innerRef: () => {
/* noop */
},
draggableProps: { style: {} },
dragHandleProps: {},
},
{ isDragging: false }
)}
</div>
),
}));
vi.mock("@/components/ui/MosaicSpinner", () => ({
MosaicSpinner: ({ label }: { label?: string }): React.JSX.Element => (
<div data-testid="mosaic-spinner">{label ?? "Loading..."}</div>
),
}));
const mockUseWorkspaceId = vi.fn<() => string | null>();
vi.mock("@/lib/hooks", () => ({
useWorkspaceId: (): string | null => mockUseWorkspaceId(),
}));
const mockFetchTasks = vi.fn<() => Promise<Task[]>>();
const mockUpdateTask = vi.fn<() => Promise<unknown>>();
const mockCreateTask = vi.fn<() => Promise<Task>>();
vi.mock("@/lib/api/tasks", () => ({
fetchTasks: (...args: unknown[]): Promise<Task[]> => mockFetchTasks(...(args as [])),
updateTask: (...args: unknown[]): Promise<unknown> => mockUpdateTask(...(args as [])),
createTask: (...args: unknown[]): Promise<Task> => mockCreateTask(...(args as [])),
}));
const mockFetchProjects = vi.fn<() => Promise<unknown[]>>();
vi.mock("@/lib/api/projects", () => ({
fetchProjects: (...args: unknown[]): Promise<unknown[]> => 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<void> => {
const user = userEvent.setup();
render(<KanbanPage />);
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<void> => {
const user = userEvent.setup();
render(<KanbanPage />);
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();
});
});