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 const mountRange = (props: DateRangeProps = {}) => mount(DateRangeForTest, {props, attachTo: document.body}) const openAndClickDays = async (wrapper: ReturnType, 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) }) })