205 lines
7.8 KiB
TypeScript
205 lines
7.8 KiB
TypeScript
/* 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(<Timetable {...defaultProps} />);
|
|
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(<Timetable {...defaultProps} />);
|
|
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(<Timetable {...defaultProps} />);
|
|
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(<Timetable {...singleDayProps} />);
|
|
// 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(<Timetable {...largeRangeProps} />);
|
|
// 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(<Timetable {...invalidRangeProps} />);
|
|
// 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(<Timetable {...defaultProps} />);
|
|
|
|
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(<Timetable {...defaultProps} />);
|
|
|
|
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(<Timetable {...defaultProps} />);
|
|
|
|
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(<Timetable {...defaultProps} />);
|
|
|
|
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(<Timetable {...defaultProps} />);
|
|
|
|
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(<Timetable {...defaultProps} />);
|
|
|
|
const marker = container.querySelector('.bg-red-500');
|
|
expect(marker).toBeInTheDocument();
|
|
expect(marker).toHaveStyle({ top: '384px' });
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
}); |