Initial commit

This commit is contained in:
2026-04-24 08:02:13 +02:00
commit 867c06295c
76 changed files with 10573 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import SleepButton from '../../src/components/tracker/SleepButton';
import { useSleepSession } from '../../src/hooks/useSleepSession';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
vi.mock('../../src/hooks/useSleepSession', () => ({
useSleepSession: vi.fn(),
}));
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
describe('SleepButton', () => {
let queryClient: any;
beforeEach(() => {
queryClient = createTestQueryClient();
vi.clearAllMocks();
});
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
);
};
it('renders "Start Sleep" when no active session', async () => {
(useSleepSession as any).mockReturnValue({
activeSession: null,
isCreating: false,
isEnding: false,
});
renderWithProviders(<SleepButton time="2024-01-01T12:00" />);
expect(screen.getByRole('button', { name: /Start Sleep/i })).toBeInTheDocument();
});
it('renders "Stop Sleep" when session is active', async () => {
(useSleepSession as any).mockReturnValue({
activeSession: { id: 1, start_time: '2024-01-01T12:00' },
isCreating: false,
isEnding: false,
});
renderWithProviders(<SleepButton time="2024-01-01T12:00" />);
expect(screen.getByRole('button', { name: /Stop Sleep/i })).toBeInTheDocument();
});
it('calls endSession when session is active and button is clicked', async () => {
const endSessionMock = vi.fn();
(useSleepSession as any).mockReturnValue({
activeSession: { id: 1, start_time: '2024-01-01T12:00' },
isCreating: false,
isEnding: false,
endSession: endSessionMock,
});
renderWithProviders(<SleepButton time="2024-01-01T14:00" />);
const button = screen.getByRole('button', { name: /Stop Sleep/i });
fireEvent.click(button);
await waitFor(() => expect(endSessionMock).toHaveBeenCalled());
});
});
+30
View File
@@ -0,0 +1,30 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import TimeAdjuster from '../../src/components/tracker/TimeAdjuster';
describe('TimeAdjuster', () => {
it('renders with default value', () => {
const { container } = render(<TimeAdjuster onTimeChange={() => {}} />);
const input = container.querySelector('input');
expect(input).toBeInTheDocument();
});
it('calls onTimeChange when value changes', () => {
const handleChange = vi.fn();
const { container } = render(<TimeAdjuster onTimeChange={handleChange} />);
const input = container.querySelector('input');
fireEvent.change(input!, { target: { value: '2024-01-01T12:00' } });
expect(handleChange).toHaveBeenCalledWith('2024-01-01T12:00');
});
it('resets to now when reset button is clicked', async () => {
const handleChange = vi.fn();
render(<TimeAdjuster onTimeChange={handleChange} />);
const resetButton = screen.getByText(/Reset to now/i);
fireEvent.click(resetButton);
expect(handleChange).toHaveBeenCalled();
});
});
+113
View File
@@ -0,0 +1,113 @@
import { describe, it, expect } from 'vitest';
import {
buildTimetableSlots,
computeWakeupMinutes,
computeBedtimeMinutes,
computeDailySummaryData,
computeAverageStats,
reverseDateOrder,
filterDaysWithData,
} from '../../src/utils/chartHelpers';
import type { TimetableSlot, WakeupBedtimeDay, DailySummaryDay } from '../../src/types';
describe('buildTimetableSlots', () => {
it('groups slots by date', () => {
const slots: TimetableSlot[] = [
{ date: '2024-01-15', slot_start: '00:00', slot_end: '00:15', sleeping: true },
{ date: '2024-01-15', slot_start: '00:15', slot_end: '00:30', sleeping: false },
{ date: '2024-01-16', slot_start: '00:00', slot_end: '00:15', sleeping: true },
];
const result = buildTimetableSlots(slots);
expect(Object.keys(result)).toHaveLength(2);
expect(result['2024-01-15']).toHaveLength(2);
expect(result['2024-01-16']).toHaveLength(1);
});
});
describe('computeWakeupMinutes', () => {
it('transforms wakeup data for chart', () => {
const days: WakeupBedtimeDay[] = [
{ date: '2024-01-15', wakeup_minutes: 195, bedtime_minutes: 75 },
];
const result = computeWakeupMinutes(days, '04:00');
expect(result[0].time).toBe('07:15');
expect(result[0].minutes).toBe(195);
});
it('handles null values', () => {
const days: WakeupBedtimeDay[] = [
{ date: '2024-01-15', wakeup_minutes: null, bedtime_minutes: null },
];
const result = computeWakeupMinutes(days, '04:00');
expect(result[0].time).toBe('N/A');
});
});
describe('computeBedtimeMinutes', () => {
it('transforms bedtime data for chart', () => {
const days: WakeupBedtimeDay[] = [
{ date: '2024-01-15', wakeup_minutes: 195, bedtime_minutes: 75 },
];
const result = computeBedtimeMinutes(days, '19:00');
expect(result[0].time).toBe('20:15');
});
});
describe('computeDailySummaryData', () => {
it('transforms daily summary for BarChart', () => {
const days: DailySummaryDay[] = [
{ date: '2024-01-15', total_minutes: 720, day_minutes: 120, night_minutes: 600 },
];
const result = computeDailySummaryData(days);
expect(result[0]).toEqual({
date: '2024-01-15',
daySleep: 120,
nightSleep: 600,
total: 720,
});
});
});
describe('computeAverageStats', () => {
it('calculates averages from daily data', () => {
const days: DailySummaryDay[] = [
{ date: '2024-01-15', total_minutes: 720, day_minutes: 120, night_minutes: 600 },
{ date: '2024-01-16', total_minutes: 600, day_minutes: 100, night_minutes: 500 },
];
const result = computeAverageStats(days);
expect(result.avgTotal).toBe(660);
expect(result.avgDay).toBe(110);
expect(result.avgNight).toBe(550);
expect(result.count).toBe(2);
});
it('handles empty array', () => {
const result = computeAverageStats([]);
expect(result.avgTotal).toBe(0);
expect(result.count).toBe(0);
});
});
describe('reverseDateOrder', () => {
it('reverses array order', () => {
const days = [
{ date: '2024-01-16' },
{ date: '2024-01-15' },
{ date: '2024-01-14' },
];
const result = reverseDateOrder(days);
expect(result[0].date).toBe('2024-01-14');
});
});
describe('filterDaysWithData', () => {
it('filters days with zero total', () => {
const days = [
{ date: '2024-01-15', total_minutes: 720 },
{ date: '2024-01-16', total_minutes: 0 },
];
const result = filterDaysWithData(days);
expect(result).toHaveLength(1);
expect(result[0].date).toBe('2024-01-15');
});
});
+106
View File
@@ -0,0 +1,106 @@
import { describe, it, expect } from 'vitest';
import {
formatMinutesAfter,
toLocalDatetimeInputValue,
formatElapsedTime,
clampToDay,
parseTime,
formatMinutes,
getDaysAgo,
getToday,
} from '../../src/utils/time';
describe('formatMinutesAfter', () => {
it('converts positive minutes after reference', () => {
expect(formatMinutesAfter(75, '04:00')).toBe('05:15');
});
it('handles zero minutes', () => {
expect(formatMinutesAfter(0, '04:00')).toBe('04:00');
});
it('wraps around midnight for large values', () => {
expect(formatMinutesAfter(480, '04:00')).toBe('12:00');
});
it('handles null input', () => {
expect(formatMinutesAfter(null, '04:00')).toBe('N/A');
});
it('handles negative minutes', () => {
expect(formatMinutesAfter(-30, '04:00')).toBe('03:30');
});
});
describe('toLocalDatetimeInputValue', () => {
it('converts Date to datetime-local format', () => {
const date = new Date('2024-01-15T21:30:00Z');
const result = toLocalDatetimeInputValue(date);
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
});
});
describe('formatElapsedTime', () => {
it('formats hours and minutes', () => {
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
expect(formatElapsedTime(twoHoursAgo)).toBe('2h');
});
it('formats minutes only', () => {
const thirtyMinsAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
expect(formatElapsedTime(thirtyMinsAgo)).toBe('30m');
});
});
describe('clampToDay', () => {
it('clamps to start of day', () => {
const date = new Date('2024-01-15T15:30:00');
const result = clampToDay(date, 'start');
expect(result.getHours()).toBe(0);
expect(result.getMinutes()).toBe(0);
});
it('clamps to end of day', () => {
const date = new Date('2024-01-15T15:30:00');
const result = clampToDay(date, 'end');
expect(result.getHours()).toBe(23);
expect(result.getMinutes()).toBe(59);
});
});
describe('parseTime', () => {
it('parses HH:MM string to Date', () => {
const result = parseTime('07:30');
expect(result.getHours()).toBe(7);
expect(result.getMinutes()).toBe(30);
});
});
describe('formatMinutes', () => {
it('formats hours and minutes', () => {
expect(formatMinutes(90)).toBe('1h 30m');
});
it('formats hours only', () => {
expect(formatMinutes(120)).toBe('2h');
});
it('formats minutes only', () => {
expect(formatMinutes(45)).toBe('45m');
});
});
describe('getDaysAgo', () => {
it('returns date string for N days ago', () => {
const result = getDaysAgo(7);
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
});
describe('getToday', () => {
it('returns today date string', () => {
const result = getToday();
const today = new Date().toISOString().split('T')[0];
expect(result).toBe(today);
});
});