import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { DatePicker } from '../../src/components/DatePicker'; type ControlledProps = { type: 'date' | 'time' | 'date-time'; initialValue: string; format?: string; min?: string; max?: string; onValueChange?: (value: string) => void; disabled?: boolean; }; function ControlledDatePicker({ type, initialValue, format, min, max, onValueChange, disabled = false, }: Readonly) { const [value, setValue] = useState(initialValue); return ( { setValue(event.target.value); onValueChange?.(event.target.value); }} /> ); } function pickCurrentMonthDay(label: string): void { const dayButtons = Array.from(document.querySelectorAll('.datepicker-day')) as HTMLButtonElement[]; const targetButton = dayButtons.find( (button) => button.textContent === label && !button.classList.contains('is-outside-month'), ); expect(targetButton).toBeDefined(); fireEvent.click(targetButton as HTMLButtonElement); } function getCurrentMonthDayButton(label: string): HTMLButtonElement { const dayButtons = Array.from(document.querySelectorAll('.datepicker-day')) as HTMLButtonElement[]; const targetButton = dayButtons.find( (button) => button.textContent === label && !button.classList.contains('is-outside-month'), ); if (!targetButton) { throw new Error(`Could not find day button for ${label}`); } return targetButton; } describe('DatePicker', () => { it('opens popup from icon button and closes with Escape', () => { render( {}} />); const input = screen.getByLabelText('Schedule') as HTMLInputElement; expect(input.type).toBe('text'); fireEvent.click(screen.getByRole('button', { name: 'Open date picker' })); expect( screen.getByRole('dialog', { name: 'Date and time picker popup' }), ).toBeInTheDocument(); fireEvent.keyDown(document, { key: 'Escape' }); expect( screen.queryByRole('dialog', { name: 'Date and time picker popup' }), ).not.toBeInTheDocument(); }); it('supports segment-by-segment editing with custom format and auto-advance', async () => { const onValueChange = vi.fn(); render( , ); const input = screen.getByLabelText('Schedule') as HTMLInputElement; fireEvent.focus(input); await waitFor(() => { expect(input.selectionStart).toBe(0); expect(input.selectionEnd).toBe(2); }); fireEvent.keyDown(input, { key: '1' }); await waitFor(() => { expect(input.value).toBe('01/02/2026 14:30'); expect(input.selectionStart).toBe(0); expect(input.selectionEnd).toBe(2); }); fireEvent.keyDown(input, { key: '1' }); await waitFor(() => { expect(input.value).toBe('11/02/2026 14:30'); expect(input.selectionStart).toBe(3); expect(input.selectionEnd).toBe(5); }); fireEvent.keyDown(input, { key: '0' }); fireEvent.keyDown(input, { key: '3' }); await waitFor(() => { expect(input.value).toBe('11/03/2026 14:30'); expect(input.selectionStart).toBe(6); expect(input.selectionEnd).toBe(10); }); fireEvent.keyDown(input, { key: '2' }); fireEvent.keyDown(input, { key: '0' }); fireEvent.keyDown(input, { key: '2' }); fireEvent.keyDown(input, { key: '7' }); await waitFor(() => { expect(input.value).toBe('11/03/2027 14:30'); expect(input.selectionStart).toBe(11); expect(input.selectionEnd).toBe(13); }); fireEvent.keyDown(input, { key: '1' }); fireEvent.keyDown(input, { key: '6' }); await waitFor(() => { expect(input.value).toBe('11/03/2027 16:30'); expect(input.selectionStart).toBe(14); expect(input.selectionEnd).toBe(16); }); fireEvent.keyDown(input, { key: '4' }); fireEvent.keyDown(input, { key: '5' }); await waitFor(() => { expect(input.value).toBe('11/03/2027 16:45'); }); expect(onValueChange).toHaveBeenCalled(); }); it('preserves year 0000 while editing and does not coerce to 19xx', async () => { render( , ); const input = screen.getByLabelText('Schedule') as HTMLInputElement; fireEvent.focus(input); await waitFor(() => { expect(input.selectionStart).toBe(0); expect(input.selectionEnd).toBe(2); }); fireEvent.keyDown(input, { key: '/' }); await waitFor(() => { expect(input.selectionStart).toBe(3); expect(input.selectionEnd).toBe(5); }); fireEvent.keyDown(input, { key: '/' }); await waitFor(() => { expect(input.selectionStart).toBe(6); expect(input.selectionEnd).toBe(10); }); fireEvent.keyDown(input, { key: '0' }); await waitFor(() => { expect(input.value).toBe('22/02/0000 14:30'); expect(input.selectionStart).toBe(6); expect(input.selectionEnd).toBe(10); }); fireEvent.keyDown(input, { key: '0' }); fireEvent.keyDown(input, { key: '0' }); fireEvent.keyDown(input, { key: '0' }); await waitFor(() => { expect(input.value).toBe('22/02/0000 14:30'); expect(input.selectionStart).toBe(11); expect(input.selectionEnd).toBe(13); }); }); it('uses separator keys to move between editable segments', async () => { render( , ); const input = screen.getByLabelText('Schedule') as HTMLInputElement; fireEvent.focus(input); await waitFor(() => { expect(input.selectionStart).toBe(0); expect(input.selectionEnd).toBe(2); }); fireEvent.keyDown(input, { key: '/' }); await waitFor(() => { expect(input.selectionStart).toBe(3); expect(input.selectionEnd).toBe(5); }); fireEvent.keyDown(input, { key: '/' }); await waitFor(() => { expect(input.selectionStart).toBe(6); expect(input.selectionEnd).toBe(10); }); fireEvent.keyDown(input, { key: ' ' }); await waitFor(() => { expect(input.selectionStart).toBe(11); expect(input.selectionEnd).toBe(13); }); fireEvent.keyDown(input, { key: ':' }); await waitFor(() => { expect(input.selectionStart).toBe(14); expect(input.selectionEnd).toBe(16); }); }); it('applies min/max constraints to calendar and time options', () => { render( , ); fireEvent.click(screen.getByRole('button', { name: 'Open date picker' })); expect(getCurrentMonthDayButton('9')).toBeDisabled(); expect(getCurrentMonthDayButton('10')).toBeEnabled(); expect(getCurrentMonthDayButton('11')).toBeDisabled(); const hoursList = screen.getByRole('listbox', { name: 'Hours' }); expect(within(hoursList).getByRole('button', { name: '08' })).toBeDisabled(); expect(within(hoursList).getByRole('button', { name: '09' })).toBeEnabled(); expect(within(hoursList).getByRole('button', { name: '10' })).toBeEnabled(); expect(within(hoursList).getByRole('button', { name: '11' })).toBeDisabled(); const minutesList = screen.getByRole('listbox', { name: 'Minutes' }); expect(within(minutesList).getByRole('button', { name: '15' })).toBeEnabled(); expect(within(minutesList).getByRole('button', { name: '20' })).toBeDisabled(); }); it('renders date mode calendar only and auto-closes after day selection', () => { const onValueChange = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: 'Open date picker' })); expect(screen.getByRole('dialog', { name: 'Date picker popup' })).toBeInTheDocument(); expect(screen.queryByText('Hours')).not.toBeInTheDocument(); expect(screen.queryByText('Minutes')).not.toBeInTheDocument(); pickCurrentMonthDay('15'); const input = screen.getByLabelText('Schedule') as HTMLInputElement; expect(input.value).toMatch(/^\d{4}\/\d{2}\/15$/); expect(onValueChange).toHaveBeenCalled(); expect(screen.queryByRole('dialog', { name: 'Date picker popup' })).not.toBeInTheDocument(); }); it('renders time mode selectors only and keeps popup open after selection', () => { const onValueChange = vi.fn(); render(); fireEvent.click(screen.getByRole('button', { name: 'Open date picker' })); expect(screen.getByRole('dialog', { name: 'Time picker popup' })).toBeInTheDocument(); expect(screen.queryByRole('grid')).not.toBeInTheDocument(); const hoursList = screen.getByRole('listbox', { name: 'Hours' }); const minutesList = screen.getByRole('listbox', { name: 'Minutes' }); fireEvent.click(within(hoursList).getByRole('button', { name: '13' })); fireEvent.click(within(minutesList).getByRole('button', { name: '45' })); const input = screen.getByLabelText('Schedule') as HTMLInputElement; expect(input.value).toBe('13:45'); expect(onValueChange).toHaveBeenCalledTimes(2); expect(screen.getByRole('dialog', { name: 'Time picker popup' })).toBeInTheDocument(); }); it('blocks popup interactions while disabled', () => { render(); const input = screen.getByLabelText('Schedule') as HTMLInputElement; const iconButton = screen.getByRole('button', { name: 'Open date picker' }); expect(input).toBeDisabled(); expect(iconButton).toBeDisabled(); fireEvent.click(iconButton); expect(screen.queryByRole('dialog', { name: 'Date picker popup' })).not.toBeInTheDocument(); }); it('renders right icon and error message', () => { const { container } = render( {}} rightIcon={R} error="Invalid date" inputClassName="custom-input" />, ); const input = container.querySelector('input'); expect(input).toBeInstanceOf(HTMLInputElement); expect(screen.getByTestId('right-icon')).toBeInTheDocument(); expect(input).toHaveClass('pr-10'); expect(input).toHaveClass('custom-input'); expect(screen.getByText('Invalid date')).toBeInTheDocument(); }); it('supports inline layout', () => { const { container } = render( {}} layout="inline" />, ); expect(container.querySelector('label')).toHaveClass('inline-flex'); expect(container.querySelector('label > div')).not.toHaveClass('mt-1'); }); });