211 lines
7.3 KiB
JavaScript
211 lines
7.3 KiB
JavaScript
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')
|
||
})
|
||
})
|