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);
});
});
});