diff --git a/frontend/src/components/tracker/SleepButton.tsx b/frontend/src/components/tracker/SleepButton.tsx index 8d80fac..0e781b6 100644 --- a/frontend/src/components/tracker/SleepButton.tsx +++ b/frontend/src/components/tracker/SleepButton.tsx @@ -10,6 +10,10 @@ const SleepButton: React.FC = ({ time }) => { const isSleeping = !!activeSession; const handleClick = async () => { + if (!time) { + alert('Please select a valid time.'); + return; + } try { if (isSleeping && activeSession?.id) { await endSession({ sessionId: activeSession.id, endTime: time }); diff --git a/frontend/src/utils/time.ts b/frontend/src/utils/time.ts index a06b79f..2a2cff8 100644 --- a/frontend/src/utils/time.ts +++ b/frontend/src/utils/time.ts @@ -90,6 +90,9 @@ export function formatDateTime(date: Date | string): string { */ export function toUTCFromLocalInput(inputValue: string): string { const d = new Date(inputValue); + if (isNaN(d.getTime())) { + return ""; + } return d.toISOString(); } diff --git a/frontend/tests/unit/SleepButton.test.tsx b/frontend/tests/unit/SleepButton.test.tsx index f5a4a1d..7efbd0d 100644 --- a/frontend/tests/unit/SleepButton.test.tsx +++ b/frontend/tests/unit/SleepButton.test.tsx @@ -70,4 +70,20 @@ describe('SleepButton', () => { await waitFor(() => expect(endSessionMock).toHaveBeenCalled()); }); + + it('shows alert when time is invalid', async () => { + const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {}); + (useSleepSession as any).mockReturnValue({ + activeSession: null, + isCreating: false, + isEnding: false, + }); + + renderWithProviders(); + const button = screen.getByRole('button', { name: /Start Sleep/i }); + fireEvent.click(button); + + expect(alertSpy).toHaveBeenCalledWith('Please select a valid time.'); + alertSpy.mockRestore(); + }); }); diff --git a/frontend/tests/unit/TimeAdjuster.test.tsx b/frontend/tests/unit/TimeAdjuster.test.tsx index 4a1d5a4..c7448b6 100644 --- a/frontend/tests/unit/TimeAdjuster.test.tsx +++ b/frontend/tests/unit/TimeAdjuster.test.tsx @@ -29,4 +29,18 @@ describe('TimeAdjuster', () => { fireEvent.click(resetButton); expect(handleChange).toHaveBeenCalled(); }); + + it('handles invalid date input gracefully (returns empty string)', async () => { + // Since we changed the implementation, it should no longer throw. + // We can't easily test the useEffect error anymore because it doesn't throw, + // but we can check if onTimeChange is called with "" for invalid input. + const handleChange = vi.fn(); + const { container } = render(); + const input = container.querySelector('input'); + + fireEvent.change(input!, { target: { value: '2026-06-0' } }); + // We might need to wait for the effect + await new Promise((r) => setTimeout(r, 0)); + expect(handleChange).toHaveBeenCalledWith(""); + }); }); diff --git a/frontend/tests/unit/time.test.ts b/frontend/tests/unit/time.test.ts index a2ef6be..5362c55 100644 --- a/frontend/tests/unit/time.test.ts +++ b/frontend/tests/unit/time.test.ts @@ -12,6 +12,7 @@ import { adjustForTimezone, getLocalOffsetFromReference, formatDateTime, + toUTCFromLocalInput, } from '../../src/utils/time'; describe('formatMinutesAfter', () => { @@ -152,3 +153,18 @@ describe('getLocalOffsetFromReference', () => { expect(offset).toBe(localMins - refMins); }); }); + +describe('toUTCFromLocalInput', () => { + it('converts valid datetime-local string to ISO UTC string', () => { + const input = '2024-01-15T10:00'; + // This is tricky because it depends on local timezone. + // But we just want to see if it works for a valid input. + const result = toUTCFromLocalInput(input); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/); + }); + + it('returns empty string on invalid datetime-local string', () => { + expect(toUTCFromLocalInput('2026-06-0')).toBe(''); + }); +}); +