A try of overhauling the frontend tests, bailed after 24h+
This commit is contained in:
@@ -1,55 +1,316 @@
|
||||
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
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', () => ({
|
||||
useSessions: () => ({
|
||||
sessions: [
|
||||
{
|
||||
id: 1,
|
||||
start_time: '2026-06-04T11:58:00',
|
||||
end_time: '2026-06-04T13:58:00',
|
||||
notes: null,
|
||||
created_at: '2026-06-04T11:58:00',
|
||||
updated_at: '2026-06-04T11:58:00',
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
updateSession: vi.fn(),
|
||||
deleteSession: vi.fn(),
|
||||
isDeleting: false,
|
||||
}),
|
||||
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 }) => (
|
||||
<div data-testid="pagination-mock">
|
||||
<span>{currentPage} / {totalPages}</span>
|
||||
<button onClick={() => onPageChange(currentPage + 1)}>Next Page</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock('../../../../src/hooks/useSettings', () => ({
|
||||
useSettings: () => ({ settings: { sessionsPerPage: 10 } }),
|
||||
}));
|
||||
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',
|
||||
};
|
||||
|
||||
const originalTZ = process.env.TZ;
|
||||
describe('SessionList', () => {
|
||||
const mockUpdateSession = vi.fn();
|
||||
const mockDeleteSession = vi.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.TZ = 'Europe/Stockholm';
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Default mock implementation for useSettings
|
||||
vi.mocked(useSettings).mockReturnValue({
|
||||
settings: { sessionsPerPage: 10 } as any,
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.TZ = originalTZ;
|
||||
});
|
||||
// Mock window.confirm and alert to prevent actual popups during tests
|
||||
vi.spyOn(window, 'confirm').mockImplementation(() => true);
|
||||
vi.spyOn(window, 'alert').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('SessionList edit timezone', () => {
|
||||
it('pre-fills edit inputs with local Stockholm time, not UTC', () => {
|
||||
const { container } = render(<SessionList />);
|
||||
it('displays loading state', () => {
|
||||
vi.mocked(useSessions).mockReturnValue({
|
||||
sessions: [],
|
||||
isLoading: true,
|
||||
updateSession: mockUpdateSession,
|
||||
deleteSession: mockDeleteSession,
|
||||
isDeleting: false,
|
||||
} as any);
|
||||
|
||||
const clickableRow = container.querySelector('.cursor-pointer') as HTMLElement;
|
||||
expect(clickableRow).not.toBeNull();
|
||||
render(<SessionList />);
|
||||
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(<SessionList />);
|
||||
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(<SessionList />);
|
||||
|
||||
// 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(<SessionList />);
|
||||
|
||||
// 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(<SessionList />);
|
||||
|
||||
// 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(<SessionList />);
|
||||
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(<SessionList />);
|
||||
|
||||
// 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(<SessionList />);
|
||||
|
||||
const clickableRow = screen.getByTestId('session-row');
|
||||
fireEvent.click(clickableRow);
|
||||
|
||||
const inputs = container.querySelectorAll('input[type="datetime-local"]');
|
||||
expect(inputs).toHaveLength(2);
|
||||
const startTimeInput = screen.getByLabelText(/Start Time/i) as HTMLInputElement;
|
||||
const endTimeInput = screen.getByLabelText(/End Time/i) as HTMLInputElement;
|
||||
|
||||
// Backend sends 11:58 UTC (no Z). Stockholm is UTC+2, so local time is 13:58.
|
||||
expect((inputs[0] as HTMLInputElement).value).toBe('2026-06-04T13:58');
|
||||
// Backend sends 13:58 UTC → Stockholm 15:58
|
||||
expect((inputs[1] as HTMLInputElement).value).toBe('2026-06-04T15:58');
|
||||
// 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(<SessionList />);
|
||||
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(<SessionList />);
|
||||
|
||||
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(<SessionList />);
|
||||
|
||||
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(<SessionList />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user