283 lines
8.9 KiB
JavaScript
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()
|
|
})
|
|
})
|