Files
dify/web/app/components/base/date-and-time-picker/utils/dayjs-extended.spec.ts
2026-02-19 10:27:11 +08:00

367 lines
11 KiB
TypeScript

import dayjs, {
clearMonthMapCache,
cloneTime,
formatDateForOutput,
getDateWithTimezone,
getDaysInMonth,
getHourIn12Hour,
parseDateWithFormat,
toDayjs,
} from './dayjs'
describe('dayjs extended utilities', () => {
// Tests for cloneTime
describe('cloneTime', () => {
it('should copy hour and minute from source to target', () => {
const target = dayjs('2024-01-15')
const source = dayjs('2024-06-20 14:30')
const result = cloneTime(target, source)
expect(result.hour()).toBe(14)
expect(result.minute()).toBe(30)
expect(result.date()).toBe(15)
expect(result.month()).toBe(0) // January
})
it('should not mutate the original target date', () => {
const target = dayjs('2024-01-15 08:00')
const source = dayjs('2024-06-20 14:30')
cloneTime(target, source)
expect(target.hour()).toBe(8)
expect(target.minute()).toBe(0)
})
})
// Tests for getDaysInMonth
describe('getDaysInMonth', () => {
beforeEach(() => {
clearMonthMapCache()
})
it('should return an array of Day objects', () => {
const date = dayjs('2024-06-15')
const days = getDaysInMonth(date)
expect(days.length).toBeGreaterThan(0)
days.forEach((day) => {
expect(day).toHaveProperty('date')
expect(day).toHaveProperty('isCurrentMonth')
})
})
it('should include days from previous and next month to fill the grid', () => {
const date = dayjs('2024-06-15') // June 2024 starts on Saturday
const days = getDaysInMonth(date)
const prevMonthDays = days.filter(d => !d.isCurrentMonth && d.date.month() < date.month())
const nextMonthDays = days.filter(d => !d.isCurrentMonth && d.date.month() > date.month())
// June 2024 starts on Saturday (6), so there are 6 days from previous month
expect(prevMonthDays.length).toBeGreaterThan(0)
expect(nextMonthDays.length).toBeGreaterThan(0)
})
it('should mark current month days correctly', () => {
const date = dayjs('2024-06-15')
const days = getDaysInMonth(date)
const currentMonthDays = days.filter(d => d.isCurrentMonth)
// June has 30 days
expect(currentMonthDays).toHaveLength(30)
})
it('should cache results for the same month', () => {
const date1 = dayjs('2024-06-15')
const date2 = dayjs('2024-06-20')
const days1 = getDaysInMonth(date1)
const days2 = getDaysInMonth(date2)
// Same reference since it's cached
expect(days1).toBe(days2)
})
it('should return different results for different months', () => {
const june = dayjs('2024-06-15')
const july = dayjs('2024-07-15')
const juneDays = getDaysInMonth(june)
const julyDays = getDaysInMonth(july)
expect(juneDays).not.toBe(julyDays)
})
})
// Tests for clearMonthMapCache
describe('clearMonthMapCache', () => {
it('should clear the cache so new days are generated', () => {
const date = dayjs('2024-06-15')
const days1 = getDaysInMonth(date)
clearMonthMapCache()
const days2 = getDaysInMonth(date)
// After clearing cache, a new array should be created
expect(days1).not.toBe(days2)
// But should have the same length
expect(days1.length).toBe(days2.length)
})
})
// Tests for getHourIn12Hour
describe('getHourIn12Hour', () => {
it('should return 12 for midnight (hour 0)', () => {
const date = dayjs('2024-01-01 00:00')
expect(getHourIn12Hour(date)).toBe(12)
})
it('should return hour as-is for 1-11 AM', () => {
expect(getHourIn12Hour(dayjs('2024-01-01 01:00'))).toBe(1)
expect(getHourIn12Hour(dayjs('2024-01-01 11:00'))).toBe(11)
})
it('should return 0 for noon (hour 12)', () => {
const date = dayjs('2024-01-01 12:00')
expect(getHourIn12Hour(date)).toBe(0)
})
it('should return hour - 12 for PM hours (13-23)', () => {
expect(getHourIn12Hour(dayjs('2024-01-01 13:00'))).toBe(1)
expect(getHourIn12Hour(dayjs('2024-01-01 23:00'))).toBe(11)
})
})
// Tests for getDateWithTimezone
describe('getDateWithTimezone', () => {
it('should return a cloned date when no timezone is provided', () => {
const date = dayjs('2024-06-15')
const result = getDateWithTimezone({ date })
expect(result.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should return current date clone when neither date nor timezone is provided', () => {
const result = getDateWithTimezone({})
const now = dayjs()
expect(result.format('YYYY-MM-DD')).toBe(now.format('YYYY-MM-DD'))
})
it('should apply timezone to provided date', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = getDateWithTimezone({ date, timezone: 'America/New_York' })
// dayjs.tz converts the date to the given timezone
expect(result).toBeDefined()
expect(result.isValid()).toBe(true)
})
it('should return current time in timezone when only timezone is provided', () => {
const result = getDateWithTimezone({ timezone: 'Asia/Tokyo' })
expect(result.utcOffset()).toBe(dayjs().tz('Asia/Tokyo').utcOffset())
})
})
// Tests for toDayjs additional edge cases
describe('toDayjs edge cases', () => {
it('should return undefined for empty string', () => {
expect(toDayjs('')).toBeUndefined()
})
it('should return undefined for undefined', () => {
expect(toDayjs(undefined)).toBeUndefined()
})
it('should handle Dayjs object input', () => {
const date = dayjs('2024-06-15')
const result = toDayjs(date)
expect(result).toBeDefined()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should handle Dayjs object with timezone', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = toDayjs(date, { timezone: 'UTC' })
expect(result).toBeDefined()
})
it('should parse with custom format when format matches common formats', () => {
// Uses a format from COMMON_PARSE_FORMATS
const result = toDayjs('2024-06-15', { format: 'YYYY-MM-DD' })
expect(result).toBeDefined()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should fall back when custom format does not match', () => {
// dayjs strict mode with format requires customParseFormat plugin
// which is not loaded, so invalid format falls through to other parsing
const result = toDayjs('2024-06-15', { format: 'INVALID', timezone: 'UTC' })
// It will still be parsed by fallback mechanisms
expect(result).toBeDefined()
})
it('should parse time with seconds', () => {
const result = toDayjs('14:30:45', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(14)
expect(result?.minute()).toBe(30)
expect(result?.second()).toBe(45)
})
it('should parse time with milliseconds', () => {
const result = toDayjs('14:30:45.123', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.millisecond()).toBe(123)
})
it('should normalize short milliseconds by padding', () => {
const result = toDayjs('14:30:45.1', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.millisecond()).toBe(100)
})
it('should truncate long milliseconds to 3 digits', () => {
// The time regex only captures up to 3 digits for ms, so 4+ digit values
// don't match the regex and fall through to common format parsing
const result = toDayjs('14:30:45.12', { timezone: 'UTC' })
expect(result).toBeDefined()
// 2-digit ms "12" gets padded to "120"
expect(result?.millisecond()).toBe(120)
})
it('should parse 12-hour AM time', () => {
const result = toDayjs('07:15 AM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(7)
expect(result?.minute()).toBe(15)
})
it('should parse 12-hour time with seconds', () => {
const result = toDayjs('07:15:30 PM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(19)
expect(result?.second()).toBe(30)
})
it('should handle 12 PM correctly', () => {
const result = toDayjs('12:00 PM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(12)
})
it('should handle 12 AM correctly', () => {
const result = toDayjs('12:00 AM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(0)
})
it('should use custom formats array when provided', () => {
const result = toDayjs('2024.06.15', { formats: ['YYYY.MM.DD'] })
expect(result).toBeDefined()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should fall back to native parsing for ISO strings', () => {
const result = toDayjs('2024-06-15T12:00:00.000Z')
expect(result).toBeDefined()
})
it('should return undefined for completely unparseable value', () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = toDayjs('not-a-date')
expect(result).toBeUndefined()
consoleSpy.mockRestore()
})
})
// Tests for parseDateWithFormat
describe('parseDateWithFormat', () => {
it('should return null for empty string', () => {
expect(parseDateWithFormat('')).toBeNull()
})
it('should parse with provided format from common formats', () => {
// Uses YYYY-MM-DD which is in COMMON_PARSE_FORMATS
const result = parseDateWithFormat('2024-06-15', 'YYYY-MM-DD')
expect(result).not.toBeNull()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should return null for invalid date with format', () => {
const result = parseDateWithFormat('not-a-date', 'YYYY-MM-DD')
expect(result).toBeNull()
})
it('should try common formats when no format is specified', () => {
const result = parseDateWithFormat('2024-06-15')
expect(result).not.toBeNull()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should parse ISO datetime format', () => {
const result = parseDateWithFormat('2024-06-15T12:00:00')
expect(result).not.toBeNull()
})
it('should return null for unparseable string without format', () => {
const result = parseDateWithFormat('gibberish')
expect(result).toBeNull()
})
})
// Tests for formatDateForOutput
describe('formatDateForOutput', () => {
it('should return empty string for invalid date', () => {
const invalidDate = dayjs('invalid')
expect(formatDateForOutput(invalidDate)).toBe('')
})
it('should format date-only output without time', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = formatDateForOutput(date)
expect(result).toBe('2024-06-15')
})
it('should format with time when includeTime is true', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = formatDateForOutput(date, true)
expect(result).toContain('2024-06-15')
expect(result).toContain('12:00:00')
})
it('should default to date-only format', () => {
const date = dayjs('2024-06-15T14:30:00')
const result = formatDateForOutput(date)
expect(result).toBe('2024-06-15')
expect(result).not.toContain('14:30')
})
})
})