Add migration script and work on tests

This commit is contained in:
2026-05-17 19:15:55 +02:00
parent 81120547ae
commit e23f8904ef
12 changed files with 2300 additions and 2658 deletions
@@ -0,0 +1,102 @@
import { render, screen } from '@testing-library/react';
import { vi, describe, it, expect } from 'vitest';
import Timetable from './SleepTimetable';
import { useStats } from '../../hooks/useStats';
vi.mock('../../hooks/useStats', () => ({
useStats: vi.fn(),
}));
describe('SleepTimetable', () => {
const defaultProps = {
from_date: '2025-04-20',
to_date: '2025-04-21',
day_start: '22:00',
day_end: '08:00',
};
it('renders loading state', () => {
(useStats as any).mockReturnValue({ timetable: null, isLoading: true });
render(<Timetable {...defaultProps} />);
expect(screen.getByText(/Loading timetable.../i)).toBeInTheDocument();
});
it('renders empty state when no data is available', () => {
(useStats as any).mockReturnValue({ timetable: { slots: [] }, isLoading: false });
render(<Timetable {...defaultProps} />);
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
});
it('renders the grid correctly with hourly rows and biennial labels', () => {
(useStats as any).mockReturnValue({
timetable: {
slots: [
{
date: '2025-04-20',
sleeping: true,
slot_start: '01:00',
slot_end: '05:00',
},
],
},
isLoading: false,
});
render(<Timetable {...defaultProps} />);
// Check for day headers
expect(screen.getByText(/SUN/i)).toBeInTheDocument(); // 2025-04-20 is Sunday
expect(screen.getByText(/MON/i)).toBeInTheDocument(); // 2025-04-21 is Monday
// Check for time labels (every 2 hours: 00, 02, 04...)
expect(screen.getByText('00:00')).toBeInTheDocument();
expect(screen.getByText('02:00')).toBeInTheDocument();
expect(screen.getByText('04:00')).toBeInTheDocument();
expect(screen.getByText('22:00')).toBeInTheDocument();
// Verify non-labeled hours (every hour that isn't %120 == 0)
// We can't easily check for empty text nodes in Vitest/RTL without more precision
// but we can check that 01:00 is NOT present as a text label
expect(screen.queryByText('01:00')).not.toBeInTheDocument();
});
it('colors sleeping intervals correctly', () => {
(useStats as any).mockReturnValue({
timetable: {
slots: [
{
date: '2025-04-20',
sleeping: true,
slot_start: '01:00',
slot_end: '03:00', // Covers 01:00 and 02:00
},
],
},
isLoading: false,
});
const { container } = render(<Timetable {...defaultProps} />);
// We are looking for the grid cells.
// Since they don't have text, we look at their classes/backgrounds.
const cells = container.querySelectorAll('.h-8');
// Total cells = 24 rows * (1 label col + 2 days) = 72? No, the structure is:
// Header row has cells. Time grid rows have (1 label + 2 days) = 3 cells each.
// Total elements with .h-8: 1 (header corner) + days.length * 1 (day headers) + 24 * 3 = 75?
// Actually, the code has:
// Header row: header corner + days.map -> 1 + 2 = 3 cells
// Time Rows: (label cell) + days.map -> 24 * 3 = 72 cells
// Total = 75.
// The sleeping cell for 01:00 on 2025-04-20 should be bg-green-500
// The cell for 02:00 on 2025-04-20 should be bg-green-500
// The cell for 03:00 on 2025-04-20 should be bg-gray-50
// Let's just check if ANY cell has bg-green-500 and ALL non-sleeping have bg-gray-50
const greenCells = container.querySelectorAll('.bg-green-500');
expect(greenCells.length).toBeGreaterThan(0);
const grayCells = container.querySelectorAll('.bg-gray-50');
expect(grayCells.length).toBeGreaterThan(0);
});
});
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { useStats } from '../../hooks/useStats';
import { timeToMinutes, formatMinutesAfter } from '../../utils/time';
import { timeToMinutes } from '../../utils/time';
interface TimetableProps {
from_date: string;
@@ -25,7 +25,7 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
}, [from_date, to_date]);
const timeSteps = useMemo(() => {
return Array.from({ length: 12 }, (_, i) => i * 120);
return Array.from({ length: 24 }, (_, i) => i * 60);
}, []);
if (isLoading) return <div className="text-center p-4">Loading timetable...</div>;
@@ -62,58 +62,56 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
const currentMins = new Date().getHours() * 60 + new Date().getMinutes();
// Total rows: 1 (header) + 12 (time steps) = 13
const totalRows = 13;
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto">
<h3 className="text-lg font-semibold mb-4 text-gray-800">Sleep Timetable</h3>
<div className="min-w-[600px] flex flex-col gap-px">
{/* Header Row */}
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
<div className="h-8" /> {/* Empty corner cell */}
{days.map((day) => (
<div key={day} className="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8">
{getDayName(day)}
</div>
))}
</div>
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto">
<h3 className="text-lg font-semibold mb-4 text-gray-800">Sleep Timetable</h3>
<div className="min-w-[600px] flex flex-col gap-px">
{/* Header Row */}
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
<div className="h-8" /> {/* Empty corner cell */}
{days.map((day) => (
<div key={day} className="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8">
{getDayName(day)}
</div>
))}
</div>
{/* Time Grid Container */}
<div className="relative">
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
{/* Time Rows */}
{timeSteps.map((step) => (
<React.Fragment key={step}>
{/* Time Label */}
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8">
{step % 120 === 0 ? formatTimeLabel(step) : ''}
</div>
{/* Grid Cells */}
{days.map((day) => {
const sleeping = isSleepingInInterval(day, step, step + 60);
return (
<div
key={`${day}-${step}`}
className={`h-8 w-full ${sleeping ? 'bg-green-500' : 'bg-gray-50'}`}
/>
);
})}
</React.Fragment>
))}
{/* Time Grid Container */}
<div className="relative">
<div
className="grid gap-px"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
{/* Time Rows */}
{timeSteps.map((step) => (
<React.Fragment key={step}>
{/* Time Label */}
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8">
{formatTimeLabel(step)}
</div>
{/* Grid Cells */}
{days.map((day) => {
const sleeping = isSleepingInInterval(day, step, step + 120);
return (
<div
key={`${day}-${step}`}
className={`h-8 w-full ${sleeping ? 'bg-green-500' : 'bg-gray-50'}`}
/>
);
})}
</React.Fragment>
))}
{/* Red Marker Line */}
{/* Red Marker Line */}
<div
className="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style={{