import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { TaskCard } from "./task-card"; import type { Task } from "@mosaic/shared"; import { TaskStatus, TaskPriority } from "@mosaic/shared"; // Mock @dnd-kit/sortable vi.mock("@dnd-kit/sortable", () => ({ useSortable: () => ({ attributes: {}, listeners: {}, setNodeRef: vi.fn(), transform: null, transition: null, isDragging: false, }), })); const mockTask: Task = { id: "task-1", title: "Complete project documentation", description: "Write comprehensive docs for the API", status: TaskStatus.IN_PROGRESS, priority: TaskPriority.HIGH, dueDate: new Date("2026-02-01"), assigneeId: "user-1", creatorId: "user-1", workspaceId: "workspace-1", projectId: null, parentId: null, sortOrder: 0, metadata: {}, completedAt: null, createdAt: new Date("2026-01-28"), updatedAt: new Date("2026-01-28"), }; describe("TaskCard", () => { describe("Rendering", () => { it("should render task title", () => { render(); expect(screen.getByText("Complete project documentation")).toBeInTheDocument(); }); it("should render as an article element for semantic HTML", () => { render(); const card = screen.getByRole("article"); expect(card).toBeInTheDocument(); }); it("should display task priority", () => { render(); expect(screen.getByText("High")).toBeInTheDocument(); }); it("should display due date when available", () => { render(); // Check for formatted date (format: "Feb 1" or similar) const dueDateElement = screen.getByText(/Feb 1/); expect(dueDateElement).toBeInTheDocument(); }); it("should not display due date when null", () => { const taskWithoutDueDate = { ...mockTask, dueDate: null }; render(); // Should not show any date expect(screen.queryByText(/Feb/)).not.toBeInTheDocument(); }); it("should truncate long titles gracefully", () => { const longTask = { ...mockTask, title: "This is a very long task title that should be truncated to prevent layout issues", }; const { container } = render(); const titleElement = container.querySelector("h4"); expect(titleElement).toBeInTheDocument(); // Should have text truncation classes expect(titleElement?.className).toMatch(/truncate|line-clamp/); }); }); describe("Priority Display", () => { it("should display HIGH priority with appropriate styling", () => { render(); const priorityBadge = screen.getByText("High"); expect(priorityBadge).toBeInTheDocument(); }); it("should display MEDIUM priority", () => { const mediumTask = { ...mockTask, priority: TaskPriority.MEDIUM }; render(); expect(screen.getByText("Medium")).toBeInTheDocument(); }); it("should display LOW priority", () => { const lowTask = { ...mockTask, priority: TaskPriority.LOW }; render(); expect(screen.getByText("Low")).toBeInTheDocument(); }); it("should use calm colors for priority badges (not aggressive red)", () => { const { container } = render(); const priorityBadge = screen.getByText("High").closest("span"); const className = priorityBadge?.className || ""; // Should not use harsh red for high priority expect(className).not.toMatch(/bg-red-[5-9]00|text-red-[5-9]00/); }); }); describe("Due Date Display", () => { it("should format due date in a human-readable way", () => { render(); // Should show month abbreviation and day expect(screen.getByText(/Feb 1/)).toBeInTheDocument(); }); it("should show overdue indicator with calm styling", () => { const overdueTask = { ...mockTask, dueDate: new Date("2025-01-01"), // Past date }; render(); // Should indicate overdue but not in harsh red const dueDateElement = screen.getByText(/Jan 1/); const className = dueDateElement.className; // Should avoid aggressive red expect(className).not.toMatch(/bg-red-[5-9]00|text-red-[5-9]00/); }); it("should show due soon indicator for tasks due within 3 days", () => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const soonTask = { ...mockTask, dueDate: tomorrow, }; const { container } = render(); // Should have some visual indicator (checked via data attribute or aria label) expect(container).toBeInTheDocument(); }); }); describe("Drag and Drop", () => { it("should be draggable", () => { const { container } = render(); const card = container.querySelector('[role="article"]'); expect(card).toBeInTheDocument(); }); it("should have appropriate cursor style for dragging", () => { const { container } = render(); const card = container.querySelector('[role="article"]'); const className = card?.className || ""; // Should have cursor-grab or cursor-move expect(className).toMatch(/cursor-(grab|move)/); }); }); describe("Accessibility", () => { it("should have accessible task card", () => { render(); const card = screen.getByRole("article"); expect(card).toBeInTheDocument(); }); it("should have semantic heading for task title", () => { render(); const heading = screen.getByRole("heading", { level: 4 }); expect(heading).toHaveTextContent("Complete project documentation"); }); it("should provide aria-label for due date icon", () => { const { container } = render(); // Icons should have proper aria labels const icons = container.querySelectorAll("svg"); icons.forEach((icon) => { const ariaLabel = icon.getAttribute("aria-label"); const parentAriaLabel = icon.parentElement?.getAttribute("aria-label"); // Either the icon or its parent should have an aria-label expect(ariaLabel || parentAriaLabel || icon.getAttribute("aria-hidden")).toBeTruthy(); }); }); }); describe("PDA-Friendly Design", () => { it("should not use harsh or demanding language", () => { const { container } = render(); const allText = container.textContent?.toLowerCase() || ""; // Should avoid demanding words expect(allText).not.toMatch(/must|required|urgent|critical|error|alert/); }); it("should use gentle visual design", () => { const { container } = render(); const card = container.querySelector('[role="article"]'); const className = card?.className || ""; // Should have rounded corners and soft shadows expect(className).toMatch(/rounded/); }); }); describe("Compact Mode", () => { it("should handle missing description gracefully", () => { const taskWithoutDescription = { ...mockTask, description: null }; render(); expect(screen.getByText("Complete project documentation")).toBeInTheDocument(); // Description should not be rendered }); }); describe("Error Handling", () => { it("should handle task with minimal data", () => { const minimalTask: Task = { id: "task-minimal", title: "Minimal task", description: null, status: TaskStatus.NOT_STARTED, priority: TaskPriority.MEDIUM, dueDate: null, assigneeId: null, creatorId: "user-1", workspaceId: "workspace-1", projectId: null, parentId: null, sortOrder: 0, metadata: {}, completedAt: null, createdAt: new Date(), updatedAt: new Date(), }; render(); expect(screen.getByText("Minimal task")).toBeInTheDocument(); }); }); describe("Visual Feedback", () => { it("should show hover state with subtle transition", () => { const { container } = render(); const card = container.querySelector('[role="article"]'); const className = card?.className || ""; // Should have hover transition expect(className).toMatch(/transition|hover:/); }); }); });