20260927215912

This commit is contained in:
oneao committed 2026-09-27 21:59:13 +08:00
1 parent 27e49e2e45
commit f22e67fc85
294 files changed
+29247 -3576

No files matched your search

@@ -0,0 +1,349 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick, ref } from 'vue'
import DatePicker from '@/components/ui/date/date-picker.vue'
/** 用真 v-model 宿主,才能验证 emit 出的是字符串并回流到输入框 */
function mountPicker(props = {}) {
return mount(
{
components: { DatePicker },
setup() {
const value = ref(props.modelValue ?? '')
return {
value,
format: props.format,
valueFormat: props.valueFormat,
disabled: Boolean(props.disabled),
}
},
template: `
<DatePicker
v-model="value"
:format="format"
:value-format="valueFormat"
:disabled="disabled"
/>
`,
},
{ attachTo: document.body },
)
}
function inputOf(wrapper) {
return wrapper.find('.fms-date-input')
}
/** emit 来自子组件,宿主只负责 v-model 回流 */
function pickerOf(wrapper) {
return wrapper.findComponent(DatePicker)
}
function lastEmitted(wrapper) {
return pickerOf(wrapper).emitted('update:modelValue')?.at(-1)?.[0]
}
/** 把光标停到指定位置后按下方向键,模拟「微调当前段」 */
async function stepAt(input, pos, key) {
input.element.setSelectionRange(pos, pos)
await input.trigger('keydown', { key })
await nextTick()
}
afterEach(() => {
document.body.innerHTML = ''
})
describe('DatePicker 手动输入', () => {
it('输入框不再 readonly,type 仍为 text', () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD' })
const input = inputOf(wrapper)
expect(input.attributes('readonly')).toBeUndefined()
expect(input.attributes('type')).toBe('text')
wrapper.unmount()
})
it('键入合法整串后失焦提交为该格式的字符串', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD' })
const input = inputOf(wrapper)
await input.setValue('2026-09-26')
await input.trigger('blur')
expect(lastEmitted(wrapper)).toBe('2026-09-26')
wrapper.unmount()
})
it('斜杠、点号与中文年月日都能提交', async () => {
for (const text of ['2026/09/26', '2026.09.26', '2026年09月26日']) {
const wrapper = mountPicker({ format: 'YYYY-MM-DD' })
const input = inputOf(wrapper)
await input.setValue(text)
await input.trigger('blur')
expect(lastEmitted(wrapper), text).toBe('2026-09-26')
wrapper.unmount()
}
})
it('Enter 提交当前文本并关闭面板', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD' })
const input = inputOf(wrapper)
await input.trigger('click')
await nextTick()
expect(wrapper.find('.fms-date-picker').classes()).toContain('is-open')
await input.setValue('2026-09-26')
await input.trigger('keydown', { key: 'Enter' })
await nextTick()
expect(lastEmitted(wrapper)).toBe('2026-09-26')
expect(wrapper.find('.fms-date-picker').classes()).not.toContain('is-open')
wrapper.unmount()
})
it('datetime:format 决定展示,valueFormat 决定提交格式(秒会被补齐)', async () => {
const wrapper = mountPicker({
format: 'YYYY-MM-DD HH:mm',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
})
const input = inputOf(wrapper)
await input.setValue('2026-09-26 14:30')
await input.trigger('blur')
expect(lastEmitted(wrapper)).toBe('2026-09-26 14:30:00')
wrapper.unmount()
})
it('非法输入失焦:不 emit,文本回退到已提交值,并标记错误态', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-01-01' })
const input = inputOf(wrapper)
await input.setValue('2026-13-45')
await input.trigger('blur')
expect(pickerOf(wrapper).emitted('update:modelValue')).toBeFalsy()
expect(input.element.value).toBe('2026-01-01')
expect(wrapper.find('.fms-date-picker').classes()).toContain('is-invalid')
expect(input.attributes('aria-invalid')).toBe('true')
wrapper.unmount()
})
it('错误态 1500ms 后自动清除', async () => {
vi.useFakeTimers()
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-01-01' })
const input = inputOf(wrapper)
await input.setValue('abc')
await input.trigger('blur')
expect(wrapper.find('.fms-date-picker').classes()).toContain('is-invalid')
vi.advanceTimersByTime(1500)
await nextTick()
expect(wrapper.find('.fms-date-picker').classes()).not.toContain('is-invalid')
wrapper.unmount()
vi.useRealTimers()
})
it('文本与已提交值相同时失焦不重复 emit', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.setValue('2026-09-26')
await input.trigger('blur')
expect(pickerOf(wrapper).emitted('update:modelValue')).toBeFalsy()
wrapper.unmount()
})
it('清空文本失焦等同清除值', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.setValue('')
await input.trigger('blur')
expect(lastEmitted(wrapper)).toBe('')
wrapper.unmount()
})
it('Esc 还原为已提交文本并关闭面板', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.trigger('click')
await nextTick()
await input.setValue('2027-01-01')
await input.trigger('keydown', { key: 'Escape' })
await nextTick()
expect(pickerOf(wrapper).emitted('update:modelValue')).toBeFalsy()
expect(input.element.value).toBe('2026-09-26')
expect(wrapper.find('.fms-date-picker').classes()).not.toContain('is-open')
wrapper.unmount()
})
it('Space 作为普通字符输入,不打开面板', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD HH:mm', modelValue: '2026-09-26 14:30' })
const input = inputOf(wrapper)
await input.setValue('2026-09-26 14:30')
await input.trigger('keydown', { key: ' ', code: 'Space' })
await nextTick()
expect(wrapper.find('.fms-date-picker').classes()).not.toContain('is-open')
wrapper.unmount()
})
})
describe('DatePicker 方向键微调当前段', () => {
it('年份段 ArrowUp +1', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.trigger('focus')
await stepAt(input, 2, 'ArrowUp')
expect(input.element.value).toBe('2027-09-26')
wrapper.unmount()
})
it('月份段 ArrowDown -1', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.trigger('focus')
await stepAt(input, 6, 'ArrowDown')
expect(input.element.value).toBe('2026-08-26')
wrapper.unmount()
})
it('日段 ArrowUp 跨月进位', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-30' })
const input = inputOf(wrapper)
await input.trigger('focus')
await stepAt(input, 9, 'ArrowUp')
expect(input.element.value).toBe('2026-10-01')
wrapper.unmount()
})
it('时分秒微调只改当前段且日期不变', async () => {
const wrapper = mountPicker({
format: 'YYYY-MM-DD HH:mm:ss',
modelValue: '2026-09-26 23:10:00',
})
const input = inputOf(wrapper)
await input.trigger('focus')
await stepAt(input, 12, 'ArrowUp') // 小时段回绕
expect(input.element.value).toBe('2026-09-26 00:10:00')
await stepAt(input, 15, 'ArrowDown') // 分钟段
expect(input.element.value).toBe('2026-09-26 00:09:00')
wrapper.unmount()
})
it('光标不在任何段上时不拦截方向键、不改文本', async () => {
// ddd 是不可步进的 token(编译为跳读片段),文本尾部的星期不产生任何段
const wrapper = mountPicker({ format: 'YYYY-MM-DD ddd', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.trigger('focus')
const before = input.element.value
expect(before).toMatch(/^2026-09-26 \S+$/)
await stepAt(input, before.length, 'ArrowUp')
expect(input.element.value).toBe(before)
wrapper.unmount()
})
it('微调结果失焦后提交为新值', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.trigger('focus')
await stepAt(input, 2, 'ArrowUp')
await input.trigger('blur')
expect(lastEmitted(wrapper)).toBe('2027-09-26')
wrapper.unmount()
})
it('Alt+ArrowDown 打开面板并把焦点交给日历网格', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
const input = inputOf(wrapper)
await input.trigger('focus')
await input.trigger('keydown', { key: 'ArrowDown', altKey: true })
await nextTick()
expect(wrapper.find('.fms-date-picker').classes()).toContain('is-open')
expect(document.body.querySelector('.fms-cal-cell:focus')).not.toBeNull()
wrapper.unmount()
})
})
describe('DatePicker 与面板 / 清除按钮的协作', () => {
it('点输入框打开面板且焦点留在输入框,可以继续打字', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD' })
const input = inputOf(wrapper)
input.element.focus()
await input.trigger('click')
await nextTick()
expect(wrapper.find('.fms-date-picker').classes()).toContain('is-open')
expect(document.activeElement).toBe(input.element)
wrapper.unmount()
})
it('面板点选日期后输入框显示新值并清空错误态', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-01-01' })
const input = inputOf(wrapper)
await input.setValue('abc')
await input.trigger('blur')
expect(wrapper.find('.fms-date-picker').classes()).toContain('is-invalid')
await input.trigger('click')
await nextTick()
const cell = document.body.querySelector('.fms-cal-cell:not(.is-disabled)')
cell.click()
await nextTick()
await nextTick()
const committed = lastEmitted(wrapper)
expect(committed).toMatch(/^\d{4}-\d{2}-\d{2}$/)
expect(input.element.value).toBe(committed)
expect(wrapper.find('.fms-date-picker').classes()).not.toContain('is-invalid')
wrapper.unmount()
})
it('清除按钮清空值且不触发重复提交', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26' })
await inputOf(wrapper).trigger('click')
await nextTick()
const clear = wrapper.find('.fms-date-clear')
await clear.trigger('click')
await nextTick()
expect(lastEmitted(wrapper)).toBe('')
expect(document.body.querySelector('.popover-content')).toBeNull()
wrapper.unmount()
})
})
describe('DatePicker 禁用态与多格式', () => {
it('禁用态下输入与方向键均不生效,也不进入错误态', async () => {
const wrapper = mountPicker({ format: 'YYYY-MM-DD', modelValue: '2026-09-26', disabled: true })
const input = inputOf(wrapper)
expect(input.attributes('disabled')).toBeDefined()
await input.trigger('keydown', { key: 'ArrowUp' })
await nextTick()
expect(pickerOf(wrapper).emitted('update:modelValue')).toBeFalsy()
expect(wrapper.find('.fms-date-picker').classes()).not.toContain('is-invalid')
wrapper.unmount()
})
it('format 数组时任一写法都能提交', async () => {
for (const text of ['2026-09-26', '2026年09月26日']) {
const wrapper = mountPicker({
format: ['YYYY年MM月DD日', 'YYYY-MM-DD'],
valueFormat: 'YYYY-MM-DD',
})
const input = inputOf(wrapper)
await input.setValue(text)
await input.trigger('blur')
expect(lastEmitted(wrapper), text).toBe('2026-09-26')
wrapper.unmount()
}
})
})