mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
test: improve coverage parameters for some files in base (#33207)
This commit is contained in:
@ -1,112 +1,353 @@
|
||||
import dayjs, {
|
||||
import dayjs from 'dayjs'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import {
|
||||
clearMonthMapCache,
|
||||
cloneTime,
|
||||
convertTimezoneToOffsetStr,
|
||||
formatDateForOutput,
|
||||
getDateWithTimezone,
|
||||
getDaysInMonth,
|
||||
getHourIn12Hour,
|
||||
isDayjsObject,
|
||||
parseDateWithFormat,
|
||||
toDayjs,
|
||||
} from '../dayjs'
|
||||
|
||||
describe('dayjs utilities', () => {
|
||||
const timezone = 'UTC'
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
it('toDayjs parses time-only strings with timezone support', () => {
|
||||
const result = toDayjs('18:45', { timezone })
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('HH:mm')).toBe('18:45')
|
||||
expect(result?.utcOffset()).toBe(getDateWithTimezone({ timezone }).utcOffset())
|
||||
// ── cloneTime ──────────────────────────────────────────────────────────────
|
||||
describe('cloneTime', () => {
|
||||
it('copies hour and minute from source to target, preserving target date', () => {
|
||||
const target = dayjs('2024-03-15')
|
||||
const source = dayjs('2020-01-01T09:30:00')
|
||||
const result = cloneTime(target, source)
|
||||
expect(result.format('YYYY-MM-DD')).toBe('2024-03-15')
|
||||
expect(result.hour()).toBe(9)
|
||||
expect(result.minute()).toBe(30)
|
||||
})
|
||||
})
|
||||
|
||||
// ── getDaysInMonth ─────────────────────────────────────────────────────────
|
||||
describe('getDaysInMonth', () => {
|
||||
beforeEach(() => clearMonthMapCache())
|
||||
|
||||
it('returns cells for a typical month view', () => {
|
||||
const date = dayjs('2024-01-01')
|
||||
const days = getDaysInMonth(date)
|
||||
expect(days.length).toBeGreaterThanOrEqual(28)
|
||||
expect(days.some(d => d.isCurrentMonth)).toBe(true)
|
||||
expect(days.some(d => !d.isCurrentMonth)).toBe(true)
|
||||
})
|
||||
|
||||
it('toDayjs parses 12-hour time strings', () => {
|
||||
const tz = 'America/New_York'
|
||||
const result = toDayjs('07:15 PM', { timezone: tz })
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('HH:mm')).toBe('19:15')
|
||||
expect(result?.utcOffset()).toBe(getDateWithTimezone({ timezone: tz }).startOf('day').utcOffset())
|
||||
it('returns cached result on second call', () => {
|
||||
const date = dayjs('2024-02-01')
|
||||
const first = getDaysInMonth(date)
|
||||
const second = getDaysInMonth(date)
|
||||
expect(first).toBe(second) // same reference
|
||||
})
|
||||
|
||||
it('isDayjsObject detects dayjs instances', () => {
|
||||
const date = dayjs()
|
||||
expect(isDayjsObject(date)).toBe(true)
|
||||
expect(isDayjsObject(getDateWithTimezone({ timezone }))).toBe(true)
|
||||
it('clears cache properly', () => {
|
||||
const date = dayjs('2024-03-01')
|
||||
const first = getDaysInMonth(date)
|
||||
clearMonthMapCache()
|
||||
const second = getDaysInMonth(date)
|
||||
expect(first).not.toBe(second) // different reference after clearing
|
||||
})
|
||||
})
|
||||
|
||||
// ── getHourIn12Hour ─────────────────────────────────────────────────────────
|
||||
describe('getHourIn12Hour', () => {
|
||||
it('returns 12 for midnight (hour=0)', () => {
|
||||
expect(getHourIn12Hour(dayjs().set('hour', 0))).toBe(12)
|
||||
})
|
||||
|
||||
it('returns hour-12 for hours >= 12', () => {
|
||||
expect(getHourIn12Hour(dayjs().set('hour', 12))).toBe(0)
|
||||
expect(getHourIn12Hour(dayjs().set('hour', 15))).toBe(3)
|
||||
expect(getHourIn12Hour(dayjs().set('hour', 23))).toBe(11)
|
||||
})
|
||||
|
||||
it('returns hour as-is for AM hours (1-11)', () => {
|
||||
expect(getHourIn12Hour(dayjs().set('hour', 1))).toBe(1)
|
||||
expect(getHourIn12Hour(dayjs().set('hour', 11))).toBe(11)
|
||||
})
|
||||
})
|
||||
|
||||
// ── getDateWithTimezone ─────────────────────────────────────────────────────
|
||||
describe('getDateWithTimezone', () => {
|
||||
it('returns a clone of now when neither date nor timezone given', () => {
|
||||
const result = getDateWithTimezone({})
|
||||
expect(dayjs.isDayjs(result)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns current tz date when only timezone given', () => {
|
||||
const result = getDateWithTimezone({ timezone: 'UTC' })
|
||||
expect(dayjs.isDayjs(result)).toBe(true)
|
||||
expect(result.utcOffset()).toBe(0)
|
||||
})
|
||||
|
||||
it('returns date in given timezone when both date and timezone given', () => {
|
||||
const date = dayjs.utc('2024-06-01T12:00:00Z')
|
||||
const result = getDateWithTimezone({ date, timezone: 'UTC' })
|
||||
expect(result.hour()).toBe(12)
|
||||
})
|
||||
|
||||
it('returns clone of given date when no timezone given', () => {
|
||||
const date = dayjs('2024-01-15T08:30:00')
|
||||
const result = getDateWithTimezone({ date })
|
||||
expect(result.isSame(date)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ── isDayjsObject ───────────────────────────────────────────────────────────
|
||||
describe('isDayjsObject', () => {
|
||||
it('detects dayjs instances', () => {
|
||||
expect(isDayjsObject(dayjs())).toBe(true)
|
||||
expect(isDayjsObject(getDateWithTimezone({ timezone: 'UTC' }))).toBe(true)
|
||||
expect(isDayjsObject('2024-01-01')).toBe(false)
|
||||
expect(isDayjsObject({})).toBe(false)
|
||||
expect(isDayjsObject(null)).toBe(false)
|
||||
expect(isDayjsObject(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ── toDayjs ────────────────────────────────────────────────────────────────
|
||||
describe('toDayjs', () => {
|
||||
const tz = 'UTC'
|
||||
|
||||
it('returns undefined for undefined value', () => {
|
||||
expect(toDayjs(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('toDayjs parses datetime strings in target timezone', () => {
|
||||
const value = '2024-05-01 12:00:00'
|
||||
const tz = 'America/New_York'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.hour()).toBe(12)
|
||||
expect(result?.format('YYYY-MM-DD HH:mm')).toBe('2024-05-01 12:00')
|
||||
it('returns undefined for empty string', () => {
|
||||
expect(toDayjs('')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('toDayjs parses ISO datetime strings in target timezone', () => {
|
||||
const value = '2024-05-01T14:30:00'
|
||||
const tz = 'Europe/London'
|
||||
it('applies timezone to an existing Dayjs object', () => {
|
||||
const date = dayjs('2024-06-01T12:00:00')
|
||||
const result = toDayjs(date, { timezone: 'UTC' })
|
||||
expect(dayjs.isDayjs(result)).toBe(true)
|
||||
})
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
it('returns the Dayjs object as-is when no timezone given', () => {
|
||||
const date = dayjs('2024-06-01')
|
||||
const result = toDayjs(date)
|
||||
expect(dayjs.isDayjs(result)).toBe(true)
|
||||
})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.hour()).toBe(14)
|
||||
it('returns undefined for non-string non-Dayjs value', () => {
|
||||
// @ts-expect-error testing invalid input
|
||||
expect(toDayjs(12345)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('parses 24h time-only strings', () => {
|
||||
const result = toDayjs('18:45', { timezone: tz })
|
||||
expect(result?.format('HH:mm')).toBe('18:45')
|
||||
})
|
||||
|
||||
it('parses time-only strings with seconds', () => {
|
||||
const result = toDayjs('09:30:45', { timezone: tz })
|
||||
expect(result?.hour()).toBe(9)
|
||||
expect(result?.minute()).toBe(30)
|
||||
expect(result?.second()).toBe(45)
|
||||
})
|
||||
|
||||
it('toDayjs handles dates without time component', () => {
|
||||
const value = '2024-05-01'
|
||||
const tz = 'America/Los_Angeles'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
it('parses time-only strings with 3-digit milliseconds', () => {
|
||||
const result = toDayjs('08:00:00.500', { timezone: tz })
|
||||
expect(result?.millisecond()).toBe(500)
|
||||
})
|
||||
|
||||
it('parses time-only strings with 3-digit ms - normalizeMillisecond exact branch', () => {
|
||||
// normalizeMillisecond: length === 3 → Number('567') = 567
|
||||
const result = toDayjs('08:00:00.567', { timezone: tz })
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.hour()).toBe(8)
|
||||
expect(result?.second()).toBe(0)
|
||||
})
|
||||
|
||||
it('parses time-only strings with <3-digit milliseconds (pads)', () => {
|
||||
const result = toDayjs('08:00:00.5', { timezone: tz })
|
||||
expect(result?.millisecond()).toBe(500)
|
||||
})
|
||||
|
||||
it('parses 12-hour time strings (PM)', () => {
|
||||
const result = toDayjs('07:15 PM', { timezone: 'America/New_York' })
|
||||
expect(result?.format('HH:mm')).toBe('19:15')
|
||||
})
|
||||
|
||||
it('parses 12-hour time strings (AM)', () => {
|
||||
const result = toDayjs('12:00 AM', { timezone: tz })
|
||||
expect(result?.hour()).toBe(0)
|
||||
})
|
||||
|
||||
it('parses 12-hour time strings with seconds', () => {
|
||||
const result = toDayjs('03:30:15 PM', { timezone: tz })
|
||||
expect(result?.hour()).toBe(15)
|
||||
expect(result?.second()).toBe(15)
|
||||
})
|
||||
|
||||
it('parses datetime strings via common formats', () => {
|
||||
const result = toDayjs('2024-05-01 12:00:00', { timezone: tz })
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('parses ISO datetime strings', () => {
|
||||
const result = toDayjs('2024-05-01T14:30:00', { timezone: 'Europe/London' })
|
||||
expect(result?.hour()).toBe(14)
|
||||
})
|
||||
|
||||
it('parses dates with an explicit format option', () => {
|
||||
// Use unambiguous format: YYYY/MM/DD + value 2024/05/01
|
||||
const result = toDayjs('2024/05/01', { format: 'YYYY/MM/DD', timezone: tz })
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('falls through to other formats when explicit format fails', () => {
|
||||
// '2024-05-01' doesn't match 'DD/MM/YYYY' but will match common formats
|
||||
const result = toDayjs('2024-05-01', { format: 'DD/MM/YYYY', timezone: tz })
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('falls through to common formats when explicit format fails without timezone', () => {
|
||||
const result = toDayjs('2024-05-01', { format: 'DD/MM/YYYY' })
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('returns undefined when explicit format parsing fails and no fallback matches', () => {
|
||||
const result = toDayjs('not-a-date-value', { format: 'YYYY/MM/DD' })
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses custom formats array', () => {
|
||||
const result = toDayjs('2024/05/01', { formats: ['YYYY/MM/DD'] })
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('returns undefined for completely invalid string', () => {
|
||||
const result = toDayjs('not-a-valid-date-at-all!!!')
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('parses date-only strings without time', () => {
|
||||
const result = toDayjs('2024-05-01', { timezone: 'America/Los_Angeles' })
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
expect(result?.hour()).toBe(0)
|
||||
expect(result?.minute()).toBe(0)
|
||||
})
|
||||
|
||||
it('uses timezone fallback parser for non-standard datetime strings', () => {
|
||||
const result = toDayjs('May 1, 2024 2:30 PM', { timezone: 'America/New_York' })
|
||||
expect(result?.isValid()).toBe(true)
|
||||
expect(result?.year()).toBe(2024)
|
||||
expect(result?.month()).toBe(4)
|
||||
expect(result?.date()).toBe(1)
|
||||
expect(result?.utcOffset()).toBe(dayjs.tz('2024-05-01', 'America/New_York').utcOffset())
|
||||
})
|
||||
|
||||
it('uses timezone fallback parser when custom formats are empty', () => {
|
||||
const result = toDayjs('2024-05-01T14:30:00Z', {
|
||||
timezone: 'America/New_York',
|
||||
formats: [],
|
||||
})
|
||||
expect(result?.isValid()).toBe(true)
|
||||
expect(result?.utcOffset()).toBe(dayjs.tz('2024-05-01', 'America/New_York').utcOffset())
|
||||
})
|
||||
})
|
||||
|
||||
// ── parseDateWithFormat ────────────────────────────────────────────────────
|
||||
describe('parseDateWithFormat', () => {
|
||||
it('returns null for empty string', () => {
|
||||
expect(parseDateWithFormat('')).toBeNull()
|
||||
})
|
||||
|
||||
it('parses with explicit format', () => {
|
||||
// Use YYYY/MM/DD which is unambiguous
|
||||
const result = parseDateWithFormat('2024/05/01', 'YYYY/MM/DD')
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('returns null for invalid string with explicit format', () => {
|
||||
expect(parseDateWithFormat('not-a-date', 'YYYY-MM-DD')).toBeNull()
|
||||
})
|
||||
|
||||
it('parses using common formats (YYYY-MM-DD)', () => {
|
||||
const result = parseDateWithFormat('2024-05-01')
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('parses using common formats (YYYY/MM/DD)', () => {
|
||||
const result = parseDateWithFormat('2024/05/01')
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('parses ISO datetime strings via common formats', () => {
|
||||
const result = parseDateWithFormat('2024-05-01T14:30:00')
|
||||
expect(result?.hour()).toBe(14)
|
||||
})
|
||||
|
||||
it('returns null for completely unparseable string', () => {
|
||||
expect(parseDateWithFormat('ZZZZ-ZZ-ZZ')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ── formatDateForOutput ────────────────────────────────────────────────────
|
||||
describe('formatDateForOutput', () => {
|
||||
it('returns empty string for invalid date', () => {
|
||||
expect(formatDateForOutput(dayjs('invalid'))).toBe('')
|
||||
})
|
||||
|
||||
it('returns date-only format by default (includeTime=false)', () => {
|
||||
const date = dayjs('2024-05-01T12:30:00')
|
||||
expect(formatDateForOutput(date)).toBe('2024-05-01')
|
||||
})
|
||||
|
||||
it('returns ISO datetime string when includeTime=true', () => {
|
||||
const date = dayjs('2024-05-01T12:30:00')
|
||||
const result = formatDateForOutput(date, true)
|
||||
expect(result).toMatch(/^2024-05-01T12:30:00/)
|
||||
})
|
||||
})
|
||||
|
||||
// ── convertTimezoneToOffsetStr ─────────────────────────────────────────────
|
||||
describe('convertTimezoneToOffsetStr', () => {
|
||||
it('should return default UTC+0 for undefined timezone', () => {
|
||||
it('returns default UTC+0 for undefined timezone', () => {
|
||||
expect(convertTimezoneToOffsetStr(undefined)).toBe('UTC+0')
|
||||
})
|
||||
|
||||
it('should return default UTC+0 for invalid timezone', () => {
|
||||
it('returns default UTC+0 for invalid timezone', () => {
|
||||
expect(convertTimezoneToOffsetStr('Invalid/Timezone')).toBe('UTC+0')
|
||||
})
|
||||
|
||||
it('should handle whole hour positive offsets without leading zeros', () => {
|
||||
it('handles positive whole-hour offsets', () => {
|
||||
expect(convertTimezoneToOffsetStr('Asia/Shanghai')).toBe('UTC+8')
|
||||
expect(convertTimezoneToOffsetStr('Pacific/Auckland')).toBe('UTC+12')
|
||||
expect(convertTimezoneToOffsetStr('Pacific/Apia')).toBe('UTC+13')
|
||||
})
|
||||
|
||||
it('should handle whole hour negative offsets without leading zeros', () => {
|
||||
it('handles negative whole-hour offsets', () => {
|
||||
expect(convertTimezoneToOffsetStr('Pacific/Niue')).toBe('UTC-11')
|
||||
expect(convertTimezoneToOffsetStr('Pacific/Honolulu')).toBe('UTC-10')
|
||||
expect(convertTimezoneToOffsetStr('America/New_York')).toBe('UTC-5')
|
||||
})
|
||||
|
||||
it('should handle zero offset', () => {
|
||||
it('handles zero offset', () => {
|
||||
expect(convertTimezoneToOffsetStr('Europe/London')).toBe('UTC+0')
|
||||
expect(convertTimezoneToOffsetStr('UTC')).toBe('UTC+0')
|
||||
})
|
||||
|
||||
it('should handle half-hour offsets (30 minutes)', () => {
|
||||
// India Standard Time: UTC+5:30
|
||||
it('handles half-hour offsets', () => {
|
||||
expect(convertTimezoneToOffsetStr('Asia/Kolkata')).toBe('UTC+5:30')
|
||||
// Australian Central Time: UTC+9:30
|
||||
expect(convertTimezoneToOffsetStr('Australia/Adelaide')).toBe('UTC+9:30')
|
||||
expect(convertTimezoneToOffsetStr('Australia/Darwin')).toBe('UTC+9:30')
|
||||
})
|
||||
|
||||
it('should handle 45-minute offsets', () => {
|
||||
// Chatham Time: UTC+12:45
|
||||
it('handles 45-minute offsets', () => {
|
||||
expect(convertTimezoneToOffsetStr('Pacific/Chatham')).toBe('UTC+12:45')
|
||||
})
|
||||
|
||||
it('should preserve leading zeros in minute part for non-zero minutes', () => {
|
||||
// Ensure +05:30 is displayed as "UTC+5:30", not "UTC+5:3"
|
||||
it('preserves leading zeros in minute part', () => {
|
||||
const result = convertTimezoneToOffsetStr('Asia/Kolkata')
|
||||
expect(result).toMatch(/UTC[+-]\d+:30/)
|
||||
expect(result).not.toMatch(/UTC[+-]\d+:3[^0]/)
|
||||
|
||||
@ -112,6 +112,7 @@ export const convertTimezoneToOffsetStr = (timezone?: string) => {
|
||||
// Extract offset from name format like "-11:00 Niue Time" or "+05:30 India Time"
|
||||
// Name format is always "{offset}:{minutes} {timezone name}"
|
||||
const offsetMatch = /^([+-]?\d{1,2}):(\d{2})/.exec(tzItem.name)
|
||||
/* v8 ignore next 2 -- timezone.json entries are normalized to "{offset} {name}"; this protects against malformed data only. */
|
||||
if (!offsetMatch)
|
||||
return DEFAULT_OFFSET_STR
|
||||
// Parse hours and minutes separately
|
||||
@ -141,6 +142,7 @@ const normalizeMillisecond = (value: string | undefined) => {
|
||||
return 0
|
||||
if (value.length === 3)
|
||||
return Number(value)
|
||||
/* v8 ignore next 2 -- TIME_ONLY_REGEX allows at most 3 fractional digits, so >3 can only occur after future regex changes. */
|
||||
if (value.length > 3)
|
||||
return Number(value.slice(0, 3))
|
||||
return Number(value.padEnd(3, '0'))
|
||||
|
||||
Reference in New Issue
Block a user