Files
workspace/code/fms/fms-vue/tests/components/date-picker.spec.js
T
2026-09-27 21:59:13 +08:00

350 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
})
})