Files
malio-layer-ui/app/components/malio/date/Date.test.ts
T
tristan 1b5a4c9920 feat(ui) : prop reserveMessageSpace (défaut true) sur Select/SelectCheckbox/Checkbox/date/time
Ajoute reserveMessageSpace (défaut true) pour permettre de ne pas réserver
la ligne de message d'aide quand aucun message n'est présent. Comportement
inchangé par défaut. La famille date hérite via $attrs → CalendarField.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:53:22 +02:00

262 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
import {mount} from '@vue/test-utils'
import type {DefineComponent} from 'vue'
import Date_ from './Date.vue'
type DateProps = {
id?: string
name?: string
label?: string
modelValue?: string | null
placeholder?: string
required?: boolean
disabled?: boolean
readonly?: boolean
hint?: string
error?: string
success?: string
min?: string
max?: string
clearable?: boolean
inputClass?: string
labelClass?: string
groupClass?: string
reserveMessageSpace?: boolean
}
const DateForTest = Date_ as DefineComponent<DateProps>
const mountDate = (props: DateProps = {}) => mount(DateForTest, {props, attachTo: document.body})
describe('MalioDate', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026
})
afterEach(() => vi.useRealTimers())
describe('rendu', () => {
it('renders the label and the calendar icon', () => {
const wrapper = mountDate({label: 'Date de naissance'})
expect(wrapper.get('label').text()).toBe('Date de naissance')
expect(wrapper.find('[data-test="calendar-icon"]').exists()).toBe(true)
})
it('affiche l\'astérisque quand required est vrai', () => {
const wrapper = mountDate({label: 'Champ', required: true})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n\'affiche pas l\'astérisque par défaut', () => {
const wrapper = mountDate({label: 'Champ'})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
it('displays the formatted value in the field', () => {
const wrapper = mountDate({modelValue: '2026-05-19'})
const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement
expect(input.value).toBe('19/05/2026')
})
it('does not show the popover initially', () => {
const wrapper = mountDate()
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
})
})
describe('popover', () => {
it('opens on field click', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
})
it('opens on the current month when there is no value', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Mai 2026')
})
it('opens on the value month when a value is set', async () => {
const wrapper = mountDate({modelValue: '2025-12-25'})
await wrapper.get('[data-test="date-input"]').trigger('click')
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Décembre 2025')
})
it('closes on outside mousedown', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
document.body.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}))
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
})
})
describe('navigation jours', () => {
it('goes to the next month on the right chevron', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
await wrapper.get('[data-test="header-next"]').trigger('click')
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Juin 2026')
})
it('rolls December to January and bumps the year', async () => {
const wrapper = mountDate({modelValue: '2026-12-15'})
await wrapper.get('[data-test="date-input"]').trigger('click')
await wrapper.get('[data-test="header-next"]').trigger('click')
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Janvier 2027')
})
})
describe('sélection', () => {
it('emits the ISO date and closes on day click', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19'])
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
})
})
describe('bornes min/max', () => {
it('disables days outside the range', async () => {
const wrapper = mountDate({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()
})
})
describe('vue mois', () => {
it('switches to month view on header toggle', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
await wrapper.get('[data-test="header-toggle"]').trigger('click')
expect(wrapper.find('[data-test="month-picker"]').exists()).toBe(true)
})
it('navigates the year with chevrons in month view', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
await wrapper.get('[data-test="header-toggle"]').trigger('click')
await wrapper.get('[data-test="header-next"]').trigger('click')
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('2027')
})
it('returns to day view on month click', async () => {
const wrapper = mountDate()
await wrapper.get('[data-test="date-input"]').trigger('click')
await wrapper.get('[data-test="header-toggle"]').trigger('click')
await wrapper.get('[data-test="month"][data-month="0"]').trigger('click')
expect(wrapper.find('[data-test="month-picker"]').exists()).toBe(false)
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Janvier 2026')
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
})
})
describe('effacement', () => {
it('shows the clear button when there is a value', () => {
const wrapper = mountDate({modelValue: '2026-05-19'})
expect(wrapper.find('[data-test="clear"]').exists()).toBe(true)
})
it('hides the clear button when empty', () => {
const wrapper = mountDate()
expect(wrapper.find('[data-test="clear"]').exists()).toBe(false)
})
it('emits null and does not open the popover on clear', async () => {
const wrapper = mountDate({modelValue: '2026-05-19'})
await wrapper.get('[data-test="clear"]').trigger('click')
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null])
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
})
})
describe('états', () => {
it('does not open when disabled', async () => {
const wrapper = mountDate({disabled: true})
await wrapper.get('[data-test="date-input"]').trigger('click')
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
})
it('does not open when readonly', async () => {
const wrapper = mountDate({readonly: true, modelValue: '2026-05-19'})
await wrapper.get('[data-test="date-input"]').trigger('click')
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
})
it('readonly vide : bordure noire sans bleu', () => {
const wrapper = mountDate({readonly: true})
const input = wrapper.get('[data-test="date-input"]')
expect(input.classes()).toContain('border-black')
expect(input.classes()).not.toContain('border-m-muted')
expect(input.classes()).not.toContain('focus:border-m-primary')
})
it('readonly vide : label muted sans bleu', () => {
const wrapper = mountDate({readonly: true, label: 'Date'})
const label = wrapper.get('label')
expect(label.classes()).toContain('text-m-muted')
expect(label.classes()).not.toContain('text-m-primary')
})
it('readonly vide : icône calendrier en text-m-muted', () => {
const wrapper = mountDate({readonly: true, label: 'Date'})
expect(wrapper.get('[data-test="calendar-icon"]').classes()).toContain('text-m-muted')
})
it('readonly rempli : label et icône en noir, bordure noire', () => {
const wrapper = mountDate({readonly: true, label: 'Date', modelValue: '2026-05-19'})
const input = wrapper.get('[data-test="date-input"]')
const label = wrapper.get('label')
const icon = wrapper.get('[data-test="calendar-icon"]')
expect(input.classes()).toContain('border-black')
expect(input.classes()).not.toContain('focus:border-m-primary')
expect(label.classes()).toContain('text-black')
expect(icon.classes()).toContain('text-black')
})
})
describe('accessibilité', () => {
it('sets aria-invalid and describedby on error', () => {
const wrapper = mountDate({error: 'Date requise'})
const input = wrapper.get('[data-test="date-input"]')
expect(input.attributes('aria-invalid')).toBe('true')
expect(input.attributes('aria-describedby')).toBeTruthy()
expect(wrapper.text()).toContain('Date requise')
})
})
describe('synchronisation externe', () => {
it('updates the displayed value when modelValue changes', async () => {
const wrapper = mountDate({modelValue: '2026-05-19'})
await wrapper.setProps({modelValue: '2026-12-25'})
const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement
expect(input.value).toBe('25/12/2026')
})
})
describe('reserveMessageSpace', () => {
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mountDate({label: 'Champ'})
const msg = wrapper.find('[id$="-describedby"]')
expect(msg.exists()).toBe(true)
expect(msg.classes()).toContain('min-h-[1rem]')
})
it('reserveMessageSpace=false sans message : pas de ligne réservée', () => {
const wrapper = mountDate({label: 'Champ', reserveMessageSpace: false})
expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false)
})
it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => {
const wrapper = mountDate({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'})
const msg = wrapper.find('[id$="-describedby"]')
expect(msg.exists()).toBe(true)
expect(msg.classes()).not.toContain('min-h-[1rem]')
})
})
})