import { afterEach, describe, expect, it, vi } from 'vitest'; import { __datePickerTestUtils as utils } from '../../src/components/DatePicker'; type GlobalDescriptor = PropertyDescriptor | undefined; function setGlobalProperty(name: string, descriptor: PropertyDescriptor): GlobalDescriptor { const key = name as keyof typeof globalThis; const original = Object.getOwnPropertyDescriptor(globalThis, key); Object.defineProperty(globalThis, key, descriptor); return original; } function restoreGlobalProperty(name: string, original: GlobalDescriptor): void { const key = name as keyof typeof globalThis; if (original) { Object.defineProperty(globalThis, key, original); return; } Reflect.deleteProperty(globalThis, key); } describe('DatePicker logic helpers', () => { afterEach(() => { vi.restoreAllMocks(); }); it('formats and clamps numeric values', () => { expect(utils.pad2(3)).toBe('03'); expect(utils.pad4(12)).toBe('0012'); expect(utils.clampNumber(99, 0, 50)).toBe(50); expect(utils.clampNumber(-1, 0, 50)).toBe(0); }); it('validates calendar dates strictly', () => { expect(utils.createValidatedDate(2026, 2, 29)).toBeNull(); expect(utils.createValidatedDate(2028, 2, 29)).toBeInstanceOf(Date); }); it('resolves locale with and without navigator', () => { const originalNavigator = setGlobalProperty('navigator', { configurable: true, value: undefined, }); expect(utils.resolveLocale()).toBe('en-US'); restoreGlobalProperty('navigator', originalNavigator); const locale = utils.resolveLocale(); expect(typeof locale).toBe('string'); expect(locale.length).toBeGreaterThan(0); }); it('resolves week start across locale and fallback branches', () => { const originalIntl = setGlobalProperty('Intl', { configurable: true, value: undefined, }); expect(utils.resolveWeekStart('en-US')).toBe(0); setGlobalProperty('Intl', { configurable: true, value: { Locale: class MockLocale { weekInfo = { firstDay: 2 }; }, }, }); expect(utils.resolveWeekStart('de-DE')).toBe(2); setGlobalProperty('Intl', { configurable: true, value: { Locale: class MockLocale { weekInfo = { firstDay: 7 }; }, }, }); expect(utils.resolveWeekStart('en-US')).toBe(0); setGlobalProperty('Intl', { configurable: true, value: { Locale: class MockLocale { weekInfo = { firstDay: 0 }; }, }, }); expect(utils.resolveWeekStart('en-US')).toBe(0); setGlobalProperty('Intl', { configurable: true, value: { Locale: class MockLocale { weekInfo = { firstDay: 'x' as unknown as number }; }, }, }); expect(utils.resolveWeekStart('en-US')).toBe(0); setGlobalProperty('Intl', { configurable: true, value: { Locale: class MockLocale { constructor() { throw new Error('boom'); } }, }, }); expect(utils.resolveWeekStart('en-US')).toBe(0); restoreGlobalProperty('Intl', originalIntl); }); it('builds and validates format configuration', () => { expect(utils.buildFormatConfigOrNull('date-time', '')).toBeNull(); expect(utils.buildFormatConfigOrNull('date-time', 'yyyy/mm/dd HH')).toBeNull(); const config = utils.buildFormatConfigOrNull('date-time', 'dd/mm/yyyy HH:mm'); expect(config).not.toBeNull(); expect(config?.segments).toHaveLength(5); expect(config?.totalLength).toBe(16); }); it('falls back to default format when requested format is invalid', () => { const config = utils.buildFormatConfig('date-time', 'yyyy/mm/dd'); expect(config.format).toBe('yyyy/mm/dd HH:mm'); }); it('parses valid values and rejects invalid input', () => { const dateTimeConfig = utils.buildFormatConfig('date-time', 'dd/mm/yyyy HH:mm'); expect(utils.parsePickerValueWithFormat('22/02/2026 14:30', dateTimeConfig)).toEqual({ date: utils.createDateAtLocalMidnight(2026, 1, 22), hour: 14, minute: 30, }); expect(utils.parsePickerValueWithFormat('2/2/2026 14:30', dateTimeConfig)).toBeNull(); expect(utils.parsePickerValueWithFormat('22-02-2026 14:30', dateTimeConfig)).toBeNull(); expect(utils.parsePickerValueWithFormat('aa/02/2026 14:30', dateTimeConfig)).toBeNull(); expect(utils.parsePickerValueWithFormat('31/02/2026 14:30', dateTimeConfig)).toBeNull(); expect(utils.parsePickerValueWithFormat('22/02/2026 24:30', dateTimeConfig)).toBeNull(); const timeConfig = utils.buildFormatConfig('time', 'HH:mm'); expect(utils.parsePickerValueWithFormat('09:00', timeConfig)).toEqual({ date: utils.startOfDay(new Date()), hour: 9, minute: 0, }); expect(utils.parsePickerValueWithFormat('09:77', timeConfig)).toBeNull(); }); it('formats picker values with the chosen configuration', () => { const config = utils.buildFormatConfig('date-time', 'dd/mm/yyyy HH:mm'); const text = utils.formatPickerValueWithFormat( { date: utils.createDateAtLocalMidnight(2027, 2, 11), hour: 6, minute: 5, }, config, ); expect(text).toBe('11/03/2027 06:05'); }); it('compares values by picker type', () => { const date = utils.comparePickerValue( { date: utils.createDateAtLocalMidnight(2026, 1, 1), hour: 23, minute: 30, }, { date: utils.createDateAtLocalMidnight(2026, 1, 2), hour: 1, minute: 0, }, 'date', ); expect(date).toBeLessThan(0); const time = utils.comparePickerValue( { date: utils.createDateAtLocalMidnight(2026, 1, 1), hour: 9, minute: 0, }, { date: utils.createDateAtLocalMidnight(2026, 1, 1), hour: 8, minute: 59, }, 'time', ); expect(time).toBeGreaterThan(0); const dateTime = utils.comparePickerValue( { date: utils.createDateAtLocalMidnight(2026, 1, 1), hour: 1, minute: 0, }, { date: utils.createDateAtLocalMidnight(2026, 1, 1), hour: 1, minute: 1, }, 'date-time', ); expect(dateTime).toBeLessThan(0); }); it('normalizes and enforces picker ranges', () => { const min = { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 9, minute: 0, }; const max = { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 10, minute: 0, }; const normalized = utils.normalizeRange(max, min, 'date-time'); expect(normalized.minValue).toEqual(min); expect(normalized.maxValue).toEqual(max); const below = utils.clampPickerToRange( { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 8, minute: 0, }, min, max, 'date-time', ); expect(below).toEqual(min); const above = utils.clampPickerToRange( { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 10, minute: 30, }, min, max, 'date-time', ); expect(above).toEqual(max); expect(utils.isWithinRange(min, min, max, 'date-time')).toBe(true); expect( utils.isWithinRange( { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 7, minute: 59, }, min, max, 'date-time', ), ).toBe(false); expect( utils.isWithinRange( { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 10, minute: 1, }, min, max, 'date-time', ), ).toBe(false); }); it('applies segment edits across year, month, day, hour and minute', () => { const base = { date: utils.createDateAtLocalMidnight(2026, 1, 28), hour: 14, minute: 30, }; expect(utils.applySegmentDigits(base, 'year', '0001').date.getFullYear()).toBe(1); expect(utils.applySegmentDigits(base, 'month', '13').date.getMonth()).toBe(11); expect(utils.applySegmentDigits(base, 'day', '99').date.getDate()).toBe(28); expect(utils.applySegmentDigits(base, 'hour', '88').hour).toBe(23); expect(utils.applySegmentDigits(base, 'minute', '99').minute).toBe(59); const unchanged = utils.applySegmentDigits(base, 'day', 'not-a-number'); expect(unchanged).toEqual(base); }); it('resolves segment index from caret position', () => { const config = utils.buildFormatConfig('date-time', 'dd/mm/yyyy HH:mm'); expect(utils.findSegmentIndexByCaret([], 3)).toBe(0); expect(utils.findSegmentIndexByCaret(config.segments, null)).toBe(0); expect(utils.findSegmentIndexByCaret(config.segments, 0)).toBe(0); expect(utils.findSegmentIndexByCaret(config.segments, 6)).toBe(2); expect(utils.findSegmentIndexByCaret(config.segments, 99)).toBe( config.segments[config.segments.length - 1].segmentIndex, ); expect(utils.findSegmentIndexByCaret(config.segments, 2)).toBe(0); const nonStandardSegments = [ { type: 'segment', kind: 'day', token: 'dd', length: 2, start: 5, end: 7, segmentIndex: 0, }, ] as unknown as Parameters[0]; expect(utils.findSegmentIndexByCaret(nonStandardSegments, 1)).toBe(0); }); it('checks date and hour selectability inside constrained ranges', () => { const min = { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 9, minute: 30, }; const max = { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 10, minute: 15, }; expect( utils.isDateSelectableForRange( utils.createDateAtLocalMidnight(2026, 2, 9), 'date-time', min, max, ), ).toBe(false); expect( utils.isDateSelectableForRange( utils.createDateAtLocalMidnight(2026, 2, 10), 'date-time', min, max, ), ).toBe(true); expect( utils.isDateSelectableForRange( utils.createDateAtLocalMidnight(2026, 2, 11), 'date-time', min, max, ), ).toBe(false); expect( utils.isDateSelectableForRange( utils.createDateAtLocalMidnight(2026, 2, 9), 'date', { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 0, minute: 0, }, { date: utils.createDateAtLocalMidnight(2026, 2, 10), hour: 0, minute: 0, }, ), ).toBe(false); expect( utils.isHourSelectableForRange( utils.createDateAtLocalMidnight(2026, 2, 10), 8, 'date-time', min, max, ), ).toBe(false); expect( utils.isHourSelectableForRange( utils.createDateAtLocalMidnight(2026, 2, 10), 9, 'date-time', min, max, ), ).toBe(true); expect( utils.isHourSelectableForRange( utils.createDateAtLocalMidnight(2026, 2, 10), 11, 'date-time', min, max, ), ).toBe(false); }); it('builds month grids and joins class names', () => { const month = utils.createDateAtLocalMidnight(2026, 2, 10); const grid = utils.buildMonthGrid(month, 1); expect(grid).toHaveLength(42); expect(grid[0]).toBeInstanceOf(Date); expect(grid[41]).toBeInstanceOf(Date); expect(utils.joinClassNames('a', false, 'b', undefined, '', null, 'c')).toBe('a b c'); }); it('assigns refs for callback and object refs', () => { const callback = vi.fn(); const objectRef = { current: null as HTMLInputElement | null }; const node = document.createElement('input'); utils.assignRef(callback, node); utils.assignRef(objectRef, node); utils.assignRef(undefined, node); expect(callback).toHaveBeenCalledWith(node); expect(objectRef.current).toBe(node); }); });