import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TimePicker } from './time-picker'; import { TIME_PICKER_CHANGE_EVENT, TIME_PICKER_OPEN_EVENT } from './time-picker.types'; const MARKUP = `
`; function setup(): HTMLElement { document.body.innerHTML = MARKUP; return document.body.firstElementChild as HTMLElement; } function part(root: HTMLElement, sel: string): T { return root.querySelector(sel)!; } function type(input: HTMLInputElement, value: string): void { input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); } describe('TimePicker', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('throws when required parts are missing', () => { document.body.innerHTML = '
'; expect(() => new TimePicker(document.body.firstElementChild as HTMLElement)).toThrow( /\[42\/time-picker\]/, ); }); it('wires ARIA and hides the panel on init', () => { const root = setup(); new TimePicker(root); const trigger = part(root, '[data-c42-timepicker-trigger]'); const panel = part(root, '[data-c42-timepicker-panel]'); expect(trigger.getAttribute('aria-haspopup')).toBe('dialog'); expect(trigger.getAttribute('aria-controls')).toBe(panel.id); expect(panel.hasAttribute('hidden')).toBe(true); }); it('parses a 24h defaultValue and exposes it', () => { const root = setup(); const picker = new TimePicker(root, { defaultValue: '14:30' }); expect(picker.value).toBe('14:30'); }); it('formats the trigger label in 12h by default', () => { const root = setup(); new TimePicker(root, { defaultValue: '14:30' }); expect(part(root, '[data-c42-timepicker-value]').textContent).toBe('2:30 PM'); }); it('formats the trigger label in 24h mode and hides the period button', () => { const root = setup(); new TimePicker(root, { defaultValue: '14:30', format: '24h' }); expect(part(root, '[data-c42-timepicker-value]').textContent).toBe('14:30'); expect(part(root, '[data-c42-timepicker-period]').hasAttribute('hidden')).toBe(true); }); it('confirms a 12h time with AM/PM into a 24h value', () => { const root = setup(); const picker = new TimePicker(root, { defaultValue: '01:30', closeOnConfirm: false }); const spy = vi.fn(); picker.on(TIME_PICKER_CHANGE_EVENT, spy); picker.openPanel(); // period starts AM (from 01:30) type(part(root, '[data-c42-timepicker-hour]'), '7'); type(part(root, '[data-c42-timepicker-minute]'), '45'); part(root, '[data-c42-timepicker-period]').click(); // AM -> PM part(root, '[data-c42-timepicker-confirm]').click(); expect(picker.value).toBe('19:45'); expect((spy.mock.calls[0][0] as CustomEvent).detail.value).toBe('19:45'); }); it('maps 12 AM to 00 and 12 PM to 12', () => { const am = setup(); const amPicker = new TimePicker(am, { defaultValue: '01:00', closeOnConfirm: false }); amPicker.openPanel(); // AM type(part(am, '[data-c42-timepicker-hour]'), '12'); type(part(am, '[data-c42-timepicker-minute]'), '00'); part(am, '[data-c42-timepicker-confirm]').click(); expect(amPicker.value).toBe('00:00'); const pm = setup(); const pmPicker = new TimePicker(pm, { defaultValue: '13:00', closeOnConfirm: false }); pmPicker.openPanel(); // PM type(part(pm, '[data-c42-timepicker-hour]'), '12'); type(part(pm, '[data-c42-timepicker-minute]'), '00'); part(pm, '[data-c42-timepicker-confirm]').click(); expect(pmPicker.value).toBe('12:00'); }); it('sanitizes out-of-range hour input', () => { const root = setup(); new TimePicker(root, { format: '24h' }); const hour = part(root, '[data-c42-timepicker-hour]'); type(hour, '99'); expect(hour.value).toBe('23'); }); it('clamps minutes to the configured step on confirm', () => { const root = setup(); const picker = new TimePicker(root, { format: '24h', minuteStep: 15, closeOnConfirm: false }); picker.openPanel(); type(part(root, '[data-c42-timepicker-hour]'), '9'); type(part(root, '[data-c42-timepicker-minute]'), '50'); part(root, '[data-c42-timepicker-confirm]').click(); expect(picker.value).toBe('09:45'); }); it('cancel closes without committing', () => { const root = setup(); const picker = new TimePicker(root, { defaultValue: '08:00' }); picker.openPanel(); type(part(root, '[data-c42-timepicker-hour]'), '11'); part(root, '[data-c42-timepicker-cancel]').click(); expect(picker.value).toBe('08:00'); expect(picker.isOpen).toBe(false); }); it('emits open event and toggles state', () => { const root = setup(); const picker = new TimePicker(root); const spy = vi.fn(); picker.on(TIME_PICKER_OPEN_EVENT, spy); part(root, '[data-c42-timepicker-trigger]').click(); expect(picker.isOpen).toBe(true); expect(spy).toHaveBeenCalledOnce(); }); it('clears the selection', () => { const root = setup(); const picker = new TimePicker(root, { defaultValue: '08:00' }); picker.clear(); expect(picker.value).toBeNull(); expect(picker.getState().value).toBeNull(); }); it('stops reacting after destroy', () => { const root = setup(); const picker = new TimePicker(root); picker.destroy(); part(root, '[data-c42-timepicker-trigger]').click(); expect(picker.isOpen).toBe(false); }); });