From d0a1b9281c841f8885240e0c7a42d34daa4970f0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 22 Feb 2026 23:03:19 -0600 Subject: [PATCH] fix(web): update calendar and knowledge tests for real API integration Calendar page and KnowledgeGraphViewer tests were broken after PRs #474 and #476 wired these components to real API data. Tests now mock fetchEvents, fetchKnowledgeGraph, useWorkspaceId, MosaicSpinner, and elkjs following the same pattern established in tasks/page.test.tsx. Refs #469 Co-Authored-By: Claude Opus 4.6 --- .../(authenticated)/calendar/page.test.tsx | 111 +++++++++++++++++- .../knowledge/KnowledgeGraphViewer.test.tsx | 24 ++++ 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(authenticated)/calendar/page.test.tsx b/apps/web/src/app/(authenticated)/calendar/page.test.tsx index 098b21a..5794b65 100644 --- a/apps/web/src/app/(authenticated)/calendar/page.test.tsx +++ b/apps/web/src/app/(authenticated)/calendar/page.test.tsx @@ -1,5 +1,6 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; +import type { Event } from "@mosaic/shared"; import CalendarPage from "./page"; // Mock the Calendar component @@ -15,15 +16,94 @@ vi.mock("@/components/calendar/Calendar", () => ({ ), })); +// Mock MosaicSpinner +vi.mock("@/components/ui/MosaicSpinner", () => ({ + MosaicSpinner: ({ label }: { label?: string }): React.JSX.Element => ( +
{label ?? "Loading..."}
+ ), +})); + +// Mock useWorkspaceId +const mockUseWorkspaceId = vi.fn<() => string | null>(); +vi.mock("@/lib/hooks", () => ({ + useWorkspaceId: (): string | null => mockUseWorkspaceId(), +})); + +// Mock fetchEvents +const mockFetchEvents = vi.fn<() => Promise>(); +vi.mock("@/lib/api/events", () => ({ + fetchEvents: (...args: unknown[]): Promise => mockFetchEvents(...(args as [])), +})); + +const fakeEvents: Event[] = [ + { + id: "event-1", + title: "Team standup", + description: "Daily standup meeting", + startTime: new Date("2026-02-20T09:00:00Z"), + endTime: new Date("2026-02-20T09:30:00Z"), + allDay: false, + location: null, + recurrence: null, + creatorId: "user-1", + projectId: null, + workspaceId: "ws-1", + metadata: {}, + createdAt: new Date("2026-01-28"), + updatedAt: new Date("2026-01-28"), + }, + { + id: "event-2", + title: "Sprint planning", + description: "Bi-weekly sprint planning", + startTime: new Date("2026-02-21T14:00:00Z"), + endTime: new Date("2026-02-21T15:00:00Z"), + allDay: false, + location: null, + recurrence: null, + creatorId: "user-1", + projectId: null, + workspaceId: "ws-1", + metadata: {}, + createdAt: new Date("2026-01-28"), + updatedAt: new Date("2026-01-28"), + }, + { + id: "event-3", + title: "All-day workshop", + description: null, + startTime: new Date("2026-02-22T00:00:00Z"), + endTime: null, + allDay: true, + location: "Conference Room A", + recurrence: null, + creatorId: "user-1", + projectId: null, + workspaceId: "ws-1", + metadata: {}, + createdAt: new Date("2026-01-28"), + updatedAt: new Date("2026-01-28"), + }, +]; + describe("CalendarPage", (): void => { + beforeEach((): void => { + vi.clearAllMocks(); + mockUseWorkspaceId.mockReturnValue("ws-1"); + mockFetchEvents.mockResolvedValue(fakeEvents); + }); + it("should render the page title", (): void => { render(); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Calendar"); }); it("should show loading state initially", (): void => { + // Never resolve so we stay in loading state + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockFetchEvents.mockReturnValue(new Promise(() => {})); render(); - expect(screen.getByTestId("calendar")).toHaveTextContent("Loading"); + expect(screen.getByTestId("mosaic-spinner")).toBeInTheDocument(); }); it("should render the Calendar with events after loading", async (): Promise => { @@ -43,4 +123,31 @@ describe("CalendarPage", (): void => { render(); expect(screen.getByText("View your schedule at a glance")).toBeInTheDocument(); }); + + it("should show empty state when no events exist", async (): Promise => { + mockFetchEvents.mockResolvedValue([]); + render(); + await waitFor((): void => { + expect(screen.getByText("No events scheduled")).toBeInTheDocument(); + }); + }); + + it("should show error state on API failure", async (): Promise => { + mockFetchEvents.mockRejectedValue(new Error("Network error")); + render(); + await waitFor((): void => { + expect(screen.getByText("Network error")).toBeInTheDocument(); + }); + expect(screen.getByRole("button", { name: /try again/i })).toBeInTheDocument(); + }); + + it("should not fetch when workspace ID is not available", async (): Promise => { + mockUseWorkspaceId.mockReturnValue(null); + render(); + + // Wait a tick to ensure useEffect ran + await waitFor((): void => { + expect(mockFetchEvents).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/web/src/components/knowledge/KnowledgeGraphViewer.test.tsx b/apps/web/src/components/knowledge/KnowledgeGraphViewer.test.tsx index 2b61dd5..345c349 100644 --- a/apps/web/src/components/knowledge/KnowledgeGraphViewer.test.tsx +++ b/apps/web/src/components/knowledge/KnowledgeGraphViewer.test.tsx @@ -7,6 +7,30 @@ import * as knowledgeApi from "@/lib/api/knowledge"; // Mock the knowledge API vi.mock("@/lib/api/knowledge"); +// Mock MosaicSpinner to expose a test ID +vi.mock("@/components/ui/MosaicSpinner", () => ({ + MosaicSpinner: ({ label }: { label?: string }): React.JSX.Element => ( +
{label ?? "Loading..."}
+ ), +})); + +// Mock elkjs since it requires APIs not available in test environment +vi.mock("elkjs/lib/elk.bundled.js", () => ({ + default: class ELK { + layout(graph: { + children?: { id: string }[]; + }): Promise<{ children: { id: string; x: number; y: number }[] }> { + return Promise.resolve({ + children: (graph.children ?? []).map((child: { id: string }, i: number) => ({ + id: child.id, + x: i * 100, + y: i * 100, + })), + }); + } + }, +})); + // Mock Next.js router const mockPush = vi.fn(); vi.mock("next/navigation", () => ({