import { render, screen, fireEvent, act } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { CustomDatePicker, CustomDateTimePicker } from './CustomDateTimePicker'; import { useSettingsStore } from '../../store/settingsStore'; // ─── CustomDatePicker ───────────────────────────────────────────────────────── describe('CustomDatePicker', () => { const onChange = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); it('FE-COMP-DATEPICKER-001: renders without crashing', () => { render(); expect(document.body).toBeTruthy(); }); it('FE-COMP-DATEPICKER-002: shows placeholder when no value', () => { render(); expect(screen.getByText('Start Date')).toBeTruthy(); }); it('FE-COMP-DATEPICKER-003: shows formatted date when value is set', () => { render(); const btn = screen.getByRole('button'); // Locale-formatted date should contain "Mar" or "15" or "2026" expect(btn.textContent).toMatch(/Mar|15|2026/); }); it('FE-COMP-DATEPICKER-004: clicking button opens calendar portal', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button')); const dayBtns = screen.getAllByRole('button').filter(b => /^\d+$/.test(b.textContent?.trim() ?? '')); expect(dayBtns.length).toBeGreaterThan(0); }); it('FE-COMP-DATEPICKER-005: clicking a day calls onChange with correct ISO date', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button')); // open March 2026 const dayBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '15'); await user.click(dayBtn!); expect(onChange).toHaveBeenCalledWith('2026-03-15'); }); it('FE-COMP-DATEPICKER-006: prev month navigation decrements month', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button')); // open March 2026 // Nav buttons have no text content (only SVG icons) const emptyBtns = screen.getAllByRole('button').filter(b => b.textContent?.trim() === ''); await user.click(emptyBtns[0]); // left chevron = prev month expect(screen.getByText(/february 2026/i)).toBeTruthy(); }); it('FE-COMP-DATEPICKER-007: next month navigation increments month', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button')); // open March 2026 const emptyBtns = screen.getAllByRole('button').filter(b => b.textContent?.trim() === ''); await user.click(emptyBtns[emptyBtns.length - 1]); // right chevron = next month expect(screen.getByText(/april 2026/i)).toBeTruthy(); }); it('FE-COMP-DATEPICKER-008: clear button calls onChange with empty string', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button')); // open const clearBtn = screen.getByText('✕'); await user.click(clearBtn); expect(onChange).toHaveBeenCalledWith(''); }); it('FE-COMP-DATEPICKER-009: clear button absent when no value', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button')); // open expect(screen.queryByText('✕')).toBeNull(); }); it('FE-COMP-DATEPICKER-010: clicking outside calendar closes it', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button')); // open // Verify calendar is open (day buttons present) expect(screen.getAllByRole('button').filter(b => /^\d+$/.test(b.textContent?.trim() ?? '')).length).toBeGreaterThan(0); // Fire mousedown outside both the component div and the portal const outsideEl = document.createElement('div'); document.body.appendChild(outsideEl); await act(async () => { fireEvent.mouseDown(outsideEl); }); document.body.removeChild(outsideEl); // Day buttons should be gone expect(screen.getAllByRole('button').filter(b => /^\d+$/.test(b.textContent?.trim() ?? '')).length).toBe(0); }); it('FE-COMP-DATEPICKER-011: double-click activates text input mode', async () => { const user = userEvent.setup(); render(); await user.dblClick(screen.getByRole('button')); expect(screen.getByPlaceholderText('DD.MM.YYYY')).toBeTruthy(); }); it('FE-COMP-DATEPICKER-012: text input accepts ISO format YYYY-MM-DD', async () => { const user = userEvent.setup(); render(); await user.dblClick(screen.getByRole('button')); const input = screen.getByPlaceholderText('DD.MM.YYYY'); fireEvent.change(input, { target: { value: '2026-07-04' } }); fireEvent.keyDown(input, { key: 'Enter' }); expect(onChange).toHaveBeenCalledWith('2026-07-04'); }); it('FE-COMP-DATEPICKER-013: text input accepts EU format DD.MM.YYYY', async () => { const user = userEvent.setup(); render(); await user.dblClick(screen.getByRole('button')); const input = screen.getByPlaceholderText('DD.MM.YYYY'); fireEvent.change(input, { target: { value: '04.07.2026' } }); fireEvent.keyDown(input, { key: 'Enter' }); expect(onChange).toHaveBeenCalledWith('2026-07-04'); }); it('FE-COMP-DATEPICKER-014: Escape in text input cancels text mode', async () => { const user = userEvent.setup(); render(); await user.dblClick(screen.getByRole('button')); const input = screen.getByPlaceholderText('DD.MM.YYYY'); fireEvent.keyDown(input, { key: 'Escape' }); expect(screen.queryByPlaceholderText('DD.MM.YYYY')).toBeNull(); expect(screen.getByRole('button')).toBeTruthy(); }); }); // ─── CustomDateTimePicker ───────────────────────────────────────────────────── describe('CustomDateTimePicker', () => { const onChange = vi.fn(); beforeEach(() => { vi.clearAllMocks(); // Use 24h format for predictable time input behavior useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, time_format: '24h' }, }); }); it('FE-COMP-DATEPICKER-015: renders date and time pickers side by side', () => { render(); // Date picker renders a trigger button expect(screen.getAllByRole('button').length).toBeGreaterThanOrEqual(1); // Time picker renders a text input expect(screen.getByRole('textbox')).toBeTruthy(); }); it('FE-COMP-DATEPICKER-016: setting a date-only value defaults time to 12:00', async () => { const user = userEvent.setup(); render(); // The date trigger is the first button const dateTrigger = screen.getAllByRole('button')[0]; await user.click(dateTrigger); // open calendar // Click day 1 const day1 = screen.getAllByRole('button').find(b => b.textContent?.trim() === '1'); await user.click(day1!); // onChange should have been called with T12:00 suffix expect(onChange).toHaveBeenCalledWith(expect.stringMatching(/T12:00$/)); }); it('FE-COMP-DATEPICKER-017: changing time part preserves date part', () => { render(); const timeInput = screen.getByRole('textbox'); fireEvent.change(timeInput, { target: { value: '10:00' } }); expect(onChange).toHaveBeenCalledWith('2026-06-01T10:00'); }); });