import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DatePicker } from './date-picker';
import { DATE_PICKER_CHANGE_EVENT, DATE_PICKER_OPEN_EVENT } from './date-picker.types';
const MARKUP = `
`;
function setup(): HTMLElement {
document.body.innerHTML = MARKUP;
return document.body.firstElementChild as HTMLElement;
}
function days(root: HTMLElement): HTMLButtonElement[] {
return Array.from(root.querySelectorAll('[data-c42-datepicker-day]'));
}
function dayFor(root: HTMLElement, iso: string): HTMLButtonElement {
return root.querySelector(`[data-c42-datepicker-day][data-value="${iso}"]`)!;
}
describe('DatePicker', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('throws when required parts are missing', () => {
document.body.innerHTML = '';
expect(() => new DatePicker(document.body.firstElementChild as HTMLElement)).toThrow(
/\[42\/date-picker\]/,
);
});
it('wires ARIA and hides the panel on init', () => {
const root = setup();
new DatePicker(root);
const trigger = root.querySelector('[data-c42-datepicker-trigger]')!;
const panel = root.querySelector('[data-c42-datepicker-panel]')!;
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog');
expect(trigger.getAttribute('aria-expanded')).toBe('false');
expect(trigger.getAttribute('aria-controls')).toBe(panel.id);
expect(panel.getAttribute('role')).toBe('dialog');
expect(panel.hasAttribute('hidden')).toBe(true);
});
it('renders a 6x7 grid plus a weekday header row', () => {
const root = setup();
new DatePicker(root, { defaultValue: '2024-03-15' });
expect(days(root)).toHaveLength(42);
const headers = root.querySelectorAll('[data-c42-datepicker-weekday]');
expect(headers).toHaveLength(7);
});
it('marks the selected day with aria-selected and data-state', () => {
const root = setup();
new DatePicker(root, { defaultValue: '2024-03-15' });
const selected = dayFor(root, '2024-03-15');
expect(selected.getAttribute('aria-selected')).toBe('true');
expect(selected.dataset.state).toBe('selected');
});
it('selects a day on click and emits change with ISO value', () => {
const root = setup();
const picker = new DatePicker(root, { defaultValue: '2024-03-15', closeOnSelect: false });
const spy = vi.fn();
picker.on(DATE_PICKER_CHANGE_EVENT, spy);
dayFor(root, '2024-03-20').click();
expect(picker.value).toBe('2024-03-20');
expect(spy).toHaveBeenCalledOnce();
expect((spy.mock.calls[0][0] as CustomEvent).detail.value).toBe('2024-03-20');
});
it('navigates months with prev/next buttons', () => {
const root = setup();
new DatePicker(root, { defaultValue: '2024-03-15' });
const label = root.querySelector('[data-c42-datepicker-label]')!;
const initial = label.textContent;
root.querySelector('[data-c42-datepicker-next]')!.click();
expect(label.textContent).not.toBe(initial);
// April should now contain April 1st.
expect(dayFor(root, '2024-04-01')).toBeTruthy();
});
it('disables days outside the min/max range', () => {
const root = setup();
new DatePicker(root, { defaultValue: '2024-03-15', min: '2024-03-10', max: '2024-03-20' });
expect(dayFor(root, '2024-03-05').disabled).toBe(true);
expect(dayFor(root, '2024-03-25').disabled).toBe(true);
expect(dayFor(root, '2024-03-15').disabled).toBe(false);
});
it('does not select a disabled day', () => {
const root = setup();
const picker = new DatePicker(root, { defaultValue: '2024-03-15', max: '2024-03-20' });
dayFor(root, '2024-03-25').click();
expect(picker.value).toBe('2024-03-15');
});
it('moves focus with arrow keys via roving tabindex', () => {
const root = setup();
new DatePicker(root, { defaultValue: '2024-03-15' });
const grid = root.querySelector('[data-c42-datepicker-grid]')!;
grid.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
const focusable = grid.querySelector('[data-c42-datepicker-day][tabindex="0"]')!;
expect(focusable.dataset.value).toBe('2024-03-16');
});
it('selects the focused day on Enter', () => {
const root = setup();
const picker = new DatePicker(root, { defaultValue: '2024-03-15', closeOnSelect: false });
const grid = root.querySelector('[data-c42-datepicker-grid]')!;
grid.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(picker.value).toBe('2024-03-22');
});
it('emits open event and toggles state', () => {
const root = setup();
const picker = new DatePicker(root);
const spy = vi.fn();
picker.on(DATE_PICKER_OPEN_EVENT, spy);
picker.openPanel();
expect(picker.isOpen).toBe(true);
expect(spy).toHaveBeenCalledOnce();
});
it('clears the selection', () => {
const root = setup();
const picker = new DatePicker(root, { defaultValue: '2024-03-15' });
picker.clear();
expect(picker.value).toBeNull();
expect(picker.getState().value).toBeNull();
});
it('accepts a Date instance as defaultValue', () => {
const root = setup();
const picker = new DatePicker(root, { defaultValue: new Date(2024, 5, 1) });
expect(picker.value).toBe('2024-06-01');
});
it('stops reacting after destroy', () => {
const root = setup();
const picker = new DatePicker(root);
picker.destroy();
root.querySelector('[data-c42-datepicker-trigger]')!.click();
expect(picker.isOpen).toBe(false);
});
describe('range mode', () => {
it('selects start on first click and end on second click', () => {
const root = setup();
const picker = new DatePicker(root, { mode: 'range', defaultValue: '2024-03-15', closeOnSelect: false });
const spy = vi.fn();
picker.on(DATE_PICKER_CHANGE_EVENT, spy);
dayFor(root, '2024-03-10').click();
expect(spy).not.toHaveBeenCalled(); // no emit until range complete
expect(dayFor(root, '2024-03-10').dataset.state).toBe('range-start');
dayFor(root, '2024-03-20').click();
expect(spy).toHaveBeenCalledOnce();
expect((spy.mock.calls[0][0] as CustomEvent).detail.range).toEqual({
start: '2024-03-10',
end: '2024-03-20',
});
expect(dayFor(root, '2024-03-20').dataset.state).toBe('range-end');
});
it('marks days between start and end as in-range', () => {
const root = setup();
new DatePicker(root, { mode: 'range', closeOnSelect: false, defaultValue: '2024-03-15' });
dayFor(root, '2024-03-10').click();
dayFor(root, '2024-03-14').click();
expect(dayFor(root, '2024-03-12').dataset.state).toBe('in-range');
});
it('normalizes order when end is before start', () => {
const root = setup();
const picker = new DatePicker(root, { mode: 'range', closeOnSelect: false, defaultValue: '2024-03-15' });
const spy = vi.fn();
picker.on(DATE_PICKER_CHANGE_EVENT, spy);
dayFor(root, '2024-03-20').click();
dayFor(root, '2024-03-10').click();
expect((spy.mock.calls[0][0] as CustomEvent).detail.range).toEqual({
start: '2024-03-10',
end: '2024-03-20',
});
});
it('resets range on third click', () => {
const root = setup();
new DatePicker(root, { mode: 'range', closeOnSelect: false, defaultValue: '2024-03-15' });
dayFor(root, '2024-03-10').click();
dayFor(root, '2024-03-14').click();
// Third click resets
dayFor(root, '2024-03-25').click();
expect(dayFor(root, '2024-03-25').dataset.state).toBe('range-start');
expect(dayFor(root, '2024-03-12').dataset.state).toBe('unselected');
});
});
});