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,197 @@
/* 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('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} />);
// Check that all days from the range are present in headers.
expect(screen.getByText(/18/i)).toBeInTheDocument();
expect(screen.getByText(/19/i)).toBeInTheDocument();
expect(screen.getByText(/20/i)).toBeInTheDocument();
expect(screen.getByText(/21/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} />);
// Trigger resize effect
act(() => {
window.dispatchEvent(new Event('resize'));
});
// Tab groups: 21, 20, 19 (Group 1) and 18 (Group 2)
const tabs = screen.getAllByRole('button');
expect(tabs.length).toBeGreaterThan(0);
const tab1 = tabs[0];
// Check if it contains 19 and 21 (as the label is "Fri 19 - Sun 21" or similar)
expect(tab1.textContent).toMatch(/19/);
expect(tab1.textContent).toMatch(/21/);
// Check if it's active (blue bg)
expect(tab1).toHaveClass('bg-blue-600');
// Click second tab (Group 2: "Thu 18 - Thu 18")
const tab2 = tabs[1];
act(() => {
fireEvent.click(tab2);
});
expect(tab2).toHaveClass('bg-blue-600');
// Grid should now only show day 18. Check via cell test-id pattern.
expect(container.querySelector('[data-testid^="cell-2025-04-18-"]')).toBeInTheDocument();
// And NOT show 21 in the grid. We can check by looking for a cell of day 21.
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'));
});
// Tabs should be single days: 21, 20, 19, 18. Using role for specificity.
expect(screen.getByRole('button', { name: /21/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /20/i })).toBeInTheDocument();
// Click 20
act(() => {
fireEvent.click(screen.getByRole('button', { name: /20/i }));
});
// Grid should only show 20
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} />);
// 10:00 should be green
expect(container.querySelector(`[data-testid="cell-${day}-10:00"]`)).toHaveClass('bg-green-500');
// 10:15 should be green
expect(container.querySelector(`[data-testid="cell-${day}-10:15"]`)).toHaveClass('bg-green-500');
// 10:30 should NOT be green (it's the boundary)
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} />);
// Check Day 1, 23:00
expect(container.querySelector(`[data-testid="cell-${day1}-23:00"]`)).toHaveClass('bg-green-500');
// Check Day 2, 00:00
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();
// 12:00 PM is exactly halfway through the day (720/1440)
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} />);
// totalHeight = 96 steps * 8px = 768px
// top = (720/1440) * 768 = 384px
const marker = container.querySelector('.bg-red-500');
expect(marker).toBeInTheDocument();
expect(marker).toHaveStyle({ top: '384px' });
vi.useRealTimers();
});
});
});