9f9723d01c
Ticket MUI-43 : exposer l'état de validité de MalioDate (saisie invalide avalée silencieusement) + portage de la saisie clavier sur MalioDateTime.
## Contenu
**MalioDate**
- Nouvel event `update:valid(boolean)` : `false` sur saisie malformée ou hors min/max (qui n'émet pas `modelValue`), `true` sinon ; émis dès le montage. La validité ne couvre pas `required` (champ vide = valide).
**MalioDateTime**
- Prop `editable` : saisie clavier `JJ/MM/AAAA HH:MM` (masque maska, validation au blur/Entrée, `invalidMessage`) + même `update:valid`.
- Nouveau parseur `parseDisplayToIsoDateTime`.
**Famille Date editable (Date + DateTime)**
- Gabarit fantôme progressif : le format s'affiche en gris et se remplit au fil de la saisie (overlay ghost mirror, texte de l'input transparent).
- Séparateurs (/, espace, :) posés automatiquement (maska `eager`), espace insécable pour éviter le collage `12/12/1999HH:MM`.
- `CalendarField` : prop `placeholderTemplate` (le masque maska en est dérivé).
**Corrections**
- La croix d'effacement réinitialise la saisie clavier même après une date invalide (le v-model restant null, le champ ne se vidait pas).
- Fix d'un test `Date.test.ts` cassé sur develop (`trigger('keydown.enter')` envoie key='enter' ≠ handler `e.key === 'Enter'`).
## Portée
MalioDate seul pour la validité (les cousins DateRange/DateWeek n'ont pas de saisie clavier donc pas le bug). Sémantique `valid` = malformé only.
## Tests
`app/components/malio/date/` : 187/187, ESLint propre. Vérifié visuellement dans le playground (page Date & heure).
## Doc
COMPONENTS.md + CHANGELOG.md à jour.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #71
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
import {describe, expect, it} from 'vitest'
|
|
import {
|
|
composeDateTime,
|
|
formatIsoDateTimeToDisplay,
|
|
isValidIsoDateTime,
|
|
parseDisplayToIsoDateTime,
|
|
splitDateTime,
|
|
} from './datetimeFormat'
|
|
|
|
describe('datetimeFormat', () => {
|
|
describe('isValidIsoDateTime', () => {
|
|
it('accepte un datetime ISO complet valide', () => {
|
|
expect(isValidIsoDateTime('2026-05-20T14:30:00')).toBe(true)
|
|
expect(isValidIsoDateTime('2026-01-01T00:00:00')).toBe(true)
|
|
expect(isValidIsoDateTime('2026-12-31T23:59:59')).toBe(true)
|
|
})
|
|
|
|
it('rejette une date seule, des composants invalides ou une chaîne vide', () => {
|
|
expect(isValidIsoDateTime('2026-05-20')).toBe(false)
|
|
expect(isValidIsoDateTime('2026-13-01T00:00:00')).toBe(false)
|
|
expect(isValidIsoDateTime('2026-05-20T24:00:00')).toBe(false)
|
|
expect(isValidIsoDateTime('2026-05-20T14:60:00')).toBe(false)
|
|
expect(isValidIsoDateTime('2026-05-20T14:30:60')).toBe(false)
|
|
expect(isValidIsoDateTime('2026-05-20T14:30')).toBe(false)
|
|
expect(isValidIsoDateTime('')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('formatIsoDateTimeToDisplay', () => {
|
|
it('formate un datetime valide en JJ/MM/AAAA HH:MM', () => {
|
|
expect(formatIsoDateTimeToDisplay('2026-05-20T14:30:00')).toBe('20/05/2026 14:30')
|
|
})
|
|
|
|
it('renvoie une chaîne vide pour nul ou invalide', () => {
|
|
expect(formatIsoDateTimeToDisplay(null)).toBe('')
|
|
expect(formatIsoDateTimeToDisplay('2026-05-20')).toBe('')
|
|
expect(formatIsoDateTimeToDisplay('nope')).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('splitDateTime', () => {
|
|
it('découpe un datetime valide', () => {
|
|
expect(splitDateTime('2026-05-20T14:30:00')).toEqual({date: '2026-05-20', time: '14:30'})
|
|
})
|
|
|
|
it('renvoie date null et time vide pour nul, date seule ou invalide', () => {
|
|
expect(splitDateTime(null)).toEqual({date: null, time: ''})
|
|
expect(splitDateTime('2026-05-20')).toEqual({date: null, time: ''})
|
|
expect(splitDateTime('nope')).toEqual({date: null, time: ''})
|
|
})
|
|
})
|
|
|
|
describe('parseDisplayToIsoDateTime', () => {
|
|
it('parse un JJ/MM/AAAA HH:MM valide en datetime ISO', () => {
|
|
expect(parseDisplayToIsoDateTime('20/05/2026 14:30')).toBe('2026-05-20T14:30:00')
|
|
expect(parseDisplayToIsoDateTime('01/01/2026 00:00')).toBe('2026-01-01T00:00:00')
|
|
expect(parseDisplayToIsoDateTime('31/12/2026 23:59')).toBe('2026-12-31T23:59:00')
|
|
})
|
|
|
|
it('tolère les espaces autour', () => {
|
|
expect(parseDisplayToIsoDateTime(' 20/05/2026 14:30 ')).toBe('2026-05-20T14:30:00')
|
|
})
|
|
|
|
it('rejette une date malformée', () => {
|
|
expect(parseDisplayToIsoDateTime('32/01/2026 10:00')).toBeNull()
|
|
expect(parseDisplayToIsoDateTime('10/13/2026 10:00')).toBeNull()
|
|
})
|
|
|
|
it('rejette une heure hors bornes', () => {
|
|
expect(parseDisplayToIsoDateTime('20/05/2026 24:00')).toBeNull()
|
|
expect(parseDisplayToIsoDateTime('20/05/2026 12:60')).toBeNull()
|
|
})
|
|
|
|
it('rejette un format incomplet ou sans heure', () => {
|
|
expect(parseDisplayToIsoDateTime('20/05/2026')).toBeNull()
|
|
expect(parseDisplayToIsoDateTime('20/05/2026 14')).toBeNull()
|
|
expect(parseDisplayToIsoDateTime('')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('composeDateTime', () => {
|
|
it('recompose un datetime ISO avec secondes à 00', () => {
|
|
expect(composeDateTime('2026-05-20', '14:30')).toBe('2026-05-20T14:30:00')
|
|
})
|
|
|
|
it('utilise 00:00 quand l\'heure est vide', () => {
|
|
expect(composeDateTime('2026-05-20', '')).toBe('2026-05-20T00:00:00')
|
|
})
|
|
})
|
|
})
|