Files
malio-layer-ui/app/components/malio/date/DateRange.test.ts
2026-05-20 11:56:05 +02:00

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)
})
})