// ======================================== // Chart Hooks Integration Tests // ======================================== // Integration tests for TanStack Query hooks: useWorkflowStatusCounts, useActivityTimeline, useTaskTypeCounts with workspace scoping import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import * as React from 'react'; import { useWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts'; import { useActivityTimeline } from '@/hooks/useActivityTimeline'; import { useTaskTypeCounts } from '@/hooks/useTaskTypeCounts'; // Mock API const mockApi = { get: vi.fn(), }; vi.mock('@/lib/api', () => ({ api: { get: (...args: any[]) => mockApi.get(...args), }, })); describe('Chart Hooks Integration Tests', () => { let queryClient: QueryClient; const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); beforeEach(() => { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0, }, }, }); mockApi.get.mockReset(); }); afterEach(() => { queryClient.clear(); }); describe('useWorkflowStatusCounts', () => { it('CHI-1.1 - should fetch workflow status counts successfully', async () => { const mockData = [ { status: 'completed', count: 30, percentage: 60 }, { status: 'in_progress', count: 10, percentage: 20 }, { status: 'pending', count: 10, percentage: 20 }, ]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(result.current.data).toEqual(mockData); expect(mockApi.get).toHaveBeenCalledWith('/api/session-status-counts'); }); it('CHI-1.2 - should apply workspace scoping to query', async () => { const mockData = [{ status: 'completed', count: 5, percentage: 100 }]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook( () => useWorkflowStatusCounts({ projectPath: '/test/workspace' } as any), { wrapper } ); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(mockApi.get).toHaveBeenCalledWith('/api/session-status-counts', { params: { workspace: '/test/workspace' }, }); }); it('CHI-1.3 - should handle API errors gracefully', async () => { mockApi.get.mockRejectedValue(new Error('API Error')); const { result } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); await waitFor(() => { expect((result.current as any).isError).toBe(true); }); expect(result.current.error).toBeDefined(); expect(result.current.data).toBeUndefined(); }); it('CHI-1.4 - should cache results with TanStack Query', async () => { const mockData = [{ status: 'completed', count: 10, percentage: 100 }]; mockApi.get.mockResolvedValue({ data: mockData }); const { result: result1 } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); await waitFor(() => expect((result1.current as any).isSuccess).toBe(true)); // Second render should use cache const { result: result2 } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); await waitFor(() => { expect((result2.current as any).isSuccess).toBe(true); }); // API should only be called once (cached) expect(mockApi.get).toHaveBeenCalledTimes(1); expect(result2.current.data).toEqual(mockData); }); it('CHI-1.5 - should support manual refetch', async () => { const mockData = [{ status: 'completed', count: 10, percentage: 100 }]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); await waitFor(() => expect((result.current as any).isSuccess).toBe(true)); // Refetch await result.current.refetch(); expect(mockApi.get).toHaveBeenCalledTimes(2); }); }); describe('useActivityTimeline', () => { it('CHI-2.1 - should fetch activity timeline with default date range', async () => { const mockData = [ { date: '2026-02-01', sessions: 5, tasks: 20 }, { date: '2026-02-02', sessions: 8, tasks: 35 }, ]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook(() => useActivityTimeline(), { wrapper }); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(result.current.data).toEqual(mockData); expect(mockApi.get).toHaveBeenCalledWith('/api/activity-timeline'); }); it('CHI-2.2 - should accept custom date range parameters', async () => { const mockData = [{ date: '2026-01-01', sessions: 3, tasks: 10 }]; mockApi.get.mockResolvedValue({ data: mockData }); const dateRange = { start: new Date('2026-01-01'), end: new Date('2026-01-31'), }; const { result } = renderHook(() => (useActivityTimeline as any)(dateRange), { wrapper }); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(mockApi.get).toHaveBeenCalledWith('/api/activity-timeline', { params: { startDate: dateRange.start.toISOString(), endDate: dateRange.end.toISOString(), }, }); }); it('CHI-2.3 - should handle empty timeline data', async () => { mockApi.get.mockResolvedValue({ data: [] }); const { result } = renderHook(() => useActivityTimeline(), { wrapper }); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(result.current.data).toEqual([]); }); it('CHI-2.4 - should apply workspace scoping', async () => { const mockData = [{ date: '2026-02-01', sessions: 2, tasks: 8 }]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook( () => (useActivityTimeline as any)(undefined, '/test/workspace'), { wrapper } ); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(mockApi.get).toHaveBeenCalledWith('/api/activity-timeline', { params: { workspace: '/test/workspace' }, }); }); it('CHI-2.5 - should invalidate cache on workspace change', async () => { const mockData1 = [{ date: '2026-02-01', sessions: 5, tasks: 20 }]; const mockData2 = [{ date: '2026-02-01', sessions: 3, tasks: 10 }]; mockApi.get.mockResolvedValueOnce({ data: mockData1 }); const { result, rerender } = renderHook( ({ workspace }: { workspace?: string }) => (useActivityTimeline as any)(undefined, workspace), { wrapper, initialProps: { workspace: '/workspace1' } } ); await waitFor(() => expect((result.current as any).isSuccess).toBe(true)); expect(result.current.data).toEqual(mockData1); // Change workspace mockApi.get.mockResolvedValueOnce({ data: mockData2 }); rerender({ workspace: '/workspace2' }); await waitFor(() => { expect(result.current.data).toEqual(mockData2); }); expect(mockApi.get).toHaveBeenCalledTimes(2); }); }); describe('useTaskTypeCounts', () => { it('CHI-3.1 - should fetch task type counts successfully', async () => { const mockData = [ { type: 'feature', count: 45 }, { type: 'bugfix', count: 30 }, { type: 'refactor', count: 15 }, ]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook(() => useTaskTypeCounts(), { wrapper }); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(result.current.data).toEqual(mockData); expect(mockApi.get).toHaveBeenCalledWith('/api/task-type-counts'); }); it('CHI-3.2 - should apply workspace scoping', async () => { const mockData = [{ type: 'feature', count: 10 }]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook( () => useTaskTypeCounts({ projectPath: '/test/workspace' } as any), { wrapper } ); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(mockApi.get).toHaveBeenCalledWith('/api/task-type-counts', { params: { workspace: '/test/workspace' }, }); }); it('CHI-3.3 - should handle zero counts', async () => { const mockData = [ { type: 'feature', count: 0 }, { type: 'bugfix', count: 0 }, ]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook(() => useTaskTypeCounts(), { wrapper }); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); expect(result.current.data).toEqual(mockData); }); it('CHI-3.4 - should support staleTime configuration', async () => { const mockData = [{ type: 'feature', count: 5 }]; mockApi.get.mockResolvedValue({ data: mockData }); const { result } = renderHook( () => useTaskTypeCounts({ staleTime: 30000 }), { wrapper } ); await waitFor(() => { expect((result.current as any).isSuccess).toBe(true); }); // Data should be fresh for 30s expect(result.current.isStale).toBe(false); }); }); describe('Multi-Hook Integration', () => { it('CHI-4.1 - should load all chart hooks concurrently', async () => { mockApi.get.mockImplementation((url: string) => { const data: Record = { '/api/session-status-counts': [{ status: 'completed', count: 10, percentage: 100 }], '/api/activity-timeline': [{ date: '2026-02-01', sessions: 5, tasks: 20 }], '/api/task-type-counts': [{ type: 'feature', count: 15 }], }; return Promise.resolve({ data: data[url] }); }); const { result: result1 } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); const { result: result2 } = renderHook(() => useActivityTimeline(), { wrapper }); const { result: result3 } = renderHook(() => useTaskTypeCounts(), { wrapper }); await waitFor(() => { expect((result1.current as any).isSuccess).toBe(true); expect((result2.current as any).isSuccess).toBe(true); expect((result3.current as any).isSuccess).toBe(true); }); expect(mockApi.get).toHaveBeenCalledTimes(3); }); it('CHI-4.2 - should handle partial failures gracefully', async () => { mockApi.get.mockImplementation((url: string) => { if (url === '/api/session-status-counts') { return Promise.reject(new Error('Failed')); } return Promise.resolve({ data: url === '/api/activity-timeline' ? [{ date: '2026-02-01', sessions: 5, tasks: 20 }] : [{ type: 'feature', count: 15 }], }); }); const { result: result1 } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); const { result: result2 } = renderHook(() => useActivityTimeline(), { wrapper }); const { result: result3 } = renderHook(() => useTaskTypeCounts(), { wrapper }); await waitFor(() => { expect((result1.current as any).isError).toBe(true); expect((result2.current as any).isSuccess).toBe(true); expect((result3.current as any).isSuccess).toBe(true); }); }); it('CHI-4.3 - should share cache across multiple components', async () => { const mockData = [{ status: 'completed', count: 10, percentage: 100 }]; mockApi.get.mockResolvedValue({ data: mockData }); // First component const { result: result1 } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); await waitFor(() => expect((result1.current as any).isSuccess).toBe(true)); // Second component should use cache const { result: result2 } = renderHook(() => useWorkflowStatusCounts(), { wrapper }); await waitFor(() => { expect((result2.current as any).isSuccess).toBe(true); }); // Only one API call expect(mockApi.get).toHaveBeenCalledTimes(1); expect(result1.current.data).toEqual(result2.current.data); }); }); });