import { describe, it, expect } from 'vitest' import { canonicalizeDateText, listDateSegments, parseDateInput, resolveDateSegment, stepDateSegment, } from '@/components/ui/date/date-utils.js' const DATE_FMT = 'YYYY-MM-DD' const DATETIME_FMT = 'YYYY-MM-DD HH:mm:ss' /** 段列表压缩成 { field: [start, end] } 便于断言 */ function segmentMap(text, format) { return Object.fromEntries(listDateSegments(text, format).map((s) => [s.field, [s.start, s.end]])) } const at = (text) => new Date(text) describe('date-utils/listDateSegments + resolveDateSegment', () => { it('YYYY-MM-DD 下三段区间正确', () => { expect(segmentMap('2026-09-26', DATE_FMT)).toEqual({ year: [0, 4], month: [5, 7], day: [8, 10], }) }) it('分隔符换成 / 、. 或年月日汉字后段区间不变', () => { expect(segmentMap('2026/09/26', DATE_FMT)).toEqual({ year: [0, 4], month: [5, 7], day: [8, 10], }) expect(segmentMap('2026.09.26', DATE_FMT)).toEqual({ year: [0, 4], month: [5, 7], day: [8, 10], }) expect(segmentMap('2026年09月26日', 'YYYY年MM月DD日')).toEqual({ year: [0, 4], month: [5, 7], day: [8, 10], }) // 反向也成立:format 带汉字,手打短横线同样能定位到段 expect(segmentMap('2026-09-26', 'YYYY年MM月DD日')).toEqual({ year: [0, 4], month: [5, 7], day: [8, 10], }) }) it('含时分秒时追加 hour / minute / second 段', () => { expect(segmentMap('2026-09-26 14:30:00', DATETIME_FMT)).toEqual({ year: [0, 4], month: [5, 7], day: [8, 10], hour: [11, 13], minute: [14, 16], second: [17, 19], }) }) it('hh:mm A 下 meridiem 段覆盖整段 PM(大小写均可识别)', () => { expect(segmentMap('02:30 PM', 'hh:mm A')).toEqual({ hour: [0, 2], minute: [3, 5], meridiem: [6, 8], }) expect(segmentMap('02:30 pm', 'hh:mm A')).toEqual({ hour: [0, 2], minute: [3, 5], meridiem: [6, 8], }) }) it('Do 的 day 段含序数后缀,MMM 的 month 段为整段字母', () => { expect(segmentMap('26th', 'Do')).toEqual({ day: [0, 4] }) expect(segmentMap('Sep', 'MMM')).toEqual({ month: [0, 3] }) }) it('相邻段首尾相接(YYYYMMDD)时边界位归左段', () => { expect(segmentMap('20260926', 'YYYYMMDD')).toEqual({ year: [0, 4], month: [4, 6], day: [6, 8], }) expect(resolveDateSegment('20260926', 4, 'YYYYMMDD')?.field).toBe('year') expect(resolveDateSegment('20260926', 5, 'YYYYMMDD')?.field).toBe('month') }) it('光标停在分隔符上归左侧段,停在文本末尾归最后一段', () => { expect(resolveDateSegment('2026-09-26', 4, DATE_FMT)?.field).toBe('year') expect(resolveDateSegment('2026-09-26', 7, DATE_FMT)?.field).toBe('month') expect(resolveDateSegment('2026-09-26', 10, DATE_FMT)?.field).toBe('day') }) it('caret 越界时被 clamp 到文本长度,空文本与不匹配文本返回 null', () => { expect(resolveDateSegment('2026-09-26', 999, DATE_FMT)?.field).toBe('day') expect(resolveDateSegment('', 0, DATE_FMT)).toBeNull() expect(resolveDateSegment('2026-09-26', 3, 'hh:mm A')).toBeNull() expect(resolveDateSegment('2026-09-26', 3, DATETIME_FMT)).toBeNull() // 缺时分秒,整串失配 }) it('不可步进的 token(dddd)不产生段', () => { expect(listDateSegments('星期一', 'dddd')).toEqual([]) }) }) describe('date-utils/stepDateSegment', () => { it('day +1 跨月进位', () => { expect(stepDateSegment(at('2026-09-30T00:00:00'), 'day', 1)).toEqual( new Date('2026-10-01T00:00:00'), ) }) it('month +1 月末夹取', () => { expect(stepDateSegment(at('2026-01-31T00:00:00'), 'month', 1)).toEqual( new Date('2026-02-28T00:00:00'), ) }) it('year +1 闰日夹取', () => { expect(stepDateSegment(at('2024-02-29T00:00:00'), 'year', 1)).toEqual( new Date('2025-02-28T00:00:00'), ) }) it('hour / minute / second 回绕且日期不变', () => { expect(stepDateSegment(at('2026-09-26T23:10:00'), 'hour', 1)).toEqual( new Date('2026-09-26T00:10:00'), ) expect(stepDateSegment(at('2026-09-26T14:59:00'), 'minute', 1)).toEqual( new Date('2026-09-26T14:00:00'), ) expect(stepDateSegment(at('2026-09-26T14:30:59'), 'second', 1)).toEqual( new Date('2026-09-26T14:30:00'), ) }) it('hour -1 从 0 回绕到 23 且日期不变', () => { expect(stepDateSegment(at('2026-09-26T00:10:00'), 'hour', -1)).toEqual( new Date('2026-09-26T23:10:00'), ) }) it('meridiem ±1 只翻转上下午,小时数字不变(12 小时制显示)', () => { const am = at('2026-09-26T09:30:00') const pm = stepDateSegment(am, 'meridiem', 1) expect(pm).toEqual(new Date('2026-09-26T21:30:00')) expect(stepDateSegment(pm, 'meridiem', -1)).toEqual(am) }) it('非法 field 返回原日期,非法日期返回 null', () => { const value = at('2026-09-26T09:30:00') expect(stepDateSegment(value, 'unknown', 1)).toEqual(value) expect(stepDateSegment(null, 'day', 1)).toBeNull() }) }) describe('date-utils/parseDateInput', () => { it('空串与纯空白视为 isEmpty 且合法(组件按清空值处理)', () => { expect(parseDateInput('', DATE_FMT)).toEqual({ date: null, isEmpty: true, isValid: true }) expect(parseDateInput(' ', DATE_FMT)).toEqual({ date: null, isEmpty: true, isValid: true }) }) it('严格格式命中', () => { expect(parseDateInput('2026-09-26', DATE_FMT).date).toEqual(new Date('2026-09-26T00:00:00')) }) it('斜杠与点号分隔经归一化后命中', () => { expect(parseDateInput('2026/09/26', DATE_FMT).date).toEqual(new Date('2026-09-26T00:00:00')) expect(parseDateInput('2026.09.26', DATE_FMT).date).toEqual(new Date('2026-09-26T00:00:00')) }) it('中文年月日经 canonicalize 后命中', () => { expect(parseDateInput('2026年09月26日', DATE_FMT).date).toEqual(new Date('2026-09-26T00:00:00')) }) it('含时分秒的输入对 YYYY-MM-DD HH:mm 也能命中', () => { const result = parseDateInput('2026-09-26 14:30:00', 'YYYY-MM-DD HH:mm') expect(result.isValid).toBe(true) expect(result.date).toEqual(new Date('2026-09-26T14:30:00')) }) it('部分输入(只有年月)命中且日补 1', () => { const result = parseDateInput('2026-09', DATE_FMT) expect(result.isValid).toBe(true) expect(result.date).toEqual(new Date('2026-09-01T00:00:00')) }) it('纯文字与非法日期判定为不合法', () => { expect(parseDateInput('abc', DATE_FMT)).toEqual({ date: null, isEmpty: false, isValid: false }) expect(parseDateInput('2026-13-45', DATE_FMT).isValid).toBe(false) }) it('format 为数组时任一写法都能命中', () => { const fmts = ['YYYY年MM月DD日', 'YYYY-MM-DD'] expect(parseDateInput('2026年09月26日', fmts).isValid).toBe(true) expect(parseDateInput('2026-09-26', fmts).isValid).toBe(true) }) }) describe('date-utils/canonicalizeDateText', () => { it('统一分隔符并压缩空白', () => { expect(canonicalizeDateText(' 2026年09月26日 ')).toBe('2026-09-26') expect(canonicalizeDateText('2026.09.26')).toBe('2026-09-26') expect(canonicalizeDateText('2026-09-26 14:30')).toBe('2026-09-26 14:30') }) })