313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
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 }) => (
|
|
<QueryClientProvider client={createTestQueryClient()}>
|
|
{children}
|
|
</QueryClientProvider>
|
|
);
|
|
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<typeof import('@tanstack/react-query')>();
|
|
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<any> }) => ({
|
|
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<any>, 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<any>, 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<any> }) => ({
|
|
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<any>, 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<any>, 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<any> }) => ({
|
|
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);
|
|
});
|
|
});
|
|
});
|