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