- Add project API routes for multi-project backend integration - Create useProjects hook for listing and managing registered projects - Create useCurrentProject hook for current project selection and switching - Create useProjectHealth hook for real-time project health metrics - Create useActivityLog hook with ActivityLogModal integration - Add comprehensive test coverage for all hooks (useProjects, useCurrentProject, useActivityLog) - Add test setup utilities for React Query mocking
230 lines
6.5 KiB
TypeScript
230 lines
6.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
import { useActivityLog } from "./useActivityLog";
|
|
import type { ActivityFeedEntry } from "../api";
|
|
|
|
function mockFetchResponse(
|
|
ok: boolean,
|
|
body: unknown,
|
|
status = ok ? 200 : 500,
|
|
contentType = "application/json"
|
|
) {
|
|
const bodyText = JSON.stringify(body);
|
|
return Promise.resolve({
|
|
ok,
|
|
status,
|
|
statusText: ok ? "OK" : "Error",
|
|
headers: {
|
|
get: (name: string) =>
|
|
name.toLowerCase() === "content-type" ? contentType : null,
|
|
},
|
|
json: () => Promise.resolve(body),
|
|
text: () => Promise.resolve(bodyText),
|
|
} as unknown as Response);
|
|
}
|
|
|
|
describe("useActivityLog", () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("initializes with empty entries and loads on mount", async () => {
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
|
|
|
|
const { result } = renderHook(() => useActivityLog());
|
|
|
|
expect(result.current.loading).toBe(true);
|
|
expect(result.current.entries).toEqual([]);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.loading).toBe(false);
|
|
});
|
|
|
|
expect(result.current.entries).toEqual([]);
|
|
});
|
|
|
|
it("fetches and displays activity entries", async () => {
|
|
const mockEntries: ActivityFeedEntry[] = [
|
|
{
|
|
id: "entry_1",
|
|
timestamp: "2026-01-01T00:00:00.000Z",
|
|
type: "task:created",
|
|
projectId: "proj_123",
|
|
projectName: "Test Project",
|
|
taskId: "FN-001",
|
|
details: "Task created",
|
|
},
|
|
];
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, mockEntries));
|
|
|
|
const { result } = renderHook(() => useActivityLog());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.entries).toHaveLength(1);
|
|
});
|
|
|
|
expect(result.current.entries[0].type).toBe("task:created");
|
|
expect(result.current.entries[0].projectName).toBe("Test Project");
|
|
});
|
|
|
|
it("filters by projectId", async () => {
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
|
|
|
|
renderHook(() => useActivityLog({ projectId: "proj_123" }));
|
|
|
|
await waitFor(() => {
|
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("projectId=proj_123"),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
});
|
|
|
|
it("filters by type", async () => {
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
|
|
|
|
renderHook(() => useActivityLog({ type: "task:created" }));
|
|
|
|
await waitFor(() => {
|
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("type=task%3Acreated"),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
});
|
|
|
|
it("respects custom limit", async () => {
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
|
|
|
|
renderHook(() => useActivityLog({ limit: 100 }));
|
|
|
|
await waitFor(() => {
|
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("limit=100"),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
});
|
|
|
|
it("does not auto-refresh when disabled", async () => {
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
|
|
|
|
renderHook(() => useActivityLog({ autoRefresh: false }));
|
|
|
|
await waitFor(() => {
|
|
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
// Fast forward time (but not using fake timers for this test)
|
|
vi.useRealTimers();
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
|
|
// Should still be 1
|
|
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("refresh function manually refreshes data", async () => {
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
|
|
|
|
const { result } = renderHook(() => useActivityLog({ autoRefresh: false }));
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.loading).toBe(false);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.refresh();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
it("clear removes all entries", async () => {
|
|
const mockEntries: ActivityFeedEntry[] = [
|
|
{
|
|
id: "entry_1",
|
|
timestamp: "2026-01-01T00:00:00.000Z",
|
|
type: "task:created",
|
|
projectId: "proj_123",
|
|
projectName: "Test Project",
|
|
details: "Task created",
|
|
},
|
|
];
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, mockEntries));
|
|
|
|
const { result } = renderHook(() => useActivityLog());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.entries).toHaveLength(1);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.clear();
|
|
});
|
|
|
|
expect(result.current.entries).toEqual([]);
|
|
expect(result.current.hasMore).toBe(false);
|
|
});
|
|
|
|
it("handles errors gracefully", async () => {
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(false, { error: "Server error" }, 500));
|
|
|
|
const { result } = renderHook(() => useActivityLog());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.loading).toBe(false);
|
|
});
|
|
|
|
expect(result.current.error).not.toBeNull();
|
|
});
|
|
|
|
it("sets hasMore when entries equal limit", async () => {
|
|
const mockEntries: ActivityFeedEntry[] = Array.from({ length: 50 }, (_, i) => ({
|
|
id: `entry_${i}`,
|
|
timestamp: "2026-01-01T00:00:00.000Z",
|
|
type: "task:created" as const,
|
|
projectId: "proj_123",
|
|
projectName: "Test Project",
|
|
details: "Task created",
|
|
}));
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, mockEntries));
|
|
|
|
const { result } = renderHook(() => useActivityLog({ limit: 50 }));
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.entries).toHaveLength(50);
|
|
});
|
|
|
|
expect(result.current.hasMore).toBe(true);
|
|
});
|
|
|
|
it("sets hasMore to false when fewer entries than limit", async () => {
|
|
const mockEntries: ActivityFeedEntry[] = Array.from({ length: 30 }, (_, i) => ({
|
|
id: `entry_${i}`,
|
|
timestamp: "2026-01-01T00:00:00.000Z",
|
|
type: "task:created" as const,
|
|
projectId: "proj_123",
|
|
projectName: "Test Project",
|
|
details: "Task created",
|
|
}));
|
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, mockEntries));
|
|
|
|
const { result } = renderHook(() => useActivityLog({ limit: 50 }));
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.entries).toHaveLength(30);
|
|
});
|
|
|
|
expect(result.current.hasMore).toBe(false);
|
|
});
|
|
});
|