import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act, waitFor } from '@testing-library/react'; import { useAdminData } from './useAdminData'; import * as adminApi from '@/api/admin'; import { UserRole, Theme, type SystemStats, type User, type WorkspaceStats, } from '@/types/models'; // Mock dependencies vi.mock('@/api/admin'); vi.mock('@mantine/notifications', () => ({ notifications: { show: vi.fn(), }, })); // Import notifications for assertions import { notifications } from '@mantine/notifications'; // Mock data const mockSystemStats: SystemStats = { totalUsers: 10, activeUsers: 8, totalWorkspaces: 15, totalFiles: 150, totalSize: 1024000, }; const mockUsers: User[] = [ { id: 1, email: 'admin@example.com', displayName: 'Admin User', role: UserRole.Admin, theme: Theme.Dark, createdAt: '2024-01-01T00:00:00Z', lastWorkspaceId: 1, }, { id: 2, email: 'editor@example.com', displayName: 'Editor User', role: UserRole.Editor, theme: Theme.Dark, createdAt: '2024-01-02T00:00:00Z', lastWorkspaceId: 2, }, ]; const mockWorkspaceStats: WorkspaceStats[] = [ { userID: 1, userEmail: 'admin@example.com', workspaceID: 1, workspaceName: 'admin-workspace', workspaceCreatedAt: '2024-01-01T00:00:00Z', fileCountStats: { totalFiles: 10, totalSize: 204800, }, }, { userID: 2, userEmail: 'editor@example.com', workspaceID: 2, workspaceName: 'editor-workspace', workspaceCreatedAt: '2024-01-02T00:00:00Z', fileCountStats: { totalFiles: 15, totalSize: 307200, }, }, ]; describe('useAdminData', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('stats data type', () => { it('initializes with empty stats and loading state', async () => { const { result } = renderHook(() => useAdminData('stats')); expect(result.current.data).toEqual({}); expect(result.current.loading).toBe(true); expect(result.current.error).toBeNull(); expect(typeof result.current.reload).toBe('function'); // Wait for the hook to complete its async initialization await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('loads system stats successfully', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); mockGetSystemStats.mockResolvedValue(mockSystemStats); const { result } = renderHook(() => useAdminData('stats')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockSystemStats); expect(result.current.error).toBeNull(); expect(mockGetSystemStats).toHaveBeenCalledTimes(1); }); it('handles stats loading errors', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); mockGetSystemStats.mockRejectedValue(new Error('Failed to load stats')); const { result } = renderHook(() => useAdminData('stats')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual({}); expect(result.current.error).toBe('Failed to load stats'); expect(notifications.show).toHaveBeenCalledWith({ title: 'Error', message: 'Failed to load stats: Failed to load stats', color: 'red', }); }); it('reloads stats data', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); mockGetSystemStats.mockResolvedValue(mockSystemStats); const { result } = renderHook(() => useAdminData('stats')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(mockGetSystemStats).toHaveBeenCalledTimes(1); await act(async () => { await result.current.reload(); }); expect(mockGetSystemStats).toHaveBeenCalledTimes(2); expect(result.current.data).toEqual(mockSystemStats); }); }); describe('users data type', () => { it('initializes with empty users array and loading state', async () => { const { result } = renderHook(() => useAdminData('users')); expect(result.current.data).toEqual([]); expect(result.current.loading).toBe(true); expect(result.current.error).toBeNull(); expect(typeof result.current.reload).toBe('function'); // Wait for the hook to complete its async initialization await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('loads users successfully', async () => { const mockGetUsers = vi.mocked(adminApi.getUsers); mockGetUsers.mockResolvedValue(mockUsers); const { result } = renderHook(() => useAdminData('users')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockUsers); expect(result.current.error).toBeNull(); expect(mockGetUsers).toHaveBeenCalledTimes(1); }); it('handles users loading errors', async () => { const mockGetUsers = vi.mocked(adminApi.getUsers); mockGetUsers.mockRejectedValue(new Error('Failed to load users')); const { result } = renderHook(() => useAdminData('users')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual([]); expect(result.current.error).toBe('Failed to load users'); expect(notifications.show).toHaveBeenCalledWith({ title: 'Error', message: 'Failed to load users: Failed to load users', color: 'red', }); }); it('reloads users data', async () => { const mockGetUsers = vi.mocked(adminApi.getUsers); mockGetUsers.mockResolvedValue(mockUsers); const { result } = renderHook(() => useAdminData('users')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(mockGetUsers).toHaveBeenCalledTimes(1); await act(async () => { await result.current.reload(); }); expect(mockGetUsers).toHaveBeenCalledTimes(2); expect(result.current.data).toEqual(mockUsers); }); it('handles empty users array', async () => { const mockGetUsers = vi.mocked(adminApi.getUsers); mockGetUsers.mockResolvedValue([]); const { result } = renderHook(() => useAdminData('users')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual([]); expect(result.current.error).toBeNull(); }); }); describe('workspaces data type', () => { it('initializes with empty workspaces array and loading state', async () => { const { result } = renderHook(() => useAdminData('workspaces')); expect(result.current.data).toEqual([]); expect(result.current.loading).toBe(true); expect(result.current.error).toBeNull(); expect(typeof result.current.reload).toBe('function'); // Wait for the hook to complete its async initialization await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('loads workspaces successfully', async () => { const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces); mockGetWorkspaces.mockResolvedValue(mockWorkspaceStats); const { result } = renderHook(() => useAdminData('workspaces')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockWorkspaceStats); expect(result.current.error).toBeNull(); expect(mockGetWorkspaces).toHaveBeenCalledTimes(1); }); it('handles workspaces loading errors', async () => { const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces); mockGetWorkspaces.mockRejectedValue( new Error('Failed to load workspaces') ); const { result } = renderHook(() => useAdminData('workspaces')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual([]); expect(result.current.error).toBe('Failed to load workspaces'); expect(notifications.show).toHaveBeenCalledWith({ title: 'Error', message: 'Failed to load workspaces: Failed to load workspaces', color: 'red', }); }); it('reloads workspaces data', async () => { const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces); mockGetWorkspaces.mockResolvedValue(mockWorkspaceStats); const { result } = renderHook(() => useAdminData('workspaces')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(mockGetWorkspaces).toHaveBeenCalledTimes(1); await act(async () => { await result.current.reload(); }); expect(mockGetWorkspaces).toHaveBeenCalledTimes(2); expect(result.current.data).toEqual(mockWorkspaceStats); }); it('handles workspaces with minimal configuration', async () => { const minimalWorkspaceStats: WorkspaceStats[] = [ { userID: 3, userEmail: 'minimal@example.com', workspaceID: 3, workspaceName: 'minimal-workspace', workspaceCreatedAt: '2024-01-03T00:00:00Z', }, ]; const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces); mockGetWorkspaces.mockResolvedValue(minimalWorkspaceStats); const { result } = renderHook(() => useAdminData('workspaces')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(minimalWorkspaceStats); expect(result.current.error).toBeNull(); }); }); describe('error handling', () => { it('handles API errors with error response object', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); // Create a properly typed error object to simulate API error response const errorWithResponse = new Error('Request failed'); type ErrorWithResponse = Error & { response: { data: { error: string; }; }; }; (errorWithResponse as ErrorWithResponse).response = { data: { error: 'Custom API error message', }, }; mockGetSystemStats.mockRejectedValue(errorWithResponse); const { result } = renderHook(() => useAdminData('stats')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.error).toBe('Custom API error message'); expect(notifications.show).toHaveBeenCalledWith({ title: 'Error', message: 'Failed to load stats: Custom API error message', color: 'red', }); }); it('clears error on successful reload', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); mockGetSystemStats .mockRejectedValueOnce(new Error('Initial error')) .mockResolvedValueOnce(mockSystemStats); const { result } = renderHook(() => useAdminData('stats')); // Wait for initial error await waitFor(() => { expect(result.current.error).toBe('Initial error'); }); // Reload successfully await act(async () => { await result.current.reload(); }); await waitFor(() => { expect(result.current.error).toBeNull(); expect(result.current.data).toEqual(mockSystemStats); }); }); }); describe('loading state management', () => { it('manages loading state correctly through full lifecycle', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); let resolvePromise: (value: SystemStats) => void; const pendingPromise = new Promise((resolve) => { resolvePromise = resolve; }); mockGetSystemStats.mockReturnValue(pendingPromise); const { result } = renderHook(() => useAdminData('stats')); // Initial load should be loading expect(result.current.loading).toBe(true); // Resolve initial load await act(async () => { resolvePromise!(mockSystemStats); await pendingPromise; }); expect(result.current.loading).toBe(false); // Test reload loading state let resolveReload: (value: SystemStats) => void; const reloadPromise = new Promise((resolve) => { resolveReload = resolve; }); mockGetSystemStats.mockReturnValueOnce(reloadPromise); act(() => { void result.current.reload(); }); expect(result.current.loading).toBe(true); await act(async () => { resolveReload!(mockSystemStats); await reloadPromise; }); expect(result.current.loading).toBe(false); }); }); describe('data consistency', () => { it('handles data type parameter changes', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); const mockGetUsers = vi.mocked(adminApi.getUsers); mockGetSystemStats.mockResolvedValue(mockSystemStats); mockGetUsers.mockResolvedValue(mockUsers); const { result, rerender } = renderHook( ({ type }) => useAdminData(type), { initialProps: { type: 'stats' as const } as { type: 'stats' | 'users' | 'workspaces'; }, } ); // Wait for stats to load await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockSystemStats); // Change to users type rerender({ type: 'users' as const }); // Should reset to loading and empty array for users expect(result.current.loading).toBe(true); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockUsers); expect(mockGetUsers).toHaveBeenCalledTimes(1); }); it('handles data type changes correctly with different initial values', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); const mockGetUsers = vi.mocked(adminApi.getUsers); const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces); mockGetSystemStats.mockResolvedValue(mockSystemStats); mockGetUsers.mockResolvedValue(mockUsers); mockGetWorkspaces.mockResolvedValue(mockWorkspaceStats); const { result, rerender } = renderHook( ({ type }) => useAdminData(type), { initialProps: { type: 'stats' as const } as { type: 'stats' | 'users' | 'workspaces'; }, } ); // Wait for stats to load await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockSystemStats); // Change to users type - should reset to empty array and reload act(() => { rerender({ type: 'users' as const }); }); // Data should reset to empty array immediately when type changes expect(result.current.data).toEqual([]); expect(result.current.loading).toBe(true); expect(result.current.error).toBeNull(); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockUsers); // Change to workspaces type - should reset to empty array and reload act(() => { rerender({ type: 'workspaces' as const }); }); // Data should reset to empty array immediately when type changes expect(result.current.data).toEqual([]); expect(result.current.loading).toBe(true); expect(result.current.error).toBeNull(); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toEqual(mockWorkspaceStats); // Verify correct API calls were made expect(mockGetSystemStats).toHaveBeenCalledTimes(1); expect(mockGetUsers).toHaveBeenCalledTimes(1); expect(mockGetWorkspaces).toHaveBeenCalledTimes(1); }); }); describe('function stability', () => { it('maintains stable reload function reference', async () => { const { result, rerender } = renderHook(() => useAdminData('stats')); const initialReload = result.current.reload; rerender(); expect(result.current.reload).toBe(initialReload); // Wait for the hook to complete its async initialization await waitFor(() => { expect(result.current.loading).toBe(false); }); }); }); describe('concurrent operations', () => { it('handles multiple concurrent reloads', async () => { const mockGetSystemStats = vi.mocked(adminApi.getSystemStats); mockGetSystemStats.mockResolvedValue(mockSystemStats); const { result } = renderHook(() => useAdminData('stats')); await waitFor(() => { expect(result.current.loading).toBe(false); }); // Trigger multiple reloads await act(async () => { await Promise.all([ result.current.reload(), result.current.reload(), result.current.reload(), ]); }); expect(mockGetSystemStats).toHaveBeenCalledTimes(4); // 1 initial + 3 reloads expect(result.current.data).toEqual(mockSystemStats); expect(result.current.loading).toBe(false); }); }); });