/* eslint-disable @typescript-eslint/no-explicit-any */
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import Timetable from '../../../../src/components/stats/SleepTimetable';
import { useStats } from '../../../../src/hooks/useStats';
vi.mock('../../../../src/hooks/useStats', () => ({
useStats: vi.fn(),
}));
describe('SleepTimetable', () => {
const defaultProps = {
from_date: '2025-04-18', // Friday
to_date: '2025-04-21', // Monday
day_start: '22:00',
day_end: '08:00',
};
beforeEach(() => {
vi.clearAllMocks();
// Default to desktop width
vi.stubGlobal('innerWidth', 1024);
});
afterEach(() => {
vi.useRealTimers();
});
describe('Loading & Empty States', () => {
it('renders loading state', () => {
(useStats as any).mockReturnValue({ timetable: null, isLoading: true });
render();
expect(screen.getByText(/Loading timetable.../i)).toBeInTheDocument();
});
it('renders empty state when timetable.slots is empty', () => {
(useStats as any).mockReturnValue({ timetable: { slots: [] }, isLoading: false });
render();
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
});
it('renders empty state when timetable is null', () => {
(useStats as any).mockReturnValue({ timetable: null, isLoading: false });
render();
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
});
});
describe('Date Range Edge Cases', () => {
it('handles a single day range correctly', () => {
const singleDayProps = { ...defaultProps, from_date: '2025-04-18', to_date: '2025-04-18' };
(useStats as any).mockReturnValue({
timetable: { slots: [{ date: '2025-04-18', sleeping: true, slot_start: '10:00', slot_end: '11:00' }] },
isLoading: false
});
render();
// Check if it renders the single day header
// Check if it renders the single day header with date format
expect(screen.getByText(/Fri 18\/4/i)).toBeInTheDocument();
});
it('handles a large date range', () => {
const largeRangeProps = { ...defaultProps, from_date: '2025-01-01', to_date: '2025-01-31' };
(useStats as any).mockReturnValue({
timetable: { slots: [{ date: '2025-01-05', sleeping: true, slot_start: '10:00', slot_end: '11:00' }] },
isLoading: false
});
render();
// Ensure it doesn't crash and renders the title
expect(screen.getByText(/Sleep Timetable/i)).toBeInTheDocument();
});
it('handles empty date range (start > end)', () => {
const invalidRangeProps = { ...defaultProps, from_date: '2025-04-21', to_date: '2025-04-18' };
(useStats as any).mockReturnValue({ timetable: { slots: [] }, isLoading: false });
render();
// Should gracefully handle empty days array (no crash) and show no data
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
});
});
describe('Responsive Tiers', () => {
const mockTimetable = {
slots: [
{ date: '2025-04-18', sleeping: true, slot_start: '23:00', slot_end: '07:00' },
],
};
it('Desktop Tier (>= 768px): shows all days', () => {
(useStats as any).mockReturnValue({ timetable: mockTimetable, isLoading: false });
vi.stubGlobal('innerWidth', 1024);
render();
expect(screen.getByText(/Fri 18\/4/i)).toBeInTheDocument();
expect(screen.getByText(/Sat 19\/4/i)).toBeInTheDocument();
expect(screen.getByText(/Sun 20\/4/i)).toBeInTheDocument();
expect(screen.getByText(/Mon 21\/4/i)).toBeInTheDocument();
});
it('Medium Tier (400-768px): shows tab groups and updates on click', () => {
(useStats as any).mockReturnValue({ timetable: mockTimetable, isLoading: false });
vi.stubGlobal('innerWidth', 500);
const { container } = render();
act(() => {
window.dispatchEvent(new Event('resize'));
});
const tabs = screen.getAllByRole('button');
expect(tabs.length).toBeGreaterThan(0);
const tab1 = tabs[0];
expect(tab1.textContent).toMatch(/19/);
expect(tab1.textContent).toMatch(/21/);
expect(tab1).toHaveClass('bg-blue-600');
const tab2 = tabs[1];
act(() => {
fireEvent.click(tab2);
});
expect(tab2).toHaveClass('bg-blue-600');
expect(container.querySelector('[data-testid^="cell-2025-04-18-"]')).toBeInTheDocument();
expect(container.querySelector('[data-testid^="cell-2025-04-21-"]')).not.toBeInTheDocument();
});
it('Small Tier (< 400px): shows single day tabs', () => {
(useStats as any).mockReturnValue({ timetable: mockTimetable, isLoading: false });
vi.stubGlobal('innerWidth', 300);
const { container } = render();
act(() => {
window.dispatchEvent(new Event('resize'));
});
expect(screen.getByRole('button', { name: /21/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /21\/4/i })).toBeInTheDocument();
act(() => {
fireEvent.click(screen.getByRole('button', { name: /20/i }));
});
expect(container.querySelector('[data-testid^="cell-2025-04-20-"]')).toBeInTheDocument();
expect(container.querySelector('[data-testid^="cell-2025-04-21-"]')).not.toBeInTheDocument();
});
});
describe('Grid Accuracy', () => {
it('renders sleeping cells correctly', () => {
const day = '2025-04-18';
(useStats as any).mockReturnValue({
timetable: {
slots: [
{ date: day, sleeping: true, slot_start: '10:00', slot_end: '10:30' }
],
},
isLoading: false,
});
vi.stubGlobal('innerWidth', 1024);
const { container } = render();
expect(container.querySelector(`[data-testid="cell-${day}-10:00"]`)).toHaveClass('bg-green-500');
expect(container.querySelector(`[data-testid="cell-${day}-10:15"]`)).toHaveClass('bg-green-500');
expect(container.querySelector(`[data-testid="cell-${day}-10:30"]`)).toHaveClass('bg-gray-50');
});
it('handles midnight spanning correctly using two slots (as implemented)', () => {
const day1 = '2025-04-18';
const day2 = '2025-04-19';
(useStats as any).mockReturnValue({
timetable: {
slots: [
{ date: day1, sleeping: true, slot_start: '23:00', slot_end: '00:00' },
{ date: day2, sleeping: true, slot_start: '00:00', slot_end: '01:00' },
],
},
isLoading: false,
});
vi.stubGlobal('innerWidth', 1024);
const { container } = render();
expect(container.querySelector(`[data-testid="cell-${day1}-23:00"]`)).toHaveClass('bg-green-500');
expect(container.querySelector(`[data-testid="cell-${day2}-00:00"]`)).toHaveClass('bg-green-500');
});
});
describe('Time Marker', () => {
it('renders red marker at correct position', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-04-18T12:00:00'));
(useStats as any).mockReturnValue({
timetable: { slots: [{ date: '2025-04-18', sleeping: true, slot_start: '11:00', slot_end: '13:00' }] },
isLoading: false,
});
vi.stubGlobal('innerWidth', 1024);
const { container } = render();
const marker = container.querySelector('.bg-red-500');
expect(marker).toBeInTheDocument();
expect(marker).toHaveStyle({ top: '384px' });
vi.useRealTimers();
});
});
});