fix(date) : borne la saisie clavier pour empêcher les dates absurdes (99/99/9999) (#79)
## Problème Sur la famille Date editable, le masque maska n'imposait que la *forme* (`##/##/####`). Une valeur structurellement absurde comme `99/99/9999` était donc **saisissable**, puis rejetée *a posteriori* par la validation. Le métier veut que ce soit **impossible à taper**. ## Solution (masque borné + validation en filet) - `composables/maskTemplate.ts` — `buildBoundedMask(template)` : borne le **premier chiffre de chaque champ** (jour `0-3`, mois `0-1`, heure `0-2`, minute `0-5`). Distingue le mois des minutes (même lettre `M`) selon la présence d'heures dans le gabarit, pour ne pas brider la saisie des minutes du DateTime. - `internal/CalendarField.vue` — branche le builder dans `maskaOptions` (remplace le `replace(/[A-Za-z]/g, '#')`). Les impossibilités plus fines (`31/02`, 29/02 non bissextile, hors `min`/`max`) restent captées par la **validation** (`invalidMessage` + `update:valid=false`). ## Tests - `maskTemplate.test.ts` (5) — bornes par champ, structure du masque, non-confusion mois/minutes. - `Date.test.ts` — test `invalidMessage` adapté (`32/13/2026`, typable→invalide) + garde de non-régression : `99/99/9999` ne s'inscrit jamais et n'émet aucune date. - Suite complète : **1004/1004 verte** (DateTime 36 incluse → saisie d'heure intacte). Doc : `COMPONENTS.md` (MalioDate) + `CHANGELOG.md` (Fixed) à jour. Reviewed-on: #79 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #79.
This commit is contained in:
@@ -145,14 +145,27 @@ describe('MalioDateTime', () => {
|
||||
it('garde le texte et affiche « Date invalide » sur saisie invalide au blur', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect((input.element as HTMLInputElement).value).toBe('32/13/2026 14:30')
|
||||
expect((input.element as HTMLInputElement).value).toBe('31/02/2026 14:30')
|
||||
expect(input.attributes('aria-invalid')).toBe('true')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
})
|
||||
|
||||
it('empêche la frappe d\'un datetime absurde (99/99/9999 99:99 borné par le masque)', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999 99:99')
|
||||
await input.trigger('blur')
|
||||
// Le masque borne le 1er chiffre de chaque champ (jour 0-3, mois 0-1,
|
||||
// heure 0-2, minute 0-5) : « 9 » est rejeté partout, rien ne s'inscrit
|
||||
// et aucun datetime réel n'est émis.
|
||||
expect((input.element as HTMLInputElement).value).not.toContain('99')
|
||||
const emitted = wrapper.emitted('update:modelValue') ?? []
|
||||
expect(emitted.every(([value]) => value === null)).toBe(true)
|
||||
})
|
||||
|
||||
it('passe en erreur si le datetime saisi est hors min/max', async () => {
|
||||
const wrapper = mountDateTime({editable: true, min: '2026-05-10T00:00:00', max: '2026-05-20T00:00:00'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
@@ -184,7 +197,8 @@ describe('MalioDateTime', () => {
|
||||
it('utilise le message invalidMessage personnalisé', async () => {
|
||||
const wrapper = mountDateTime({editable: true, invalidMessage: 'Format incorrect'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999 10:00')
|
||||
// 31/02 : champs valides mais date inexistante (le masque la laisse passer, la validation la rejette).
|
||||
await input.setValue('31/02/2026 10:00')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Format incorrect')
|
||||
})
|
||||
@@ -192,7 +206,7 @@ describe('MalioDateTime', () => {
|
||||
it('efface l\'erreur de saisie quand modelValue change de l\'extérieur', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
await wrapper.setProps({modelValue: '2026-05-20T14:30:00'})
|
||||
@@ -246,7 +260,7 @@ describe('MalioDateTime', () => {
|
||||
it('émet valid=false sur saisie malformée sans émettre modelValue', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
@@ -276,7 +290,7 @@ describe('MalioDateTime', () => {
|
||||
it('repasse valid=true quand modelValue change de l\'extérieur après une saisie invalide', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
await wrapper.setProps({modelValue: '2026-05-20T14:30:00'})
|
||||
@@ -288,9 +302,9 @@ describe('MalioDateTime', () => {
|
||||
it('émet le texte brut trimmé sur saisie malformée, sans émettre modelValue', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026 14:30'])
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['31/02/2026 14:30'])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -329,9 +343,9 @@ describe('MalioDateTime', () => {
|
||||
it('émet rawValue vide quand on sélectionne une date au calendrier', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.setValue('31/02/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026 14:30'])
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['31/02/2026 14:30'])
|
||||
await input.trigger('focus')
|
||||
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||
|
||||
Reference in New Issue
Block a user