Files
2026-09-27 21:59:13 +08:00

283 lines
8.9 KiB
JavaScript

import { describe, it, expect, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick, ref } from 'vue'
import RangePicker from '@/components/ui/date/range-picker.vue'
/** 用真 v-model 宿主,才能验证 emit 出的是字符串数组并回流到输入框 */
function mountRange(props = {}) {
return mount(
{
components: { RangePicker },
setup() {
const value = ref(props.modelValue ?? null)
return {
value,
format: props.format,
valueFormat: props.valueFormat,
disabled: Boolean(props.disabled),
}
},
template: `
<RangePicker
v-model="value"
:format="format"
:value-format="valueFormat"
:disabled="disabled"
/>
`,
},
{ attachTo: document.body },
)
}
function inputs(wrapper) {
return wrapper.findAll('.fms-range-input')
}
/** emit 来自子组件,宿主只负责 v-model 回流 */
function pickerOf(wrapper) {
return wrapper.findComponent(RangePicker)
}
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('RangePicker 两端手动输入', () => {
it('两端输入框都不再 readonly', () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [start, end] = inputs(wrapper)
expect(start.attributes('readonly')).toBeUndefined()
expect(end.attributes('readonly')).toBeUndefined()
wrapper.unmount()
})
it('只编辑开始端并失焦:另一端保持空串', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [start] = inputs(wrapper)
await start.setValue('2026-09-26')
await start.trigger('blur')
expect(lastEmitted(wrapper)).toEqual(['2026-09-26', ''])
wrapper.unmount()
})
it('只编辑结束端并失焦:开始端保持空串', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [, end] = inputs(wrapper)
await end.setValue('2026-12-31')
await end.trigger('blur')
expect(lastEmitted(wrapper)).toEqual(['', '2026-12-31'])
wrapper.unmount()
})
it('两端倒序输入时自动排序为有序区间', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [start, end] = inputs(wrapper)
await start.setValue('2026-12-31')
await start.trigger('blur')
await end.setValue('2026-01-01')
await end.trigger('blur')
expect(lastEmitted(wrapper)).toEqual(['2026-01-01', '2026-12-31'])
wrapper.unmount()
})
it('开始端非法:不 emit,错误只标在开始端', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [start, end] = inputs(wrapper)
await start.setValue('2026-13-45')
await start.trigger('blur')
expect(pickerOf(wrapper).emitted('update:modelValue')).toBeFalsy()
expect(start.element.value).toBe('')
expect(start.attributes('aria-invalid')).toBe('true')
expect(end.attributes('aria-invalid')).toBeUndefined()
expect(wrapper.find('.fms-range-picker').classes()).toContain('is-invalid')
wrapper.unmount()
})
it('结束端非法:错误只标在结束端', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD', modelValue: ['2026-01-01', '2026-02-01'] })
const [start, end] = inputs(wrapper)
await end.setValue('abc')
await end.trigger('blur')
expect(pickerOf(wrapper).emitted('update:modelValue')).toBeFalsy()
expect(end.element.value).toBe('2026-02-01')
expect(end.attributes('aria-invalid')).toBe('true')
expect(start.attributes('aria-invalid')).toBeUndefined()
wrapper.unmount()
})
it('Tab 链路:开始端失焦先提交,结束端聚焦时预填已提交文本', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [start, end] = inputs(wrapper)
await start.setValue('2026-09-26')
// jsdom 的 trigger('blur') 不带 relatedTarget,这里手动派发以模拟 Tab 到结束端
start.element.dispatchEvent(new FocusEvent('blur', { relatedTarget: end.element }))
await nextTick()
expect(lastEmitted(wrapper)).toEqual(['2026-09-26', ''])
end.element.focus()
await end.trigger('focus')
expect(end.element.value).toBe('')
await end.setValue('2026-12-31')
await end.trigger('blur')
expect(lastEmitted(wrapper)).toEqual(['2026-09-26', '2026-12-31'])
wrapper.unmount()
})
it('两端都清空后 emit 空串数组,清除按钮消失', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD', modelValue: ['2026-09-26', '2026-12-31'] })
const [start, end] = inputs(wrapper)
await start.setValue('')
await start.trigger('blur')
await end.setValue('')
await end.trigger('blur')
expect(lastEmitted(wrapper)).toEqual(['', ''])
await nextTick()
expect(wrapper.find('.fms-date-clear').exists()).toBe(false)
wrapper.unmount()
})
it('Esc 只还原当前聚焦端,另一端保持不变', async () => {
const wrapper = mountRange({
format: 'YYYY-MM-DD',
modelValue: ['2026-01-01', '2026-12-31'],
})
const [start, end] = inputs(wrapper)
await start.setValue('2030-05-05')
await start.trigger('keydown', { key: 'Escape' })
await nextTick()
expect(start.element.value).toBe('2026-01-01')
expect(end.element.value).toBe('2026-12-31')
expect(pickerOf(wrapper).emitted('update:modelValue')).toBeFalsy()
wrapper.unmount()
})
it('Enter 提交当前端并关闭面板', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [start] = inputs(wrapper)
await start.trigger('click')
await nextTick()
expect(wrapper.find('.fms-range-picker').classes()).toContain('is-open')
await start.setValue('2026-09-26')
await start.trigger('keydown', { key: 'Enter' })
await nextTick()
expect(lastEmitted(wrapper)).toEqual(['2026-09-26', ''])
expect(wrapper.find('.fms-range-picker').classes()).not.toContain('is-open')
wrapper.unmount()
})
})
describe('RangePicker 两端方向键微调', () => {
it('两端各自独立微调:开始端改年、结束端改日', async () => {
const wrapper = mountRange({
format: 'YYYY-MM-DD',
modelValue: ['2026-01-01', '2026-12-31'],
})
const [start, end] = inputs(wrapper)
await start.trigger('focus')
await stepAt(start, 2, 'ArrowUp')
expect(start.element.value).toBe('2027-01-01')
expect(end.element.value).toBe('2026-12-31')
await end.trigger('focus')
await stepAt(end, 9, 'ArrowUp')
expect(end.element.value).toBe('2027-01-01') // 12-31 跨年进位
wrapper.unmount()
})
it('微调结果失焦后提交,且倒序时自动排序', async () => {
const wrapper = mountRange({
format: 'YYYY-MM-DD',
modelValue: ['2026-01-01', '2026-12-31'],
})
const [start] = inputs(wrapper)
await start.trigger('focus')
await stepAt(start, 2, 'ArrowUp') // 2027-01-01 超过结束端
await start.trigger('blur')
expect(lastEmitted(wrapper)).toEqual(['2026-12-31', '2027-01-01'])
wrapper.unmount()
})
})
describe('RangePicker datetime 与面板协作', () => {
it('datetime 端点提交出秒级字符串', async () => {
const wrapper = mountRange({
format: 'YYYY-MM-DD HH:mm',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
})
const [start, end] = inputs(wrapper)
await start.setValue('2026-09-26 14:30')
await start.trigger('blur')
await end.setValue('2026-09-26 18:00')
await end.trigger('blur')
expect(lastEmitted(wrapper)).toEqual(['2026-09-26 14:30:00', '2026-09-26 18:00:00'])
wrapper.unmount()
})
it('回归:面板点选两端后「确定」仍提交字符串数组', async () => {
const wrapper = mountRange({ format: 'YYYY-MM-DD' })
const [start] = inputs(wrapper)
await start.trigger('click')
await nextTick()
const cells = document.body.querySelectorAll('.fms-cal-cell:not(.is-disabled)')
cells[10].click()
await nextTick()
cells[20].click()
await nextTick()
const committed = lastEmitted(wrapper)
expect(committed).toHaveLength(2)
expect(committed[0]).toMatch(/^\d{4}-\d{2}-\d{2}$/)
expect(committed[1]).toMatch(/^\d{4}-\d{2}-\d{2}$/)
expect(committed[0] <= committed[1]).toBe(true)
wrapper.unmount()
})
it('回归:清除按钮清空两端', async () => {
const wrapper = mountRange({
format: 'YYYY-MM-DD',
modelValue: ['2026-01-01', '2026-12-31'],
})
await inputs(wrapper)[0].trigger('click')
await nextTick()
await wrapper.find('.fms-date-clear').trigger('click')
await nextTick()
expect(lastEmitted(wrapper)).toEqual(['', ''])
wrapper.unmount()
})
})