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", () => ({