156 lines
6.9 KiB
TypeScript
156 lines
6.9 KiB
TypeScript
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
import {mount} from '@vue/test-utils'
|
|
import type {DefineComponent} from 'vue'
|
|
import DateRange from './DateRange.vue'
|
|
|
|
type RangeValue = {start: string; end: string}
|
|
type DateRangeProps = {
|
|
modelValue?: RangeValue | null
|
|
label?: string
|
|
disabled?: boolean
|
|
readonly?: boolean
|
|
error?: string
|
|
min?: string
|
|
max?: string
|
|
clearable?: boolean
|
|
}
|
|
|
|
const DateRangeForTest = DateRange as DefineComponent<DateRangeProps>
|
|
const mountRange = (props: DateRangeProps = {}) =>
|
|
mount(DateRangeForTest, {props, attachTo: document.body})
|
|
|
|
const openAndClickDays = async (wrapper: ReturnType<typeof mountRange>, isos: string[]) => {
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
for (const iso of isos) {
|
|
await wrapper.get(`[data-test="day"][data-iso="${iso}"]`).trigger('click')
|
|
}
|
|
}
|
|
|
|
describe('MalioDateRange', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026
|
|
})
|
|
afterEach(() => vi.useRealTimers())
|
|
|
|
it('renders the label and calendar icon', () => {
|
|
const wrapper = mountRange({label: 'Période'})
|
|
expect(wrapper.get('label').text()).toBe('Période')
|
|
expect(wrapper.find('[data-test="calendar-icon"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('displays the formatted range when modelValue is set', () => {
|
|
const wrapper = mountRange({modelValue: {start: '2026-05-19', end: '2026-05-25'}})
|
|
const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement
|
|
expect(input.value).toBe('19/05/2026 - 25/05/2026')
|
|
})
|
|
|
|
it('shows an empty field without a value', () => {
|
|
const wrapper = mountRange()
|
|
const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement
|
|
expect(input.value).toBe('')
|
|
})
|
|
|
|
it('opens on the start month when a range is set', async () => {
|
|
const wrapper = mountRange({modelValue: {start: '2025-12-10', end: '2025-12-20'}})
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Décembre 2025')
|
|
})
|
|
|
|
it('does not emit on the first click', async () => {
|
|
const wrapper = mountRange()
|
|
await openAndClickDays(wrapper, ['2026-05-19'])
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('emits the range and closes on the second click', async () => {
|
|
const wrapper = mountRange()
|
|
await openAndClickDays(wrapper, ['2026-05-19', '2026-05-25'])
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{start: '2026-05-19', end: '2026-05-25'}])
|
|
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('auto-inverts when the second click is before the first', async () => {
|
|
const wrapper = mountRange()
|
|
await openAndClickDays(wrapper, ['2026-05-25', '2026-05-19'])
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{start: '2026-05-19', end: '2026-05-25'}])
|
|
})
|
|
|
|
it('allows a single-day range', async () => {
|
|
const wrapper = mountRange()
|
|
await openAndClickDays(wrapper, ['2026-05-19', '2026-05-19'])
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{start: '2026-05-19', end: '2026-05-19'}])
|
|
})
|
|
|
|
it('restarts a new range on the third click', async () => {
|
|
const wrapper = mountRange()
|
|
await openAndClickDays(wrapper, ['2026-05-19', '2026-05-25'])
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
await wrapper.get('[data-test="day"][data-iso="2026-05-10"]').trigger('click')
|
|
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
|
await wrapper.get('[data-test="day"][data-iso="2026-05-12"]').trigger('click')
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{start: '2026-05-10', end: '2026-05-12'}])
|
|
})
|
|
|
|
it('previews the range on hover while selecting', async () => {
|
|
const wrapper = mountRange()
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
|
await wrapper.get('[data-test="day"][data-iso="2026-05-22"]').trigger('mouseenter')
|
|
expect(wrapper.get('[data-test="day"][data-iso="2026-05-20"]').attributes('data-range-role')).toBe('in-range')
|
|
})
|
|
|
|
it('does not preview before selecting', async () => {
|
|
const wrapper = mountRange()
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
await wrapper.get('[data-test="day"][data-iso="2026-05-22"]').trigger('mouseenter')
|
|
expect(wrapper.get('[data-test="day"][data-iso="2026-05-20"]').attributes('data-range-role')).toBe('none')
|
|
})
|
|
|
|
it('marks start, end and in-range roles for a committed range', async () => {
|
|
const wrapper = mountRange({modelValue: {start: '2026-05-19', end: '2026-05-25'}})
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
expect(wrapper.get('[data-test="day"][data-iso="2026-05-19"]').attributes('data-range-role')).toBe('start')
|
|
expect(wrapper.get('[data-test="day"][data-iso="2026-05-25"]').attributes('data-range-role')).toBe('end')
|
|
expect(wrapper.get('[data-test="day"][data-iso="2026-05-22"]').attributes('data-range-role')).toBe('in-range')
|
|
})
|
|
|
|
it('cancels an in-progress selection on outside click', async () => {
|
|
const wrapper = mountRange()
|
|
await openAndClickDays(wrapper, ['2026-05-19'])
|
|
document.body.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}))
|
|
await wrapper.vm.$nextTick()
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
expect(wrapper.get('[data-test="day"][data-iso="2026-05-19"]').attributes('data-range-role')).toBe('none')
|
|
})
|
|
|
|
it('emits null on clear', async () => {
|
|
const wrapper = mountRange({modelValue: {start: '2026-05-19', end: '2026-05-25'}})
|
|
await wrapper.get('[data-test="clear"]').trigger('click')
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null])
|
|
})
|
|
|
|
it('disables days outside min/max', async () => {
|
|
const wrapper = mountRange({min: '2026-05-10', max: '2026-05-20'})
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
const outside = wrapper.get('[data-test="day"][data-iso="2026-05-05"]')
|
|
expect((outside.element as HTMLButtonElement).disabled).toBe(true)
|
|
await outside.trigger('click')
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
})
|
|
|
|
it('sets aria-invalid on error', () => {
|
|
const wrapper = mountRange({error: 'Période requise'})
|
|
expect(wrapper.get('[data-test="date-input"]').attributes('aria-invalid')).toBe('true')
|
|
expect(wrapper.text()).toContain('Période requise')
|
|
})
|
|
|
|
it('does not open when disabled', async () => {
|
|
const wrapper = mountRange({disabled: true})
|
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
|
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
|
|
})
|
|
})
|