Spaces:
Sleeping
Sleeping
File size: 9,776 Bytes
cd99321 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | import { render, screen, fireEvent, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import CustomTimePicker from './CustomTimePicker';
import { useSettingsStore } from '../../store/settingsStore';
import { seedStore, resetAllStores } from '../../../tests/helpers/store';
import { buildSettings } from '../../../tests/helpers/factories';
describe('CustomTimePicker', () => {
const onChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
resetAllStores();
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '24h' }) });
});
it('FE-COMP-TIMEPICKER-001: renders without crashing', () => {
render(<CustomTimePicker value="" onChange={onChange} />);
expect(document.body).toBeTruthy();
});
it('FE-COMP-TIMEPICKER-002: shows value in text input in 24h format', () => {
render(<CustomTimePicker value="14:30" onChange={onChange} />);
const input = screen.getByRole('textbox');
expect(input).toHaveProperty('value', '14:30');
});
it('FE-COMP-TIMEPICKER-003: shows value in 12h format', () => {
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '12h' }) });
render(<CustomTimePicker value="14:30" onChange={onChange} />);
const input = screen.getByRole('textbox');
expect(input).toHaveProperty('value', '2:30 PM');
});
it('FE-COMP-TIMEPICKER-004: shows raw value while focused', async () => {
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '12h' }) });
render(<CustomTimePicker value="14:30" onChange={onChange} />);
const input = screen.getByRole('textbox');
await userEvent.setup().click(input);
expect(input).toHaveProperty('value', '14:30');
});
it('FE-COMP-TIMEPICKER-005: clicking clock icon opens dropdown', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="10:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
// Dropdown should show hour and minute display boxes with "10" and "00"
expect(screen.getByText('10')).toBeTruthy();
expect(screen.getByText('00')).toBeTruthy();
});
it('FE-COMP-TIMEPICKER-006: hour increment button increases hour', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="10:00" onChange={onChange} />);
// Open dropdown
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
// The first empty button inside the dropdown is the hour up chevron
const chevrons = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
// chevrons[0] is the clock icon, chevrons after that are up/down for hour, up/down for minute
await user.click(chevrons[1]); // hour up
expect(onChange).toHaveBeenCalledWith('11:00');
});
it('FE-COMP-TIMEPICKER-007: hour decrement button decreases hour', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="10:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
const chevrons = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
await user.click(chevrons[2]); // hour down
expect(onChange).toHaveBeenCalledWith('09:00');
});
it('FE-COMP-TIMEPICKER-008: minute increment steps by 5', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="10:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
const chevrons = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
await user.click(chevrons[3]); // minute up
expect(onChange).toHaveBeenCalledWith('10:05');
});
it('FE-COMP-TIMEPICKER-009: minute increment wraps and carries hour', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="10:55" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
const chevrons = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
await user.click(chevrons[3]); // minute up
expect(onChange).toHaveBeenCalledWith('11:00');
});
it('FE-COMP-TIMEPICKER-010: hour wraps at 23→0', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="23:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
const chevrons = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
await user.click(chevrons[1]); // hour up
expect(onChange).toHaveBeenCalledWith('00:00');
});
it('FE-COMP-TIMEPICKER-011: clear button calls onChange with empty string', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="10:30" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
const clearBtn = screen.getByText('✕');
await user.click(clearBtn);
expect(onChange).toHaveBeenCalledWith('');
});
it('FE-COMP-TIMEPICKER-012: clear button absent when no value', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
expect(screen.queryByText('✕')).toBeNull();
});
it('FE-COMP-TIMEPICKER-013: AM/PM toggle shown in 12h mode', async () => {
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '12h' }) });
const user = userEvent.setup();
render(<CustomTimePicker value="14:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
expect(screen.getByText('PM')).toBeTruthy();
});
it('FE-COMP-TIMEPICKER-014: AM/PM toggle hidden in 24h mode', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="14:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
expect(screen.queryByText('AM')).toBeNull();
expect(screen.queryByText('PM')).toBeNull();
});
it('FE-COMP-TIMEPICKER-015: AM/PM toggle switches hour', async () => {
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '12h' }) });
const user = userEvent.setup();
render(<CustomTimePicker value="14:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
// In 12h mode with value "14:00", there are AM/PM chevrons after hour and minute chevrons
const chevrons = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
// chevrons: [0]=clock, [1]=hour up, [2]=hour down, [3]=min up, [4]=min down, [5]=ampm up, [6]=ampm down
await user.click(chevrons[5]); // AM/PM toggle
expect(onChange).toHaveBeenCalledWith('02:00');
});
it('FE-COMP-TIMEPICKER-016: blur normalizes HH:MM input', () => {
// "9:05" matches /^\d{1,2}:\d{2}$/ and normalizes the hour to zero-padded
render(<CustomTimePicker value="9:05" onChange={onChange} />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.blur(input);
expect(onChange).toHaveBeenCalledWith('09:05');
});
it('FE-COMP-TIMEPICKER-017: blur normalizes 4-digit HHMM input', () => {
render(<CustomTimePicker value="1430" onChange={onChange} />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.blur(input);
expect(onChange).toHaveBeenCalledWith('14:30');
});
it('FE-COMP-TIMEPICKER-018: blur normalizes bare hour', () => {
render(<CustomTimePicker value="8" onChange={onChange} />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.blur(input);
expect(onChange).toHaveBeenCalledWith('08:00');
});
it('FE-COMP-TIMEPICKER-019: blur normalizes 12h string "5:30 PM"', () => {
seedStore(useSettingsStore, { settings: buildSettings({ time_format: '12h' }) });
render(<CustomTimePicker value="5:30 PM" onChange={onChange} />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.blur(input);
expect(onChange).toHaveBeenCalledWith('17:30');
});
it('FE-COMP-TIMEPICKER-020: clicking outside dropdown closes it', async () => {
const user = userEvent.setup();
render(<CustomTimePicker value="10:00" onChange={onChange} />);
const clockBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '');
await user.click(clockBtn!);
// Verify dropdown is open
expect(screen.getByText('10')).toBeTruthy();
// Click outside
const outsideEl = document.createElement('div');
document.body.appendChild(outsideEl);
await act(async () => {
fireEvent.mouseDown(outsideEl);
});
document.body.removeChild(outsideEl);
// Hour display should be gone (only visible in dropdown)
const allText = Array.from(document.querySelectorAll('div')).map(d => d.textContent);
// The "10" in the dropdown display box should no longer be rendered as a standalone element
expect(screen.queryByText('✕')).toBeNull(); // clear button gone = dropdown closed
});
});
|