feat(#41): implement Widget/HUD system

- BaseWidget wrapper with loading/error states
- WidgetRegistry for central widget management
- WidgetGrid with react-grid-layout integration
- TasksWidget, CalendarWidget, QuickCaptureWidget
- useLayouts hooks for layout persistence
- Comprehensive test suite (TDD approach)
This commit is contained in:
Jason Woltje
2026-01-29 17:54:46 -06:00
parent 95833fb4ea
commit 14a1e218a5
14 changed files with 2142 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
/**
* CalendarWidget Component Tests
* Following TDD principles
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { CalendarWidget } from "../CalendarWidget";
global.fetch = vi.fn();
describe("CalendarWidget", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should render loading state initially", () => {
(global.fetch as any).mockImplementation(() => new Promise(() => {}));
render(<CalendarWidget id="calendar-1" />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it("should render upcoming events", async () => {
const mockEvents = [
{
id: "1",
title: "Team Meeting",
startTime: new Date(Date.now() + 3600000).toISOString(),
endTime: new Date(Date.now() + 7200000).toISOString(),
},
{
id: "2",
title: "Project Review",
startTime: new Date(Date.now() + 86400000).toISOString(),
endTime: new Date(Date.now() + 90000000).toISOString(),
},
];
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockEvents,
});
render(<CalendarWidget id="calendar-1" />);
await waitFor(() => {
expect(screen.getByText("Team Meeting")).toBeInTheDocument();
expect(screen.getByText("Project Review")).toBeInTheDocument();
});
});
it("should handle empty event list", async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => [],
});
render(<CalendarWidget id="calendar-1" />);
await waitFor(() => {
expect(screen.getByText(/no upcoming events/i)).toBeInTheDocument();
});
});
it("should handle API errors gracefully", async () => {
(global.fetch as any).mockRejectedValueOnce(new Error("API Error"));
render(<CalendarWidget id="calendar-1" />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
it("should format event times correctly", async () => {
const now = new Date();
const startTime = new Date(now.getTime() + 3600000); // 1 hour from now
const mockEvents = [
{
id: "1",
title: "Meeting",
startTime: startTime.toISOString(),
endTime: new Date(startTime.getTime() + 3600000).toISOString(),
},
];
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockEvents,
});
render(<CalendarWidget id="calendar-1" />);
await waitFor(() => {
expect(screen.getByText("Meeting")).toBeInTheDocument();
// Should show time in readable format
});
});
it("should display current date", async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => [],
});
render(<CalendarWidget id="calendar-1" />);
await waitFor(() => {
const currentDate = new Date().toLocaleDateString();
// Widget should display current date or month
expect(screen.getByTestId("calendar-header")).toBeInTheDocument();
});
});
});