// Datepicker value conversions (Phase 12.2, D-18 to D-21): parse and emit // per mode, ignoreTimezone, display formats, week start and bounds. The // datetime cases run in a fixed non-UTC zone. import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { CalendarDate, CalendarDateTime, Time, ZonedDateTime, resetLocalTimeZone, setLocalTimeZone, } from '@internationalized/date' import { DEFAULT_DISPLAY, boundValue, dateMode, dayBounds, emitFieldValue, formatDisplay, parseFieldValue, startValue, weekStart, } from '../../src/app/dateFormat' beforeEach(() => setLocalTimeZone('Europe/Warsaw')) afterEach(() => resetLocalTimeZone()) describe('dateMode', () => { it('defaults to datetime', () => { expect(dateMode('date')).toBe('date') expect(dateMode('time')).toBe('time') expect(dateMode('datetime')).toBe('datetime') expect(dateMode(undefined)).toBe('datetime') expect(dateMode(null)).toBe('datetime') expect(dateMode('week')).toBe('datetime') }) }) describe('parseFieldValue', () => { it('reads null, empty and unreadable values as null', () => { for (const mode of ['date', 'datetime', 'time'] as const) { expect(parseFieldValue(mode, null)).toBeNull() expect(parseFieldValue(mode, undefined)).toBeNull() expect(parseFieldValue(mode, '')).toBeNull() expect(parseFieldValue(mode, ' ')).toBeNull() expect(parseFieldValue(mode, 42)).toBeNull() expect(parseFieldValue(mode, 'garbage')).toBeNull() } }) it('reads a date as a calendar day, ignoring any time part', () => { expect(parseFieldValue('date', '2026-10-02')?.toString()).toBe('2026-10-02') expect(parseFieldValue('date', '2026-10-02T23:30:00Z')?.toString()).toBe('2026-10-02') expect(parseFieldValue('date', '2026-10-02')).toBeInstanceOf(CalendarDate) }) it('reads a time with or without seconds or a date', () => { expect(parseFieldValue('time', '14:30')?.toString()).toBe('14:30:00') expect(parseFieldValue('time', '14:30:15')?.toString()).toBe('14:30:15') expect(parseFieldValue('time', '2026-10-02T08:05:00Z')?.toString()).toBe('08:05:00') expect(parseFieldValue('time', '14:30')).toBeInstanceOf(Time) }) it('shows a datetime in the local zone, a zone-less one read as UTC', () => { const zoned = parseFieldValue('datetime', '2026-10-02T10:30:00Z') as ZonedDateTime expect(zoned).toBeInstanceOf(ZonedDateTime) expect(zoned.timeZone).toBe('Europe/Warsaw') expect([zoned.hour, zoned.minute]).toEqual([12, 30]) const winter = parseFieldValue('datetime', '2026-01-15 10:30:00') as ZonedDateTime expect([winter.hour, winter.minute]).toEqual([11, 30]) const offset = parseFieldValue('datetime', '2026-10-02T10:30:00+02:00') as ZonedDateTime expect([offset.hour, offset.minute]).toEqual([10, 30]) }) it('keeps the UTC wall clock with ignoreTimezone', () => { const wall = parseFieldValue('datetime', '2026-10-02T10:30:00Z', true) expect(wall).toBeInstanceOf(CalendarDateTime) expect(wall?.toString()).toBe('2026-10-02T10:30:00') }) }) describe('emitFieldValue', () => { it('emits null for no value', () => { expect(emitFieldValue('date', null)).toBeNull() expect(emitFieldValue('datetime', undefined)).toBeNull() expect(emitFieldValue('time', null)).toBeNull() }) it('emits a date as YYYY-MM-DD and a time as HH:MM:SS', () => { expect(emitFieldValue('date', new CalendarDate(26, 1, 2))).toBe('0026-01-02') expect(emitFieldValue('date', new CalendarDateTime(2026, 10, 2, 23, 59))).toBe('2026-10-02') expect(emitFieldValue('time', new Time(7, 5))).toBe('07:05:00') expect(emitFieldValue('time', new CalendarDate(2026, 10, 2))).toBeNull() expect(emitFieldValue('date', new Time(7, 5))).toBeNull() }) it('emits a local datetime as a UTC instant without milliseconds', () => { expect(emitFieldValue('datetime', new CalendarDateTime(2026, 10, 2, 12, 30))).toBe('2026-10-02T10:30:00Z') expect(emitFieldValue('datetime', new CalendarDateTime(2026, 1, 1, 0, 30))).toBe('2025-12-31T23:30:00Z') expect(emitFieldValue('datetime', new CalendarDate(2026, 10, 2))).toBe('2026-10-01T22:00:00Z') const zoned = parseFieldValue('datetime', '2026-10-02T10:30:00Z') expect(emitFieldValue('datetime', zoned)).toBe('2026-10-02T10:30:00Z') }) it('emits the wall clock unchanged with ignoreTimezone', () => { expect(emitFieldValue('datetime', new CalendarDateTime(2026, 10, 2, 12, 30), true)).toBe('2026-10-02T12:30:00Z') const zoned = parseFieldValue('datetime', '2026-10-02T10:30:00Z') as ZonedDateTime expect(emitFieldValue('datetime', zoned, true)).toBe('2026-10-02T12:30:00Z') expect(emitFieldValue('datetime', new CalendarDate(2026, 10, 2), true)).toBe('2026-10-02T00:00:00Z') }) it('round-trips every mode', () => { for (const [mode, text] of [ ['date', '2026-02-28'], ['time', '23:59:59'], ['datetime', '2026-03-29T01:30:00Z'], ] as const) { expect(emitFieldValue(mode, parseFieldValue(mode, text))).toBe(text) } expect(emitFieldValue('datetime', parseFieldValue('datetime', '2026-10-02T10:30:00Z', true), true)).toBe('2026-10-02T10:30:00Z') }) }) describe('formatDisplay', () => { const day = new CalendarDate(2026, 10, 2) const at = new CalendarDateTime(2026, 10, 2, 14, 5, 9) it('uses the mode default without a display format', () => { expect(formatDisplay(day, 'date', undefined, 'en')).toBe('2026-10-02') expect(formatDisplay(at, 'datetime', '', 'en')).toBe('2026-10-02 14:05') expect(formatDisplay(new Time(9, 3), 'time', null, 'en')).toBe('09:03') expect(DEFAULT_DISPLAY).toEqual({ date: 'YYYY-MM-DD', datetime: 'YYYY-MM-DD HH:mm', time: 'HH:mm' }) expect(formatDisplay(null, 'date', 'DD', 'en')).toBe('') }) it('renders every token', () => { expect(formatDisplay(at, 'datetime', 'DD.MM.YYYY HH:mm:ss', 'pl')).toBe('02.10.2026 14:05:09') expect(formatDisplay(at, 'datetime', 'D/M/YY H:mm', 'en')).toBe('2/10/26 14:05') expect(formatDisplay(at, 'datetime', 'hh:mm A', 'en')).toBe('02:05 PM') expect(formatDisplay(new CalendarDateTime(2026, 10, 2, 0, 5), 'datetime', 'h:mm a', 'en')).toBe('12:05 am') expect(formatDisplay(at, 'datetime', 'dddd, D MMMM YYYY', 'en')).toBe('Friday, 2 October 2026') expect(formatDisplay(at, 'datetime', 'ddd D MMM', 'en')).toBe('Fri 2 Oct') expect(formatDisplay(at, 'datetime', 'YYYY-MM-DD [at] HH:mm', 'en')).toBe('2026-10-02 at 14:05') }) it('names months and days in the admin locale, genitive next to a day', () => { expect(formatDisplay(day, 'date', 'D MMMM YYYY', 'pl')).toBe('2 października 2026') expect(formatDisplay(day, 'date', 'MMMM YYYY', 'pl')).toBe('październik 2026') expect(formatDisplay(day, 'date', 'dddd', 'pl')).toBe('piątek') }) it('leaves clock tokens empty on a date and date tokens empty on a time', () => { expect(formatDisplay(day, 'date', 'YYYY-MM-DD HH:mm', 'en')).toBe('2026-10-02 :') expect(formatDisplay(new Time(9, 3), 'time', 'DD HH:mm', 'en')).toBe(' 09:03') }) }) describe('weekStart', () => { it('takes an explicit firstDay', () => { expect(weekStart('pl', 0)).toBe(0) expect(weekStart('en-US', 6)).toBe(6) }) it('falls back to the locale for a missing or invalid firstDay', () => { expect(weekStart('pl')).toBe(1) expect(weekStart('en-US')).toBe(0) expect(weekStart('pl', 7)).toBe(1) expect(weekStart('pl', 1.5)).toBe(1) expect(weekStart('en-US', null)).toBe(0) }) }) describe('dayBounds and boundValue', () => { const now = new CalendarDate(2026, 10, 2) it('prefers minDate and maxDate over yearRange', () => { const bounds = dayBounds({ minDate: '2026-01-01', maxDate: '2026-12-31T00:00:00Z', yearRange: [5] }, now) expect(bounds.min?.toString()).toBe('2026-01-01') expect(bounds.max?.toString()).toBe('2026-12-31') expect(dayBounds({ minDate: 'garbage' }, now)).toEqual({}) }) it('reads both yearRange forms', () => { const around = dayBounds({ yearRange: [10] }, now) expect([around.min?.toString(), around.max?.toString()]).toEqual(['2016-01-01', '2036-12-31']) const span = dayBounds({ yearRange: [2000, 2040] }, now) expect([span.min?.toString(), span.max?.toString()]).toEqual(['2000-01-01', '2040-12-31']) expect(dayBounds({ yearRange: [] }, now)).toEqual({}) }) it('turns a bound day into the start or end of that day per mode', () => { const day = new CalendarDate(2026, 10, 2) expect(boundValue(day, 'date', false, true)?.toString()).toBe('2026-10-02') expect(boundValue(day, 'time', false, true)).toBeUndefined() expect(boundValue(undefined, 'date', false, true)).toBeUndefined() expect(boundValue(day, 'datetime', true, true)?.toString()).toBe('2026-10-02T23:59:59') const zoned = boundValue(day, 'datetime', false, false) as ZonedDateTime expect(zoned.timeZone).toBe('Europe/Warsaw') expect([zoned.hour, zoned.minute]).toEqual([0, 0]) }) it('starts a fresh picker at today in its value type', () => { expect(startValue('date', false)).toBeInstanceOf(CalendarDate) expect(startValue('datetime', true)).toBeInstanceOf(CalendarDateTime) expect(startValue('datetime', false)).toBeInstanceOf(ZonedDateTime) expect(startValue('time', false)).toBeInstanceOf(ZonedDateTime) }) })