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);
});
});