feat(#37-41): Add domains, ideas, relationships, agents, widgets schema
Schema additions for issues #37-41: New models: - Domain (#37): Life domains (work, marriage, homelab, etc.) - Idea (#38): Brain dumps with pgvector embeddings - Relationship (#39): Generic entity linking (blocks, depends_on) - Agent (#40): ClawdBot agent tracking with metrics - AgentSession (#40): Conversation session tracking - WidgetDefinition (#41): HUD widget registry - UserLayout (#41): Per-user dashboard configuration Updated models: - Task, Event, Project: Added domainId foreign key - User, Workspace: Added new relations New enums: - IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED - RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc. - AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED - EntityType: Added IDEA, DOMAIN Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
227
apps/web/src/components/tasks/TaskItem.test.tsx
Normal file
227
apps/web/src/components/tasks/TaskItem.test.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { TaskItem } from "./TaskItem";
|
||||
import { TaskStatus, TaskPriority, type Task } from "@mosaic/shared";
|
||||
|
||||
describe("TaskItem", () => {
|
||||
const baseTask: Task = {
|
||||
id: "task-1",
|
||||
title: "Test task",
|
||||
description: "Task description",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
dueDate: new Date("2026-01-29"),
|
||||
creatorId: "user-1",
|
||||
assigneeId: "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"),
|
||||
};
|
||||
|
||||
it("should render task title", () => {
|
||||
render(<TaskItem task={baseTask} />);
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render task description when present", () => {
|
||||
render(<TaskItem task={baseTask} />);
|
||||
expect(screen.getByText("Task description")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show status indicator for active task", () => {
|
||||
render(<TaskItem task={{ ...baseTask, status: TaskStatus.IN_PROGRESS }} />);
|
||||
expect(screen.getByText("🟢")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show status indicator for not started task", () => {
|
||||
render(<TaskItem task={{ ...baseTask, status: TaskStatus.NOT_STARTED }} />);
|
||||
expect(screen.getByText("⚪")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show status indicator for paused task", () => {
|
||||
render(<TaskItem task={{ ...baseTask, status: TaskStatus.PAUSED }} />);
|
||||
expect(screen.getByText("⏸️")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display priority badge", () => {
|
||||
render(<TaskItem task={{ ...baseTask, priority: TaskPriority.HIGH }} />);
|
||||
expect(screen.getByText("High priority")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not use demanding language", () => {
|
||||
const { container } = render(<TaskItem task={baseTask} />);
|
||||
const text = container.textContent;
|
||||
expect(text).not.toMatch(/overdue/i);
|
||||
expect(text).not.toMatch(/urgent/i);
|
||||
expect(text).not.toMatch(/must/i);
|
||||
expect(text).not.toMatch(/critical/i);
|
||||
});
|
||||
|
||||
it("should show 'Target passed' for past due dates", () => {
|
||||
const pastTask = {
|
||||
...baseTask,
|
||||
dueDate: new Date("2026-01-27"), // Past date
|
||||
};
|
||||
render(<TaskItem task={pastTask} />);
|
||||
expect(screen.getByText(/target passed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'Approaching target' for near due dates", () => {
|
||||
const soonTask = {
|
||||
...baseTask,
|
||||
dueDate: new Date(Date.now() + 12 * 60 * 60 * 1000), // 12 hours from now
|
||||
};
|
||||
render(<TaskItem task={soonTask} />);
|
||||
expect(screen.getByText(/approaching target/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("error states", () => {
|
||||
it("should handle task with missing title", () => {
|
||||
const taskWithoutTitle = {
|
||||
...baseTask,
|
||||
title: "",
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={taskWithoutTitle} />);
|
||||
// Should render without crashing, even with empty title
|
||||
expect(container.querySelector(".bg-white")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with missing description", () => {
|
||||
const taskWithoutDescription = {
|
||||
...baseTask,
|
||||
description: null,
|
||||
};
|
||||
|
||||
render(<TaskItem task={taskWithoutDescription} />);
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
// Description paragraph should not be rendered when null
|
||||
expect(screen.queryByText("Task description")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with invalid status", () => {
|
||||
const taskWithInvalidStatus = {
|
||||
...baseTask,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
status: "invalid-status" as any,
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={taskWithInvalidStatus} />);
|
||||
// Should render without crashing even with invalid status
|
||||
expect(container.querySelector(".bg-white")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with invalid priority", () => {
|
||||
const taskWithInvalidPriority = {
|
||||
...baseTask,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
priority: "invalid-priority" as any,
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={taskWithInvalidPriority} />);
|
||||
// Should render without crashing even with invalid priority
|
||||
expect(container.querySelector(".bg-white")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with missing dueDate", () => {
|
||||
const taskWithoutDueDate = {
|
||||
...baseTask,
|
||||
dueDate: null,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
render(<TaskItem task={taskWithoutDueDate as any} />);
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with invalid dueDate", () => {
|
||||
const taskWithInvalidDate = {
|
||||
...baseTask,
|
||||
dueDate: new Date("invalid-date"),
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={taskWithInvalidDate} />);
|
||||
expect(container.querySelector(".bg-white")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with very long title", () => {
|
||||
const longTitle = "A".repeat(500);
|
||||
const taskWithLongTitle = {
|
||||
...baseTask,
|
||||
title: longTitle,
|
||||
};
|
||||
|
||||
render(<TaskItem task={taskWithLongTitle} />);
|
||||
expect(screen.getByText(longTitle)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with special characters in title", () => {
|
||||
const taskWithSpecialChars = {
|
||||
...baseTask,
|
||||
title: '<img src="x" onerror="alert(1)">',
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={taskWithSpecialChars} />);
|
||||
// Should render escaped HTML entities, not execute
|
||||
// React escapes to <img... > which is safe
|
||||
expect(container.innerHTML).toContain("<img");
|
||||
expect(container.innerHTML).not.toContain("<img src=");
|
||||
// Text should be displayed as-is
|
||||
expect(screen.getByText(/<img src="x" onerror="alert\(1\)">/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with HTML in description", () => {
|
||||
const taskWithHtmlDesc = {
|
||||
...baseTask,
|
||||
description: '<b>Bold text</b><script>alert("xss")</script>',
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={taskWithHtmlDesc} />);
|
||||
// Should render as text, not HTML - React escapes by default
|
||||
expect(container.innerHTML).not.toContain("<script>");
|
||||
// Text should be displayed as-is
|
||||
expect(screen.getByText(/Bold text/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with missing required IDs", () => {
|
||||
const taskWithMissingIds = {
|
||||
...baseTask,
|
||||
id: "",
|
||||
workspaceId: "",
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={taskWithMissingIds} />);
|
||||
expect(container.querySelector(".bg-white")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with extremely old due date", () => {
|
||||
const veryOldTask = {
|
||||
...baseTask,
|
||||
dueDate: new Date("1970-01-01"),
|
||||
};
|
||||
|
||||
render(<TaskItem task={veryOldTask} />);
|
||||
expect(screen.getByText(/target passed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle task with far future due date", () => {
|
||||
const farFutureTask = {
|
||||
...baseTask,
|
||||
dueDate: new Date("2099-12-31"),
|
||||
};
|
||||
|
||||
const { container } = render(<TaskItem task={farFutureTask} />);
|
||||
expect(container.querySelector(".bg-white")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user