import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, fireEvent, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import { SessionList } from '../../../../src/components/stats/SessionList'; import { useSessions } from '../../../../src/hooks/useSleepSession'; import { useSettings } from '../../../../src/hooks/useSettings'; vi.mock('../../../../src/hooks/useSleepSession'); vi.mock('../../../../src/hooks/useSettings'); // Mock Pagination component to simplify integration testing vi.mock('../../../../src/components/stats/Pagination', () => ({ Pagination: vi.fn(({ currentPage, totalPages, onPageChange }) => (
{currentPage} / {totalPages}
)), })); const mockSession = { id: 1, start_time: '2026-06-04T11:58:00', // UTC end_time: '2026-06-04T13:58:00', // UTC notes: null, created_at: '2026-06-04T11:58:00', updated_at: '2026-06-04T11:58:00', }; describe('SessionList', () => { const mockUpdateSession = vi.fn(); const mockDeleteSession = vi.fn(); beforeEach(() => { vi.clearAllMocks(); // Default mock implementation for useSettings vi.mocked(useSettings).mockReturnValue({ settings: { sessionsPerPage: 10 } as any, }); // Mock window.confirm and alert to prevent actual popups during tests vi.spyOn(window, 'confirm').mockImplementation(() => true); vi.spyOn(window, 'alert').mockImplementation(() => {}); }); it('displays loading state', () => { vi.mocked(useSessions).mockReturnValue({ sessions: [], isLoading: true, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); expect(screen.getByText(/Loading sessions.../i)).toBeInTheDocument(); }); it('displays empty state when no sessions are recorded', () => { vi.mocked(useSessions).mockReturnValue({ sessions: [], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); expect(screen.getByText(/No sessions recorded./i)).toBeInTheDocument(); }); it('renders session rows with correct start time and duration', () => { vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); // Check that the session row is rendered const row = screen.getByTestId('session-row'); expect(row).toBeInTheDocument(); // Check for presence of duration (13:58 - 11:58 = 2h 0m) // Note: formatMinutes(120) -> "2h 0m" // Using getAllByText because duration is rendered twice (desktop and mobile views) const durations = screen.getAllByText(/2h 0m/i); expect(durations.length).toBeGreaterThan(0); }); it('handles editing flow: start, cancel and save', async () => { vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); // 1. Start Editing const row = screen.getByTestId('session-row'); fireEvent.click(row); // Verify edit inputs are visible expect(screen.getByLabelText(/Start Time/i)).toBeInTheDocument(); expect(screen.getByLabelText(/End Time/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Save/i })).toBeInTheDocument(); // 2. Cancel Edit fireEvent.click(screen.getByRole('button', { name: /Cancel/i })); expect(mockUpdateSession).not.toHaveBeenCalled(); // Should be back in view mode (row should be visible again) expect(screen.getByTestId('session-row')).toBeInTheDocument(); // 3. Save Edit fireEvent.click(screen.getByTestId('session-row')); // Re-enter edit mode const startInput = screen.getByLabelText(/Start Time/i) as HTMLInputElement; const endInput = screen.getByLabelText(/End Time/i) as HTMLInputElement; const newStart = '2026-06-04T14:00'; const newEnd = '2026-06-04T15:00'; fireEvent.change(startInput, { target: { value: newStart } }); fireEvent.change(endInput, { target: { value: newEnd } }); fireEvent.click(screen.getByRole('button', { name: /Save/i })); await waitFor(() => { expect(mockUpdateSession).toHaveBeenCalledWith({ sessionId: 1, payload: expect.objectContaining({ start_time: expect.any(String), end_time: expect.any(String) }) }); }); // Check if it exited edit mode (row is back) expect(screen.getByTestId('session-row')).toBeInTheDocument(); }); it('handles deletion flow', async () => { vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); // Clicking the delete button (Trash2 icon) const deleteButtons = screen.getAllByTitle('Delete session'); fireEvent.click(deleteButtons[0]); // Verify confirmation was called expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to delete this sleep session?'); // If confirmed (mockImplementation returns true) await waitFor(() => { expect(mockDeleteSession).toHaveBeenCalledWith(1); }); }); it('disables delete button when isDeleting is true', () => { vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: true, // DELETING! } as any); render(); const deleteButtons = screen.getAllByTitle('Delete session'); expect(deleteButtons[0]).toBeDisabled(); }); it('integrates with pagination when sessions exceed page size', () => { const manySessions = Array.from({ length: 15 }, (_, i) => ({ ...mockSession, id: i + 1, })); vi.mocked(useSessions).mockReturnValue({ sessions: manySessions, isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); // Pagination should be rendered (using the mocked test-id) expect(screen.getByTestId('pagination-mock')).toBeInTheDocument(); }); it('verifies timezone accuracy for Stockholm/UTC', () => { vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); const clickableRow = screen.getByTestId('session-row'); fireEvent.click(clickableRow); const startTimeInput = screen.getByLabelText(/Start Time/i) as HTMLInputElement; const endTimeInput = screen.getByLabelText(/End Time/i) as HTMLInputElement; // Stockholm is UTC+2 in June (daylight savings). // mockSession start_time: '2026-06-04T11:58:00' (UTC) // Stockholm local time: 2026-06-04T13:58 expect(startTimeInput.value).toBe('2026-06-04T13:58'); expect(endTimeInput.value).toBe('2026-06-04T15:58'); }); it('handles updateSession error gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {}); vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: vi.fn().mockRejectedValue(new Error('Update failed')), deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); const row = screen.getByTestId('session-row'); fireEvent.click(row); const saveButton = screen.getByRole('button', { name: /Save/i }); fireEvent.click(saveButton); await waitFor(() => { expect(alertSpy).toHaveBeenCalledWith('Failed to update session'); expect(consoleSpy).toHaveBeenCalled(); }); consoleSpy.mockRestore(); alertSpy.mockRestore(); }); it('handles deleteSession error gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {}); vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: vi.fn().mockRejectedValue(new Error('Delete failed')), isDeleting: false, } as any); render(); const deleteButtons = screen.getAllByTitle('Delete session'); fireEvent.click(deleteButtons[0]); await waitFor(() => { expect(alertSpy).toHaveBeenCalledWith('Failed to delete session'); expect(consoleSpy).toHaveBeenCalled(); }); consoleSpy.mockRestore(); alertSpy.mockRestore(); }); it('renders in-progress session correctly (without end time)', () => { const inProgressSession = { ...mockSession, end_time: null } as any; vi.mocked(useSessions).mockReturnValue({ sessions: [inProgressSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); render(); expect(screen.getByText(/In progress/i)).toBeInTheDocument(); expect(screen.getAllByText(/—/i).length).toBeGreaterThan(0); }); it('handles deletion cancellation', async () => { vi.mocked(useSessions).mockReturnValue({ sessions: [mockSession], isLoading: false, updateSession: mockUpdateSession, deleteSession: mockDeleteSession, isDeleting: false, } as any); vi.spyOn(window, 'confirm').mockImplementation(() => false); render(); const deleteButtons = screen.getAllByTitle('Delete session'); fireEvent.click(deleteButtons[0]); expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to delete this sleep session?'); await waitFor(() => { expect(mockDeleteSession).not.toHaveBeenCalled(); }); }); });