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:
2026-06-19 13:04:11 +00:00
committed by Autin
parent 06c739cdc7
commit be3d88ed45
31 changed files with 939 additions and 125 deletions
+43
View File
@@ -0,0 +1,43 @@
import {describe, it, expect} from 'vitest'
import {computeVisibleCount} from './tabFit'
const base = {gap: 60, chevronReserve: 110}
const widths = (n: number, w = 180) => Array.from({length: n}, () => w)
describe('computeVisibleCount', () => {
it('sans layout : respecte maxVisibleTabs', () => {
expect(computeVisibleCount({...base, count: 5, containerWidth: 0, tabWidths: [], maxVisibleTabs: 3})).toBe(3)
})
it('sans layout ni maxVisibleTabs : tous les onglets', () => {
expect(computeVisibleCount({...base, count: 5, containerWidth: 0, tabWidths: []})).toBe(5)
})
it('tout tient : retourne le total (pas de chevrons)', () => {
// 4×180 + 3×60 = 900 <= 1000
expect(computeVisibleCount({...base, count: 4, containerWidth: 1000, tabWidths: widths(4)})).toBe(4)
})
it('trop large : additionne les vraies largeurs (pas la pire) — pas d\'effondrement à 1', () => {
// total 7×180+6×60=1620 > 1400 ; avail=1400-110=1290 ; 180,420,660,900,1140,(1380>1290) → 5
expect(computeVisibleCount({...base, count: 7, containerWidth: 1400, tabWidths: widths(7)})).toBe(5)
})
it('largeur étroite : montre ce qui tient (≥ 2 ici, pas 1)', () => {
// avail=570-110=460 ; 180,(420),(660>460) → 2
expect(computeVisibleCount({...base, count: 7, containerWidth: 570, tabWidths: widths(7)})).toBe(2)
})
it('maxVisibleTabs plafonne le résultat', () => {
expect(computeVisibleCount({...base, count: 7, containerWidth: 1400, tabWidths: widths(7), maxVisibleTabs: 3})).toBe(3)
})
it('au moins 1 onglet si rien ne tient', () => {
expect(computeVisibleCount({...base, count: 5, containerWidth: 150, tabWidths: widths(5, 300)})).toBe(1)
})
it('gère des largeurs hétérogènes', () => {
// total 1080 > 1000 → fenêtré ; avail=1000-110=890 ; 300,560,820,(1080>890) → 3
expect(computeVisibleCount({...base, count: 4, containerWidth: 1000, tabWidths: [300, 200, 200, 200]})).toBe(3)
})
})