import { renderHook } from '@testing-library/react'; import { useSleepSession, useSessions } from '../../../src/hooks/useSleepSession'; import * as api from '../../../src/api/client'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { vi, describe, it, expect, beforeEach } from 'vitest'; import type { SleepSessionRead, SessionListResponse } from '../../../src/types'; // Mock the API client vi.mock('../../../src/api/client', () => ({ fetchActiveSession: vi.fn(), createSession: vi.fn(), endSession: vi.fn(), fetchSessions: vi.fn(), deleteSession: vi.fn(), updateSession: vi.fn(), })); const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0, }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); vi.mock('../../../src/api/client', () => ({ fetchActiveSession: vi.fn(), createSession: vi.fn(), endSession: vi.fn(), fetchSessions: vi.fn(), deleteSession: vi.fn(), updateSession: vi.fn(), })); // Mock React Query as requested vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useQuery: vi.fn(), useMutation: vi.fn(), useQueryClient: vi.fn(), }; }); describe('useSleepSession and useSessions hooks', () => { const mockInvalidateQueries = vi.fn(); const mockQueryClient = { invalidateQueries: mockInvalidateQueries }; beforeEach(() => { vi.clearAllMocks(); (useQueryClient as any).mockReturnValue(mockQueryClient); }); describe('useSleepSession', () => { const mockSession: SleepSessionRead = { id: 1, start_time: '2026-06-10T10:00:00Z', end_time: null, notes: 'Testing session', created_at: '2026-06-10T10:00:00Z', updated_at: '2026-06-10T10:00:00Z', }; it('Test Case 6 (State): should return correct loading and session states', () => { (useQuery as any).mockReturnValue({ data: mockSession, isLoading: false, isFetching: false, }); (useMutation as any).mockImplementation(({ mutationFn }: { mutationFn: (args: any) => Promise }) => ({ mutateAsync: vi.fn().mockImplementation(mutationFn), isPending: false, })); const { result } = renderHook(() => useSleepSession()); expect(result.current.activeSession).toEqual(mockSession); expect(result.current.isLoading).toBe(false); }); it('Test Case 1: should successfully create a session and invalidate queries', async () => { (useQuery as any).mockReturnValue({ data: null, isLoading: false, isFetching: false }); (api.createSession as any).mockResolvedValue(mockSession); (useMutation as any).mockImplementation(({ mutationFn, onSuccess }: { mutationFn: (args: any) => Promise, onSuccess?: () => void }) => ({ mutateAsync: async (args: any) => { const res = await mutationFn(args); if (onSuccess) onSuccess(); return res; }, isPending: false, })); const { result } = renderHook(() => useSleepSession()); await result.current.createSession({ start_time: '2026-06-10T10:00:00Z' } as any); expect(api.createSession).toHaveBeenCalled(); expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['activeSession'] }); expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['sessions'] }); }); it('Test Case 2: should successfully end a session and invalidate queries', async () => { (useQuery as any).mockReturnValue({ data: mockSession, isLoading: false, isFetching: false }); (api.endSession as any).mockResolvedValue({ ...mockSession, end_time: '2026-06-10T12:00:00Z' }); (useMutation as any).mockImplementation(({ mutationFn, onSuccess }: { mutationFn: (args: any) => Promise, onSuccess?: () => void }) => ({ mutateAsync: async (args: any) => { const res = await mutationFn(args); if (onSuccess) onSuccess(); return res; }, isPending: false, })); const { result } = renderHook(() => useSleepSession()); await result.current.endSession({ sessionId: 1, endTime: '2026-06-10T12:00:00Z' } as any); expect(api.endSession).toHaveBeenCalled(); expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['activeSession'] }); expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['sessions'] }); }); it('Test Case 5 (Error Handling): should propagate errors during session creation', async () => { (useQuery as any).mockReturnValue({ data: null, isLoading: false, isFetching: false }); const error = new Error('API Error'); (api.createSession as any).mockRejectedValue(error); (useMutation as any).mockImplementation(({ mutationFn }: { mutationFn: (args: any) => Promise }) => ({ mutateAsync: async (args: any) => { return await mutationFn(args); }, isPending: false, })); const { result } = renderHook(() => useSleepSession()); await expect(result.current.createSession({ start_time: '...' } as any)).rejects.toThrow('API Error'); }); it('Test Case 6 (State): should show isCreating when mutation is pending', () => { (useQuery as any).mockReturnValue({ data: null, isLoading: false, isFetching: false }); (useMutation as any).mockReturnValueOnce({ mutateAsync: vi.fn(), isPending: true, }).mockReturnValueOnce({ mutateAsync: vi.fn(), isPending: false, }); const { result } = renderHook(() => useSleepSession()); expect(result.current.isCreating).toBe(true); }); it('Test Case 6 (State): should show isEnding when mutation is pending', () => { (useQuery as any).mockReturnValue({ data: mockSession, isLoading: false, isFetching: false }); (useMutation as any).mockReturnValueOnce({ mutateAsync: vi.fn(), isPending: false, }).mockReturnValueOnce({ mutateAsync: vi.fn(), isPending: true, }); const { result } = renderHook(() => useSleepSession()); expect(result.current.isEnding).toBe(true); }); }); describe('useSessions', () => { const mockSessionsResponse: SessionListResponse = { sessions: [ { id: 1, start_time: '2026-06-10T10:00:00Z', end_time: null, notes: null, created_at: '...', updated_at: '...' }, { id: 2, start_time: '2026-06-10T10:00:00Z', end_time: null, notes: null, created_at: '...', updated_at: '...' }, ], total: 2, }; it('Test Case 6 (State): should return correct loading and sessions states', () => { (useQuery as any).mockReturnValue({ data: mockSessionsResponse, isLoading: false, }); (useMutation as any).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); const { result } = renderHook(() => useSessions()); expect(result.current.sessions).toEqual(mockSessionsResponse.sessions); expect(result.current.total).toBe(2); expect(result.current.isLoading).toBe(false); }); it('Test Case 3: should successfully delete a session and invalidate queries', async () => { (useQuery as any).mockReturnValue({ data: mockSessionsResponse, isLoading: false }); (api.deleteSession as any).mockResolvedValue(undefined); (useMutation as any).mockImplementation(({ mutationFn, onSuccess }: { mutationFn: (args: any) => Promise, onSuccess?: () => void }) => ({ mutateAsync: async (args: any) => { const res = await mutationFn(args); if (onSuccess) onSuccess(); return res; }, isPending: false, })); const { result } = renderHook(() => useSessions()); await (result.current.deleteSession as any)(1); expect(api.deleteSession).toHaveBeenCalledWith(1); expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['sessions'] }); }); it('Test Case 4: should successfully update a session and invalidate queries', async () => { (useQuery as any).mockReturnValue({ data: mockSessionsResponse, isLoading: false }); const updatePayload = { end_time: '2026-06-10T12:00:00Z' }; (api.updateSession as any).mockResolvedValue({ id: 1, ...updatePayload } as any); (useMutation as any).mockImplementation(({ mutationFn, onSuccess }: { mutationFn: (args: any) => Promise, onSuccess?: () => void }) => ({ mutateAsync: async (args: any) => { const res = await mutationFn(args); if (onSuccess) onSuccess(); return res; }, isPending: false, })); const { result } = renderHook(() => useSessions()); await (result.current.updateSession as any)({ sessionId: 1, payload: updatePayload }); expect(api.updateSession).toHaveBeenCalledWith(1, updatePayload); expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['sessions'] }); }); it('Test Case 5 (Error Handling): should propagate errors during session deletion', async () => { (useQuery as any).mockReturnValue({ data: mockSessionsResponse, isLoading: false }); const error = new Error('Delete failed'); (api.deleteSession as any).mockRejectedValue(error); (useMutation as any).mockImplementation(({ mutationFn }: { mutationFn: (args: any) => Promise }) => ({ mutateAsync: async (args: any) => { return await mutationFn(args); }, isPending: false, })); const { result } = renderHook(() => useSessions()); await expect((result.current.deleteSession as any)(1)).rejects.toThrow('Delete failed'); }); it('Test Case 6 (State): should show isDeleting when mutation is pending', () => { (useQuery as any).mockReturnValue({ data: mockSessionsResponse, isLoading: false }); (useMutation as any).mockReturnValue({ mutateAsync: vi.fn(), isPending: true, }); const { result } = renderHook(() => useSessions()); expect(result.current.isDeleting).toBe(true); }); it('Test Case 7 (Data Undefined): should return empty sessions and 0 total when data is undefined', () => { (useQuery as any).mockReturnValue({ data: undefined, isLoading: false, }); (useMutation as any).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); const { result } = renderHook(() => useSessions()); expect(result.current.sessions).toEqual([]); expect(result.current.total).toBe(0); }); it('Test Case 8 (Error Handling): should handle error from fetchSessions gracefully', () => { (useQuery as any).mockReturnValue({ data: undefined, isLoading: false, isError: true, error: new Error('Fetch failed'), }); (useMutation as any).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, }); const { result } = renderHook(() => useSessions()); expect(result.current.sessions).toEqual([]); expect(result.current.total).toBe(0); }); }); });