import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import TimeAdjuster from '../../src/components/tracker/TimeAdjuster'; import { toLocalDatetimeInputValue } from '../../src/utils/time'; describe('TimeAdjuster', () => { const MOCK_NOW = new Date('2024-06-10T12:00:00Z'); beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(MOCK_NOW); }); afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); }); it('initializes with the current time when no defaultValue is provided', () => { render( {}} />); const input = screen.getByLabelText(/Time/i) as HTMLInputElement; // The value should be the local format of MOCK_NOW expect(input.value).toBe(toLocalDatetimeInputValue(MOCK_NOW)); }); it('initializes with the provided defaultValue converted to local format', () => { const defaultValue = '2024-01-01T10:00:00Z'; const expectedLocal = toLocalDatetimeInputValue(new Date(defaultValue)); render( {}} defaultValue={defaultValue} />); const input = screen.getByLabelText(/Time/i) as HTMLInputElement; expect(input.value).toBe(expectedLocal); }); it('calls onTimeChange with UTC time when manual value is changed via input', async () => { const onTimeChange = vi.fn(); render(); const input = screen.getByLabelText(/Time/i) as HTMLInputElement; // Change to a specific local time const newLocalTime = '2024-06-11T15:45'; fireEvent.change(input, { target: { value: newLocalTime } }); // onTimeChange should be called with the UTC equivalent // We need to be careful about how Date(newLocalTime) interprets it. // Without timezone, it's usually treated as local time. const expectedUTC = new Date(newLocalTime).toISOString(); // Since onTimeChange is in a useEffect, we might need to wait or use vi.runAllTimers() // But standard React state updates + useEffect should be synchronous enough for fireEvent in Vitest expect(onTimeChange).toHaveBeenCalledWith(expectedUTC); }); it('resets to current time when "Reset to now" button is clicked', () => { const onTimeChange = vi.fn(); render(); const input = screen.getByLabelText(/Time/i) as HTMLInputElement; // 1. Change value to something else const differentTime = '2023-01-01T00:00'; fireEvent.change(input, { target: { value: differentTime } }); expect(input.value).toBe(differentTime); // 2. Click reset const resetButton = screen.getByText(/Reset to now/i); fireEvent.click(resetButton); // 3. Verify input is back to MOCK_NOW's local format expect(input.value).toBe(toLocalDatetimeInputValue(MOCK_NOW)); // 4. Verify onTimeChange was called with MOCK_NOW's UTC format expect(onTimeChange).toHaveBeenLastCalledWith(MOCK_NOW.toISOString()); }); it('handles invalid date input gracefully by calling onTimeChange with an empty string', async () => { const onTimeChange = vi.fn(); render(); const input = screen.getByLabelText(/Time/i) as HTMLInputElement; // Simulate invalid input (though datetime-local usually prevents this via UI, // we test the logic in toUTCFromLocalInput) fireEvent.change(input, { target: { value: 'not-a-date' } }); // Wait for useEffect await vi.waitFor(() => { expect(onTimeChange).toHaveBeenCalledWith(""); }); }); it('does not call onTimeChange if the input value is same as current (initialization check)', () => { // The component has an effect on [value, onTimeChange]. // On mount, it will call onTimeChange because value is set in useState. const onTimeChange = vi.fn(); render(); // Should be called once on mount due to useEffect [value] expect(onTimeChange).toHaveBeenCalledTimes(1); expect(onTimeChange).toHaveBeenCalledWith(MOCK_NOW.toISOString()); }); it('does not call onTimeChange if it is not provided (edge case for coverage)', () => { // @ts-ignore - testing the branch where onTimeChange might be undefined render(); }); });