A try of overhauling the frontend tests, bailed after 24h+

This commit is contained in:
2026-06-11 09:09:02 +02:00
parent a9e1e96622
commit bb58389675
33 changed files with 3257 additions and 275 deletions
@@ -0,0 +1,123 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useSettings } from '../../../src/hooks/useSettings';
import { SettingsProvider } from '../../../src/hooks/SettingsProvider';
describe('useSettings hook', () => {
// Mock implementation of localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: vi.fn((key: string) => store[key] || null),
setItem: vi.fn((key: string, value: string) => {
store[key] = value.toString();
}),
removeItem: vi.fn((key: string) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
};
})();
beforeEach(() => {
vi.stubGlobal('localStorage', localStorageMock);
vi.clearAllMocks();
// Clear the mock store
(localStorage as any).clear();
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<SettingsProvider>{children}</SettingsProvider>
);
it('should return default settings when wrapped in SettingsProvider', () => {
const { result } = renderHook(() => useSettings(), { wrapper });
expect(result.current.settings).toEqual({
dayStart: '07:00',
dayEnd: '20:00',
wakeupRef: '04:00',
bedtimeRef: '19:00',
statsInterval: 7,
sessionsPerPage: 10,
});
});
it('should throw an error when used outside of SettingsProvider', () => {
expect(() => renderHook(() => useSettings())).toThrow(
'useSettings must be used within a SettingsProvider'
);
});
it('should update settings correctly via the update function', () => {
const { result } = renderHook(() => useSettings(), { wrapper });
act(() => {
result.current.update('dayStart', '08:30');
});
expect(result.current.settings.dayStart).toBe('08:30');
// Verify other settings remain unchanged
expect(result.current.settings.dayEnd).toBe('20:00');
});
it('should persist updated settings to localStorage', () => {
const setItemSpy = vi.spyOn(localStorage, 'setItem');
const { result } = renderHook(() => useSettings(), { wrapper });
act(() => {
result.current.update('statsInterval', 14);
});
expect(setItemSpy).toHaveBeenCalledWith(
'baby-sleep-settings',
expect.stringContaining('"statsInterval":14')
);
});
it('should initialize with settings from localStorage if available', () => {
const storedSettings = {
dayStart: '10:00',
dayEnd: '22:00',
wakeupRef: '06:00',
bedtimeRef: '21:00',
statsInterval: 5,
sessionsPerPage: 20,
};
localStorage.setItem('baby-sleep-settings', JSON.stringify(storedSettings));
const { result } = renderHook(() => useSettings(), { wrapper });
expect(result.current.settings).toEqual(storedSettings);
});
it('should fallback to default settings if localStorage contains invalid JSON', () => {
localStorage.setItem('baby-sleep-settings', 'invalid-json');
const { result } = renderHook(() => useSettings(), { wrapper });
expect(result.current.settings).toEqual({
dayStart: '07:00',
dayEnd: '20:00',
wakeupRef: '04:00',
bedtimeRef: '19:00',
statsInterval: 7,
sessionsPerPage: 10,
});
});
it('should handle empty localStorage by using default settings', () => {
localStorage.clear();
const { result } = renderHook(() => useSettings(), { wrapper });
expect(result.current.settings).toEqual({
dayStart: '07:00',
dayEnd: '20:00',
wakeupRef: '04:00',
bedtimeRef: '19:00',
statsInterval: 7,
sessionsPerPage: 10,
});
});
});
@@ -0,0 +1,312 @@
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);
});
});
});
+162
View File
@@ -0,0 +1,162 @@
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useStats, useSummary } from '../../../src/hooks/useStats';
import * as api from '../../../src/api/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Mock the api client
vi.mock('../../../src/api/client', () => ({
fetchDailySummary: vi.fn(),
fetchTimetable: vi.fn(),
fetchWakeSleepTimes: vi.fn(),
fetchSummary: vi.fn(),
}));
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
describe('useStats', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={createTestQueryClient()}>
{children}
</QueryClientProvider>
);
beforeEach(() => {
vi.clearAllMocks();
});
it('should return data and correctly transform daily stats on success', async () => {
const mockDailySummary = {
days: [
{ date: '2024-01-01', total_minutes: 480, day_minutes: 240, night_minutes: 240 },
{ date: '2024-01-02', total_minutes: 540, day_minutes: 300, night_minutes: 240 },
],
};
const mockTimetable = { slots: [] };
const mockWakeupBedtime = { days: [] };
vi.mocked(api.fetchDailySummary).mockResolvedValueOnce(mockDailySummary);
vi.mocked(api.fetchTimetable).mockResolvedValueOnce(mockTimetable);
vi.mocked(api.fetchWakeSleepTimes).mockResolvedValueOnce(mockWakeupBedtime);
const { result } = renderHook(() => useStats('2024-01-01', '2024-01-02'), { wrapper });
// Initially loading
expect(result.current.isLoading).toBe(true);
await waitFor(() => expect(result.current.isLoading).toBe(false));
// Verify raw data
expect(result.current.dailySummary).toEqual(mockDailySummary);
// Verify transformation (total, day/night split in hours)
expect(result.current.transformedDailySummary).toEqual([
{ date: '2024-01-01', daySleep: 4, nightSleep: 4, total: 8 },
{ date: '2024-01-02', daySleep: 5, nightSleep: 4, total: 9 },
]);
expect(result.current.timetable).toEqual(mockTimetable);
expect(result.current.wakeupBedtime).toEqual(mockWakeupBedtime);
expect(result.current.error).toBeNull();
});
it('should handle empty data from the API', async () => {
const mockEmptyDailySummary = { days: [] };
const mockTimetable = { slots: [] };
const mockWakeupBedtime = { days: [] };
vi.mocked(api.fetchDailySummary).mockResolvedValueOnce(mockEmptyDailySummary);
vi.mocked(api.fetchTimetable).mockResolvedValueOnce(mockTimetable);
vi.mocked(api.fetchWakeSleepTimes).mockResolvedValueOnce(mockWakeupBedtime);
const { result } = renderHook(() => useStats('2024-01-01', '2024-01-02'), { wrapper });
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.dailySummary).toEqual(mockEmptyDailySummary);
expect(result.current.transformedDailySummary).toEqual([]);
expect(result.current.error).toBeNull();
});
it('should handle error state when the API request fails', async () => {
const error = new Error('API Failure');
vi.mocked(api.fetchDailySummary).mockRejectedValueOnce(error);
vi.mocked(api.fetchTimetable).mockResolvedValueOnce({ slots: [] });
vi.mocked(api.fetchWakeSleepTimes).mockResolvedValueOnce({ days: [] });
const { result } = renderHook(() => useStats('2024-01-01', '2024-01-02'), { wrapper });
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.error).toBeDefined();
expect(result.current.error).toEqual(error);
});
it('should handle loading state transitions', async () => {
let resolvePromise: (value: any) => void;
const pendingPromise = new Promise((resolve) => {
resolvePromise = resolve;
});
vi.mocked(api.fetchDailySummary).mockReturnValueOnce(pendingPromise as any);
vi.mocked(api.fetchTimetable).mockResolvedValueOnce({ slots: [] });
vi.mocked(api.fetchWakeSleepTimes).mockResolvedValueOnce({ days: [] });
const { result } = renderHook(() => useStats('2024-01-01', '2024-01-02'), { wrapper });
// Should be loading while promise is pending
expect(result.current.isLoading).toBe(true);
// Resolve the promise
resolvePromise!({ days: [] });
await waitFor(() => expect(result.current.isLoading).toBe(false));
});
});
describe('useSummary', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={createTestQueryClient()}>
{children}
</QueryClientProvider>
);
beforeEach(() => {
vi.clearAllMocks();
});
it('should return data on success', async () => {
const mockSummary = {
total_sleeps: 10,
avg_sleeps_per_day: 2,
total_sleeps_all_time: 100,
avg_sleeps_per_day_all_time: 2.5,
};
vi.mocked(api.fetchSummary).mockResolvedValueOnce(mockSummary);
const { result } = renderHook(() => useSummary('2024-01-01', '2024-01-02'), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual(mockSummary);
expect(api.fetchSummary).toHaveBeenCalledWith('2024-01-01', '2024-01-02');
});
it('should handle error on failure', async () => {
const error = new Error('API Failure');
vi.mocked(api.fetchSummary).mockRejectedValueOnce(error);
const { result } = renderHook(() => useSummary('2024-01-01', '2024-01-02'), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(error);
});
});