@portnet/ui 6.0.1 → 6.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/v2/__tests__/FieldError.test.js +0 -1
- package/dist/esm/v2/__tests__/FieldError.test.js.map +1 -1
- package/dist/esm/v2/__tests__/FieldWidth.test.js +146 -0
- package/dist/esm/v2/__tests__/FieldWidth.test.js.map +1 -0
- package/dist/esm/v2/__tests__/FileCard.test.js +0 -1
- package/dist/esm/v2/__tests__/FileCard.test.js.map +1 -1
- package/dist/esm/v2/__tests__/FormikCheckbox.test.js +265 -0
- package/dist/esm/v2/__tests__/FormikCheckbox.test.js.map +1 -0
- package/dist/esm/v2/__tests__/FormikDateTimePicker.test.js +248 -0
- package/dist/esm/v2/__tests__/FormikDateTimePicker.test.js.map +1 -0
- package/dist/esm/v2/__tests__/FormikRadioGroup.test.js +380 -0
- package/dist/esm/v2/__tests__/FormikRadioGroup.test.js.map +1 -0
- package/dist/esm/v2/__tests__/MobileRowCard.test.js +0 -1
- package/dist/esm/v2/__tests__/MobileRowCard.test.js.map +1 -1
- package/dist/esm/v2/__tests__/SectionErrorBoundary.test.js.map +1 -1
- package/dist/esm/v2/__tests__/labels-merge.test.js +0 -1
- package/dist/esm/v2/__tests__/labels-merge.test.js.map +1 -1
- package/dist/esm/v2/components/DateTimePicker.js +37 -4
- package/dist/esm/v2/components/DateTimePicker.js.map +1 -1
- package/dist/esm/v2/components/Field.js +63 -1
- package/dist/esm/v2/components/Field.js.map +1 -1
- package/dist/esm/v2/components/index.js +27 -0
- package/dist/esm/v2/components/index.js.map +1 -1
- package/dist/esm/v2/index.js +12 -1
- package/dist/esm/v2/index.js.map +1 -1
- package/dist/esm/v2/patterns/FavoriteColumn.js +14 -61
- package/dist/esm/v2/patterns/FavoriteColumn.js.map +1 -1
- package/dist/esm/v2/patterns/FormikCheckbox.js +140 -0
- package/dist/esm/v2/patterns/FormikCheckbox.js.map +1 -0
- package/dist/esm/v2/patterns/FormikCombobox.js +5 -2
- package/dist/esm/v2/patterns/FormikCombobox.js.map +1 -1
- package/dist/esm/v2/patterns/FormikDatePicker.js +13 -47
- package/dist/esm/v2/patterns/FormikDatePicker.js.map +1 -1
- package/dist/esm/v2/patterns/FormikDateTimePicker.js +150 -0
- package/dist/esm/v2/patterns/FormikDateTimePicker.js.map +1 -0
- package/dist/esm/v2/patterns/FormikInput.js +5 -2
- package/dist/esm/v2/patterns/FormikInput.js.map +1 -1
- package/dist/esm/v2/patterns/FormikRadioGroup.js +141 -0
- package/dist/esm/v2/patterns/FormikRadioGroup.js.map +1 -0
- package/dist/esm/v2/patterns/FormikSelect.js +5 -2
- package/dist/esm/v2/patterns/FormikSelect.js.map +1 -1
- package/dist/esm/v2/patterns/FormikTextarea.js +5 -2
- package/dist/esm/v2/patterns/FormikTextarea.js.map +1 -1
- package/dist/esm/v2/patterns/KeyboardShortcutsHelp.js +25 -8
- package/dist/esm/v2/patterns/KeyboardShortcutsHelp.js.map +1 -1
- package/dist/esm/v2/patterns/RowFavoriteToggle.js +122 -0
- package/dist/esm/v2/patterns/RowFavoriteToggle.js.map +1 -0
- package/dist/esm/v2/patterns/_pickerSerialisation.js +74 -0
- package/dist/esm/v2/patterns/_pickerSerialisation.js.map +1 -0
- package/dist/esm/v2/patterns/index.js +83 -0
- package/dist/esm/v2/patterns/index.js.map +1 -1
- package/dist/esm/v2/personalization/capabilities/gridPreferences.js +209 -0
- package/dist/esm/v2/personalization/capabilities/gridPreferences.js.map +1 -0
- package/dist/esm/v2/personalization/capabilities/index.js +5 -0
- package/dist/esm/v2/personalization/capabilities/index.js.map +1 -1
- package/dist/esm/v2/personalization/index.js +18 -0
- package/dist/esm/v2/personalization/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/v2/__tests__/FieldError.test.js +26 -30
- package/dist/v2/__tests__/FieldError.test.js.map +1 -1
- package/dist/v2/__tests__/FieldWidth.test.js +150 -0
- package/dist/v2/__tests__/FieldWidth.test.js.map +1 -0
- package/dist/v2/__tests__/FileCard.test.js +25 -29
- package/dist/v2/__tests__/FileCard.test.js.map +1 -1
- package/dist/v2/__tests__/FormikCheckbox.test.js +268 -0
- package/dist/v2/__tests__/FormikCheckbox.test.js.map +1 -0
- package/dist/v2/__tests__/FormikDateTimePicker.test.js +247 -0
- package/dist/v2/__tests__/FormikDateTimePicker.test.js.map +1 -0
- package/dist/v2/__tests__/FormikRadioGroup.test.js +381 -0
- package/dist/v2/__tests__/FormikRadioGroup.test.js.map +1 -0
- package/dist/v2/__tests__/MobileRowCard.test.js +52 -56
- package/dist/v2/__tests__/MobileRowCard.test.js.map +1 -1
- package/dist/v2/__tests__/SectionErrorBoundary.test.js.map +1 -1
- package/dist/v2/__tests__/labels-merge.test.js +20 -24
- package/dist/v2/__tests__/labels-merge.test.js.map +1 -1
- package/dist/v2/components/DateTimePicker.d.ts.map +1 -1
- package/dist/v2/components/DateTimePicker.js +37 -4
- package/dist/v2/components/DateTimePicker.js.map +1 -1
- package/dist/v2/components/Field.d.ts +59 -1
- package/dist/v2/components/Field.d.ts.map +1 -1
- package/dist/v2/components/Field.js +62 -2
- package/dist/v2/components/Field.js.map +1 -1
- package/dist/v2/components/index.d.ts +27 -0
- package/dist/v2/components/index.d.ts.map +1 -1
- package/dist/v2/components/index.js.map +1 -1
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/index.js.map +1 -1
- package/dist/v2/patterns/FavoriteColumn.d.ts +9 -9
- package/dist/v2/patterns/FavoriteColumn.d.ts.map +1 -1
- package/dist/v2/patterns/FavoriteColumn.js +14 -61
- package/dist/v2/patterns/FavoriteColumn.js.map +1 -1
- package/dist/v2/patterns/FormikCheckbox.d.ts +77 -0
- package/dist/v2/patterns/FormikCheckbox.d.ts.map +1 -0
- package/dist/v2/patterns/FormikCheckbox.js +147 -0
- package/dist/v2/patterns/FormikCheckbox.js.map +1 -0
- package/dist/v2/patterns/FormikCombobox.d.ts +7 -1
- package/dist/v2/patterns/FormikCombobox.d.ts.map +1 -1
- package/dist/v2/patterns/FormikCombobox.js +5 -2
- package/dist/v2/patterns/FormikCombobox.js.map +1 -1
- package/dist/v2/patterns/FormikDatePicker.d.ts +13 -1
- package/dist/v2/patterns/FormikDatePicker.d.ts.map +1 -1
- package/dist/v2/patterns/FormikDatePicker.js +33 -66
- package/dist/v2/patterns/FormikDatePicker.js.map +1 -1
- package/dist/v2/patterns/FormikDateTimePicker.d.ts +111 -0
- package/dist/v2/patterns/FormikDateTimePicker.d.ts.map +1 -0
- package/dist/v2/patterns/FormikDateTimePicker.js +158 -0
- package/dist/v2/patterns/FormikDateTimePicker.js.map +1 -0
- package/dist/v2/patterns/FormikInput.d.ts +11 -1
- package/dist/v2/patterns/FormikInput.d.ts.map +1 -1
- package/dist/v2/patterns/FormikInput.js +5 -2
- package/dist/v2/patterns/FormikInput.js.map +1 -1
- package/dist/v2/patterns/FormikRadioGroup.d.ts +80 -0
- package/dist/v2/patterns/FormikRadioGroup.d.ts.map +1 -0
- package/dist/v2/patterns/FormikRadioGroup.js +148 -0
- package/dist/v2/patterns/FormikRadioGroup.js.map +1 -0
- package/dist/v2/patterns/FormikSelect.d.ts +9 -1
- package/dist/v2/patterns/FormikSelect.d.ts.map +1 -1
- package/dist/v2/patterns/FormikSelect.js +5 -2
- package/dist/v2/patterns/FormikSelect.js.map +1 -1
- package/dist/v2/patterns/FormikTextarea.d.ts +9 -1
- package/dist/v2/patterns/FormikTextarea.d.ts.map +1 -1
- package/dist/v2/patterns/FormikTextarea.js +5 -2
- package/dist/v2/patterns/FormikTextarea.js.map +1 -1
- package/dist/v2/patterns/KeyboardShortcutsHelp.d.ts +14 -1
- package/dist/v2/patterns/KeyboardShortcutsHelp.d.ts.map +1 -1
- package/dist/v2/patterns/KeyboardShortcutsHelp.js +25 -8
- package/dist/v2/patterns/KeyboardShortcutsHelp.js.map +1 -1
- package/dist/v2/patterns/RowFavoriteToggle.d.ts +92 -0
- package/dist/v2/patterns/RowFavoriteToggle.d.ts.map +1 -0
- package/dist/v2/patterns/RowFavoriteToggle.js +127 -0
- package/dist/v2/patterns/RowFavoriteToggle.js.map +1 -0
- package/dist/v2/patterns/_pickerSerialisation.d.ts +44 -0
- package/dist/v2/patterns/_pickerSerialisation.d.ts.map +1 -0
- package/dist/v2/patterns/_pickerSerialisation.js +81 -0
- package/dist/v2/patterns/_pickerSerialisation.js.map +1 -0
- package/dist/v2/patterns/index.d.ts +87 -0
- package/dist/v2/patterns/index.d.ts.map +1 -1
- package/dist/v2/patterns/index.js +34 -0
- package/dist/v2/patterns/index.js.map +1 -1
- package/dist/v2/personalization/capabilities/gridPreferences.d.ts +100 -0
- package/dist/v2/personalization/capabilities/gridPreferences.d.ts.map +1 -0
- package/dist/v2/personalization/capabilities/gridPreferences.js +215 -0
- package/dist/v2/personalization/capabilities/gridPreferences.js.map +1 -0
- package/dist/v2/personalization/capabilities/index.d.ts +6 -0
- package/dist/v2/personalization/capabilities/index.d.ts.map +1 -1
- package/dist/v2/personalization/capabilities/index.js +13 -0
- package/dist/v2/personalization/capabilities/index.js.map +1 -1
- package/dist/v2/personalization/index.d.ts +18 -0
- package/dist/v2/personalization/index.d.ts.map +1 -1
- package/dist/v2/personalization/index.js +13 -0
- package/dist/v2/personalization/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,91 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/
|
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- **`Field.width` — les cinq gabarits de largeur semantique du standard (G-H).**
|
|
14
|
+
|
|
15
|
+
La section « Form Field Width & Layout Standards » des Platform Mandatory UX Patterns impose de rattacher chaque champ a l'un de cinq gabarits d'apres sa contrainte de CARACTERES (`xs` 1-5, `s` 6-15, `m` 16-60, `l` 61-256, `xl` au-dela), et y interdit explicitement les « arbitrary input field widths » et les « jagged alignment fields within forms ».
|
|
16
|
+
|
|
17
|
+
**La library n'avait rien pour l'exprimer.** `FormGrid` raisonne en COLONNES (1/2/3/4, ou `minColumnWidth`), `FormGrid.Item span` en colonnes, et `Input` n'expose ni largeur ni taille. Un module voulant s'y conformer n'avait donc que le `sx` — c'est-a-dire precisement les « local UI tokens » que le meme document interdit. Releve depuis `refonte-frontend` (lot 8), ou l'audit de conformite a constate que la section entiere n'avait pas pu etre appliquee.
|
|
18
|
+
|
|
19
|
+
**Sur `Field`, et non sur chaque controle.** Le gabarit est une contrainte de MISE EN PAGE, pas une propriete de l'input : c'est la boite du champ — libelle, controle, indication, erreur — qui doit etre bornee, sinon le message d'erreur s'etale sur toute la cellule pendant que l'input est borne. `Field` est le seul endroit qui voit cette boite entiere.
|
|
20
|
+
|
|
21
|
+
**Un plafond, et seulement a partir de `sm`.** Le standard exige aussi l'empilement a 100 % de la largeur de grille sur mobile, et l'alignement des champs heterogenes sur des fractions de colonnes. Avec un `maxWidth` pose au palier tablette, la grille continue de decider des COLONNES et le gabarit borne le champ DANS sa cellule : les deux regles coexistent au lieu de se contredire.
|
|
22
|
+
|
|
23
|
+
**Les cinq valeurs sont une decision de design, pas une lecture.** Le standard donne des fourchettes (« ~80px to 120px max »). Le HAUT de chacune a ete retenu, ces largeurs etant des plafonds : prendre le bas tronquerait des valeurs qui tiennent dans la contrainte de caracteres annoncee. Elles passent par des variables CSS (`--field-w-xs` a `--field-w-l`) pour etre revues sans modification de code, et `FIELD_MAX_WIDTH` est exporte pour que le test les epingle — le plafond etant pose derriere une requete media, jsdom ne peut pas lire la largeur effective, seule la story le montre.
|
|
24
|
+
|
|
25
|
+
Relaye par `FormikInput`, `FormikTextarea`, `FormikSelect`, `FormikCombobox`, `FormikDatePicker` et `FormikDateTimePicker`, chacun documentant le gabarit que le standard associe a SON type de controle (les « simple dropdowns » en `m`, les « long dropdowns » et les « complex search auto-completes » en `l`, les dates en `s`). `FormikReferentielField` ne le relaie pas encore : une prop non declaree echoue a la compilation, donc rien ne se perd en silence.
|
|
26
|
+
|
|
27
|
+
Retrocompatible : sans `width`, aucun plafond n'est pose.
|
|
28
|
+
- **`FormikDateTimePicker` — le pont date + HEURE qui manquait (G-E).**
|
|
29
|
+
|
|
30
|
+
`FormikDatePicker` etait le SEUL pont de date de la library, et il est date-seule. Sa propre note d'audit le disait : « DateTimePicker, TimePicker and DateRangePicker are exposed as bare components ». Un hote ayant besoin de l'heure n'avait donc que deux options : la perdre, ou recabler `useField` + le `DateTimePicker` nu.
|
|
31
|
+
|
|
32
|
+
**Perdre l'heure n'est pas un compromis d'affichage : ca change ce que recoit le backend.** Releve depuis `refonte-frontend` (lot 7). Le formulaire de visite physique stocke une date de visite portant une heure et soumet `moment(valeur).format("YYYY-MM-DD HH:mm")`. Passe par le pont date-seule, toute visite aurait ete programmee a 00:00 — silencieusement, sans erreur nulle part, et seul l'agent se presentant a la mauvaise heure l'aurait fini par decouvrir. Le test unitaire epingle les deux comportements cote a cote, pour que la raison d'etre des deux ponts reste lisible.
|
|
33
|
+
|
|
34
|
+
Et recabler a la main, c'est la duplication interdite par R-2.1, dans la forme ou les messages de validation disparaissent — meme histoire que G-A et G-C.
|
|
35
|
+
|
|
36
|
+
**Au passage, `coerceToDate` / `serialiseDate` sortent en module interne partage** (`patterns/_pickerSerialisation`), consommees par les DEUX ponts. Aucun changement de comportement : le code est deplace, pas modifie.
|
|
37
|
+
|
|
38
|
+
Ce n'est pas du rangement. La note S-03 de `FormikDatePicker` documente ce qui arrive quand la LECTURE et l'ECRITURE d'une date divergent : un « 31-12-2026 » relu par `new Date()` devient Invalid Date, un « 05-03-2026 » devient le 3 mai au lieu du 5 mars. Le chemin d'ecriture fonctionne, donc le degat n'apparait qu'a la RELECTURE — en modification, le parcours qu'un test de fumee n'exerce jamais. Un module a livre ce defaut pendant sept lots sous un typecheck strict. Deux copies privees de ce couple auraient recree la meme classe de bug, sous une forme encore moins visible : deux ponts qui se ressemblent, que personne ne compare, et qui ont l'air corrects isolement.
|
|
39
|
+
|
|
40
|
+
- **`FormikRadioGroup` — le pont qui manquait pour un groupe de radios (G-C).**
|
|
41
|
+
|
|
42
|
+
R-Sel.2 est un MUST : 2-3 options -> Radio ou Segmented, jamais un Select. Toute question metier binaire passe donc obligatoirement par `RadioGroup` — qui n'avait aucun pont Formik, la ou `FormikSelect`, `FormikCombobox`, `FormikCheckboxGroup`, `FormikDatePicker`, `FormikTextarea`, `FormikFile` et `FormikInput` en ont tous un. Les hotes recablaient donc `useField` + `RadioGroup` a la main, avec la meme consequence que pour G-A : le controle est rendu, `FieldError` est oublie. Releve depuis `refonte-frontend` (lot 5), ou UN formulaire portait deux de ces groupes.
|
|
43
|
+
|
|
44
|
+
**Le contrat qui compte est invisible : la valeur ecrite est celle DECLAREE dans `options`, pas la chaine que rapporte le DOM.** Un `<input type="radio">` ne rapporte jamais qu'une chaine. Un hote dont les options sont numeriques — `{ value: 1 }`, `{ value: 2 }` — et dont le payload declare un nombre se mettrait a envoyer `"1"` des le premier clic. L'API l'accepte, ou le refuse trois jours plus tard ; rien dans l'interface ne le signale. Le pont reapparie donc la chaine du DOM contre la liste d'options et ecrit la valeur d'origine. La chaine brute ne passe qu'a defaut de correspondance — perdre le clic de l'utilisateur serait pire.
|
|
45
|
+
|
|
46
|
+
Symetriquement, la SELECTION se compare en chaine : un champ portant le booleen `false` selectionne l'option declaree `"false"`, un champ portant `2` selectionne `{ value: 2 }`. C'est le cas des hotes qui initialisent en booleen et transmettent du texte au backend. La valeur STOCKEE n'est jamais reecrite par cette comparaison — un formulaire ouvert puis ferme sans reponse transmet exactement ce dont il a ete amorce.
|
|
47
|
+
|
|
48
|
+
Le champ est marque touche AU CLIC : pour un groupe de radios, la premiere interaction EST la reponse, il n'y a pas de blur-avant-changement. Attendre un blur laisserait le message d'un groupe requis cache pendant que l'utilisateur clique.
|
|
49
|
+
|
|
50
|
+
`required` est porte par le CHAMP et non par les options : `Field` rend l'asterisque sur le libelle du groupe et injecte `aria-required`, valide sur `role="radiogroup"` — contrairement au cas de la case a cocher seule (G-A), ou l'injection atterrissait sur un conteneur qui ne la transmet pas.
|
|
51
|
+
|
|
52
|
+
- **`FormikCheckbox` — le pont qui manquait pour une case a cocher BOOLEENNE (G-A).**
|
|
53
|
+
|
|
54
|
+
`FormikCheckboxGroup` existait deja, et il porte un TABLEAU de valeurs selectionnees. C'est le bon modele pour « choisir plusieurs dans une liste », et le mauvais pour un drapeau seul. Un hote dont l'etat est `autreInfo.fabrication: boolean` ou `objectDeclaration.premiereDeclaration: boolean` ne peut pas l'adopter sans changer la forme de ce que son payload envoie au backend.
|
|
55
|
+
|
|
56
|
+
**Consequence observee** : faute de pont, les modules recablaient `Checkbox` + `useField` a la main. C'est la duplication que R-2.1 interdit, et elle a coute exactement ce que coutent les ponts artisanaux — le rendu de l'erreur. Releve depuis `refonte-frontend` (lot 5) : ses champs cables main n'affichaient AUCUN message de validation, si bien qu'une erreur sur un champ requis etait invisible pour l'utilisateur (R-4.1 MUST). Le defaut ne se voit ni a la relecture ni sur une capture : les pixels sont corrects, seul le message manque.
|
|
57
|
+
|
|
58
|
+
Trois details du contrat valent d'etre notes, parce qu'ils ne sont pas devinables :
|
|
59
|
+
|
|
60
|
+
La case est enveloppee dans un `CheckboxGroup`, comme le fait `FormikCheckboxGroup`, et pour la meme raison : `Field` clone son enfant avec `error` / `disabled`, que `MuiCheckbox` propagerait jusqu'a l'`<input>` natif (avertissement React, attribut DOM invalide). `CheckboxGroup` destructure ses props explicitement et absorbe le reste.
|
|
61
|
+
|
|
62
|
+
Le libelle nomme la CASE, via `FormControlLabel`, et non le champ : une case seule portant un libelle de champ ET un libelle de controle se lit comme deux questions. `Field` ne recoit donc pas de `label`, et `required` s'exprime la ou il compte — asterisque sur le libelle du controle, `aria-required` sur l'`<input>` (l'injection de `Field` atterrit sur le groupe, qui ne la transmet pas).
|
|
63
|
+
|
|
64
|
+
Le champ est marque touche AU CHANGEMENT, pas au blur. Une case n'a pas de blur-avant-changement significatif : attendre un blur garderait le message cache quel que soit le nombre de clics.
|
|
65
|
+
|
|
66
|
+
`checked` compare a `true` explicitement : les hotes initialisent ces drapeaux a `null` (« pas encore repondu »), et `checked={null}` rend l'input non controle le temps d'un rendu.
|
|
67
|
+
|
|
68
|
+
- **`RowFavoriteToggle` — the follow star, reachable outside a DataGrid column (G-36).**
|
|
69
|
+
|
|
70
|
+
This component already existed. It was `FavoriteCell`, private inside `makeFavoriteColumn`, which made the follow control reachable at exactly one composition point: as a DataGrid column definition.
|
|
71
|
+
|
|
72
|
+
The other place a row always appears is the mobile card. `DataGrid` swaps to `renderMobileCard` below its `mobileBreakpoint`, and `MobileRowCard` has a `leading` slot for precisely this kind of per-row control — but a column definition cannot be rendered into a slot, so every host re-derived the star by hand. `onssa-export-fe` carried a local `MobileFavoriteToggle`; `ms-ti-fe` destructures the same `MobileRowCard` + `FavoriteToggle` + `useIsFavorite` trio for the same reason.
|
|
73
|
+
|
|
74
|
+
**Same failure mode as G-10**, one level down: the library owned the behaviour but exposed it at a single composition point, so consumers reimplemented it at the others.
|
|
75
|
+
|
|
76
|
+
**And the copy drifted exactly where drift is hardest to notice.** The local twin announced itself as "Ajouter aux favoris" / "Retirer des favoris" while the grid column *in the same list* announced "Suivre" / "Ne plus suivre", and it omitted the status tooltip entirely. One affordance, one list, two accessible names — selected by viewport width. R-2.1 MUST is "un même concept = un même composant + un même libellé partout"; a sighted user could not have seen this, and a screen-reader user met a different control on a phone than on a desktop.
|
|
77
|
+
|
|
78
|
+
`makeFavoriteColumn` now renders this component rather than a private twin, so the column and the card agree **by construction instead of by review**. `FavoriteColumnLabels` extends `RowFavoriteToggleLabels` and the column's defaults spread the shared ones, so the label set has one definition too.
|
|
79
|
+
|
|
80
|
+
Behaviour is carried over unchanged: action-oriented accessible name with `aria-pressed` for state, visible status on hover/focus via `Tooltip`, and click/keydown propagation stopped so toggling never fires the row's `onRowClick`. No change for existing `makeFavoriteColumn` callers.
|
|
81
|
+
|
|
82
|
+
- **`useGridPreferences` — per-user hidden columns, because a bare `localStorage` key is not a user (G-35).**
|
|
83
|
+
|
|
84
|
+
`DataGridToolbar` owns the column-visibility UI (`hiddenColumns` + `onHiddenColumnsChange`) but deliberately holds no state, so *persisting* the choice was left to the host. Every listing screen therefore reached for raw `localStorage` — `onssa-export-fe` had `"onssaCertificates:listing:hiddenColumns"`, hand-written `JSON.parse` included.
|
|
85
|
+
|
|
86
|
+
**The defect is not the duplication, it is the key.** This foundation partitions every capability as `portnet-ui:perso:v1:{tenantId?}:{userId}:{capabilityId}`; a host-invented key has no user segment. Favorites and saved filters were already immune because they route through the store — column visibility was the one grid preference that bypassed it. On a shared workstation, which is the norm at a port, user B inherited user A's hidden columns: a column A had hidden was simply absent for B, with nothing on screen to explain why. A preference silently became a data-visibility rule for someone else.
|
|
87
|
+
|
|
88
|
+
**This could not be fixed in a host, and that is a second finding.** `registerCapability` is exported as a "capability registration seam — for custom/host-defined capabilities", but the two primitives every capability hook needs, `usePersonalizationStore` and `useStoreSelector`, are private by design (architecture doc §2.8, minimum API surface). A host could register a descriptor and then had no way to read or write its payload. The seam is advertised without being complete. Completing it was rejected as the fix here: it would widen a certified library's internal surface *and* leave every module re-authoring the same slice. Owning the slice does neither — but the incomplete seam should either be finished or stop advertising itself.
|
|
89
|
+
|
|
90
|
+
Payload is `{ [resourceKey]: { hiddenColumns: string[] } }`, with the entry object rather than a bare array so column order and widths can join it later without a `schemaVersion` bump. The pure helpers rebuild only the touched `resourceKey` and shallow-copy the map, so structural sharing keeps every other resource referentially stable; the exposed `Set` is memoised on the stored array so its identity survives re-renders and does not defeat `DataGrid`'s memoisation.
|
|
91
|
+
|
|
92
|
+
`migrate` normalises on load even though v1 has no predecessor version — it is the trust boundary for a payload coming back from storage, and a column key is used to *hide* grid columns, so a number or an object from an older build (or from hand-edited devtools storage) must not reach the grid as if it were one. This absorbs, once, the validation each host was writing for itself.
|
|
93
|
+
|
|
94
|
+
Density is deliberately **not** part of this: `useDensity` owns it as a global, non-resource-scoped value and applies `data-density` to `<html>`, and folding it in would give one preference two owners.
|
|
95
|
+
|
|
96
|
+
**Known related debt, not addressed here.** `useDensity` (`portnet-ui-density`) and `useFormAutoSave` (host-supplied `storageKey`) both persist outside this foundation on unscoped keys, for the same reason G-35 existed. `useFormAutoSave` is the more serious of the two — a draft is keyed by resource id only, so on a shared workstation one user is offered another's unsubmitted form, business data included. Neither is fixed here: `storageKey` is a required public prop, so scoping it is a signature change (MAJOR under §10.1) affecting every module with a create form, and moving density into the foundation would change its storage key and make it require a provider. Both need their own RFC.
|
|
97
|
+
|
|
13
98
|
- **`SkeletonPageShell` — the route-level `Suspense` fallback R-P.1 requires (G-02).**
|
|
14
99
|
|
|
15
100
|
R-P.1 MUST says "routing avec `React.lazy` + `Suspense`, **fallback skeleton page**". The library shipped every skeleton *part* — `Skeleton`, `SkeletonRow`, `SkeletonKPI`, `SkeletonDetailHero`, `SkeletonTabContent`, `SkeletonText` — plus `PageBusy`, and yet nothing that satisfies the rule: `PageBusy` is a full-viewport spinner, which R-8.1 MUST forbids for navigation, and the parts are shaped for the inside of a page rather than the page frame.
|
package/dist/esm/index.js
CHANGED
|
@@ -147,6 +147,24 @@ export { MODAL_POPPER_PROPS, MODAL_ZINDEX } from "./v2/modalContext";
|
|
|
147
147
|
// internally for the same reason.
|
|
148
148
|
export { UserPersonalizationProvider, createLocalStorageAdapter, createMemoryAdapter, registerCapability, useFavorites, useIsFavorite, useFavoritesView, filterRowsByFavorites, sortRowsByFavorites, FAVORITES_FILTER, FAVORITES_SORT, useSavedFilters } from "./v2/personalization";
|
|
149
149
|
|
|
150
|
+
// Grid preferences capability (gap G-35) — per-user, per-resource hidden
|
|
151
|
+
// columns. FLAT-ONLY, per the policy note on `v2` at the bottom of this file:
|
|
152
|
+
// deliberately NOT added to ./v2/index.ts.
|
|
153
|
+
//
|
|
154
|
+
// DataGridToolbar owns the column-visibility UI but holds no state, so hosts
|
|
155
|
+
// persisted it themselves in a bare localStorage key — not user-scoped, unlike
|
|
156
|
+
// this foundation's `…:{userId}:{capabilityId}` scheme, so on a shared
|
|
157
|
+
// workstation one user inherited another's hidden columns. Observed in
|
|
158
|
+
// onssa-export-fe ("onssaCertificates:listing:hiddenColumns").
|
|
159
|
+
//
|
|
160
|
+
// It had to land in the library rather than the host: `registerCapability` is
|
|
161
|
+
// exported above as a seam "for custom/host-defined capabilities", but the two
|
|
162
|
+
// primitives a capability hook needs (usePersonalizationStore,
|
|
163
|
+
// useStoreSelector) are private by design (§2.8, minimum API surface), so a
|
|
164
|
+
// host-registered capability could be declared and never read or written.
|
|
165
|
+
// Completing that seam instead would widen the internal surface AND leave every
|
|
166
|
+
// module re-authoring the same slice; owning the slice here does neither.
|
|
167
|
+
export { useGridPreferences, GRID_PREFERENCES_CAPABILITY_ID } from "./v2/personalization";
|
|
150
168
|
// Referentiel data layer runtime API (config provider + hooks). Backs
|
|
151
169
|
// ReferentielField / FormikReferentielField, which are flat-exported
|
|
152
170
|
// above via ./v2/patterns - the field UI was reachable while the data
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["createPortnetTheme","getCssVariables","injectCssVariables","removeCssVariables","cx","fmtMoney","contrast","downloadBase64File","getMimeType","openBase64FileInTab","downloadBlobFile","getApiErrorMessage","getApiErrorDetail","getApiErrorStatus","DEFAULT_API_ERROR_MESSAGE","networkStatus","createNetworkStatus","default","SkeletonPageShell","MODAL_POPPER_PROPS","MODAL_ZINDEX","UserPersonalizationProvider","createLocalStorageAdapter","createMemoryAdapter","registerCapability","useFavorites","useIsFavorite","useFavoritesView","filterRowsByFavorites","sortRowsByFavorites","FAVORITES_FILTER","FAVORITES_SORT","useSavedFilters","ReferentielProvider","ReferentielContext","useReferentielClient","useReferentielOptions","useReferentielSearch","_v","v2"],"sources":["../../src/lib/index.ts"],"sourcesContent":["/**\r\n * @portnet/ui — root entry (MUI v9)\r\n *\r\n * The v1 Pui* surface was REMOVED in 0.3.0-mui9. The package now\r\n * exposes the modernised v2 Design System directly at its root.\r\n *\r\n * Imports\r\n * -------\r\n *\r\n * // Components (flat — preferred)\r\n * import { Button, Modal, FormGrid, Combobox, ... } from \"@portnet/ui\";\r\n *\r\n * // Theme + CSS variables + tokens + hooks (flat, or subpath entrypoints)\r\n * import { createPortnetTheme, injectCssVariables } from \"@portnet/ui\";\r\n * import { palette, spacing } from \"@portnet/ui/tokens\";\r\n * import { useFocusTrap } from \"@portnet/ui/hooks\";\r\n *\r\n * // Misc utils (flat) — class names, money formatting, WCAG contrast,\r\n * // and base64 → browser download\r\n * import { cx, fmtMoney, contrast, downloadBase64File } from \"@portnet/ui\";\r\n *\r\n * // Modal layering constants (flat)\r\n * import { MODAL_POPPER_PROPS, MODAL_ZINDEX } from \"@portnet/ui\";\r\n *\r\n * // Personalization foundation (flat, or \"@portnet/ui/personalization\")\r\n * import { UserPersonalizationProvider, createLocalStorageAdapter } from \"@portnet/ui\";\r\n *\r\n * // Referentiel data layer (flat) — backs ReferentielField /\r\n * // FormikReferentielField. Mount the provider once at the app root.\r\n * import { ReferentielProvider, useReferentielSearch } from \"@portnet/ui\";\r\n *\r\n * // Domain models + UI types (flat, type-only)\r\n * import type { FavoriteItem, SavedFilter, GridColumn } from \"@portnet/ui\";\r\n *\r\n * // Legacy `@portnet/ui/v2/*` paths still resolve transitionally\r\n * // through the v2 re-export below, deprecated and scheduled for\r\n * // removal in 0.4.0. NOTE: capability added after that deprecation\r\n * // is surfaced flat ONLY and is deliberately absent from `v2.*`.\r\n *\r\n * No v1 surface remains. Consuming modules that still import any\r\n * Pui* component will fail to resolve and must migrate per the\r\n * table in docs/migration-mui9/03-batch3-purge-v1.md.\r\n */\r\n\r\n// Flat re-export of every v2 component, pattern, hook, and the\r\n// theme factory. This is what consuming modules pin against\r\n// (`import { Button } from \"@portnet/ui\"`).\r\nexport * from \"./v2/components\";\r\nexport * from \"./v2/patterns\";\r\nexport * from \"./v2/hooks\";\r\nexport {\r\n createPortnetTheme,\r\n getCssVariables,\r\n injectCssVariables,\r\n removeCssVariables,\r\n} from \"./v2/theme\";\r\n\r\n// Theme factory typed contract, flat + type-only (capability gap G-33).\r\n//\r\n// The theme VALUES were flat-exported above while the types describing their\r\n// options were reachable ONLY from the deprecated `@portnet/ui/v2` subpath.\r\n// Every host module wraps createPortnetTheme in a local `build<Module>Theme`\r\n// helper (see the GU modules' utils/appearance), so every host needs this\r\n// contract to type its own wrapper — and had exactly two ways to get it:\r\n// import from a surface scheduled for removal in 0.4.0, or fall back to\r\n// `brand?: object` + `as Parameters<typeof createPortnetTheme>[0]`, both on\r\n// the Platform TypeScript Guidelines FORBIDDEN list. ./v2/theme's own doc\r\n// comment already names that anti-pattern as the reason these types were\r\n// published; it just never reached the surface consumers are told to migrate\r\n// TO. Closing that here removes the last reason a module needs `@portnet/ui/v2`\r\n// for the theme layer.\r\n//\r\n// Flat-only, per the `v2` policy note at the bottom of this file.\r\n// Purely additive and type-only; nothing existing changed.\r\nexport type {\r\n CreatePortnetThemeOptions,\r\n BrandTokens,\r\n BrandSecondaryTokens,\r\n} from \"./v2/theme\";\r\n\r\n// Added while migrating ms-dfd-fe to TypeScript: cx / fmtMoney / contrast had\r\n// no flat or subpath route, only the deprecated `v2.*` namespace below.\r\n// downloadBase64File + getMimeType were added later (mjcc-fe Area 5) and are\r\n// flat-only by policy — see the note on `v2` at the bottom of this file.\r\n// Purely additive re-exports; nothing existing changed.\r\n//\r\n// G-34 (caution-ui Area 6) — openBase64FileInTab + downloadBlobFile added.\r\n// These two were NOT flat-only additions: ./v2/index.ts has exported them all\r\n// along, so the FLAT ROOT — the surface the policy at the bottom of this file\r\n// tells every module to migrate TO — was a strict SUBSET of the deprecated\r\n// surface it replaces. A module obeying the migration policy lost access to\r\n// both, which is how caution-ui's Area 6 build broke:\r\n//\r\n// Attempted import error: 'downloadBlobFile' is not exported\r\n// from '@portnet/ui'\r\n//\r\n// Root cause is mechanical, and worth fixing properly rather than name by name:\r\n// ./v2/components, ./v2/patterns and ./v2/hooks are re-exported above with\r\n// `export *`, so anything added to them surfaces automatically. ./v2/utils is\r\n// re-exported by EXPLICIT NAME LIST, so every new util silently fails to reach\r\n// consumers until someone hits a build error. A wildcard is not a safe swap\r\n// here — ./v2/utils also carries `mergeLabels`, an internal label-merging\r\n// helper with no host use case, and widening a certified public surface by\r\n// accident is worse than the drift. The list stays, and the invariant is stated\r\n// instead: ANY addition to ./v2/utils intended for hosts must be added here in\r\n// the same commit.\r\nexport {\r\n cx,\r\n fmtMoney,\r\n contrast,\r\n downloadBase64File,\r\n getMimeType,\r\n openBase64FileInTab,\r\n downloadBlobFile,\r\n} from \"./v2/utils\";\r\n\r\n// Error narrowing + the network-status channel. Flat-only per the `v2` policy\r\n// note at the bottom of this file. Both were added from the\r\n// procuration-portnet-fe Area 5 gap audit (G-04, G-03):\r\n//\r\n// - getApiErrorMessage / getApiErrorStatus close the hole between two rules\r\n// the platform imposes together — `useUnknownInCatchVariables` is mandatory\r\n// and `any` / unsafe assertions are forbidden, with nothing sanctioned in\r\n// between. Every audited module had bridged it with\r\n// `error as { response?: { data?: { message?: unknown } } }`.\r\n//\r\n// - networkStatus is the feed for NetworkErrorBanner, which shipped as a\r\n// presentational component with its trigger contract left to consumers.\r\n//\r\n// G-34 — getApiErrorDetail added. Its own docstring PRESCRIBES a pairing that\r\n// was impossible to write, because the companion was never surfaced:\r\n//\r\n// toast(getApiErrorMessage(err, \"Échec de l'envoi.\"), \"error\");\r\n// console.error(\"[submit]\", getApiErrorStatus(err), getApiErrorDetail(err));\r\n//\r\n// The split exists because getApiErrorMessage deliberately SUPPRESSES transport\r\n// phrasings (G-07) — right for a toast, wrong for a console or an APM\r\n// breadcrumb, where \"Request failed with status code 502\" is exactly what an\r\n// operator needs. Without the escape hatch a host either logs the sanitised\r\n// string (losing the diagnostic) or re-reads `error.message` by hand with an\r\n// assertion (the very thing G-04 exists to prevent).\r\n//\r\n// Purely additive; nothing existing changed.\r\nexport {\r\n getApiErrorMessage,\r\n getApiErrorDetail,\r\n getApiErrorStatus,\r\n DEFAULT_API_ERROR_MESSAGE,\r\n networkStatus,\r\n createNetworkStatus,\r\n} from \"./v2/utils\";\r\nexport type { NetworkStatus, NetworkStatusListener } from \"./v2/utils\";\r\n\r\n// Route-level Suspense fallback. R-P.1 MUST requires a \"fallback skeleton page\",\r\n// and the library had every skeleton PART plus PageBusy, but nothing satisfying\r\n// the rule: PageBusy is a full-screen spinner, which R-8.1 MUST forbids for\r\n// navigation. So every module hand-built one, and to avoid the CLS a skeleton\r\n// exists to prevent it had to copy ActionBar's and PageContent's padding\r\n// literals out of this library. Owning it here keeps the three in lockstep.\r\n// Capability gap G-02 (procuration-portnet-fe Area 5). Flat-only, per the policy\r\n// note below.\r\nexport { default as SkeletonPageShell } from \"./v2/patterns/SkeletonPageShell\";\r\nexport type {\r\n SkeletonPageShellProps,\r\n SkeletonPageShellVariant,\r\n} from \"./v2/patterns/SkeletonPageShell\";\r\n\r\n// Modal/Drawer/Combobox/DatePicker layering constants - needed by any\r\n// consumer stacking a popover-ish component (Combobox, DateTimePicker)\r\n// above a Modal or Drawer. Was v2-namespace-only.\r\nexport { MODAL_POPPER_PROPS, MODAL_ZINDEX } from \"./v2/modalContext\";\r\n\r\n// Personalization foundation runtime API (favorites, saved filters).\r\n// Named re-export, not a wildcard: ./v2/personalization also carries\r\n// the Scope/FavoriteRef/SavedFilter model types for its own internal\r\n// use, which are already flat-exported canonically via ./v2/models\r\n// below - a wildcard here would re-export them a second time and\r\n// collide (TS2308). Mirrors exactly what ./v2/index.ts already does\r\n// internally for the same reason.\r\nexport {\r\n UserPersonalizationProvider,\r\n createLocalStorageAdapter,\r\n createMemoryAdapter,\r\n registerCapability,\r\n useFavorites,\r\n useIsFavorite,\r\n useFavoritesView,\r\n filterRowsByFavorites,\r\n sortRowsByFavorites,\r\n FAVORITES_FILTER,\r\n FAVORITES_SORT,\r\n useSavedFilters,\r\n} from \"./v2/personalization\";\r\n\r\n// Referentiel data layer runtime API (config provider + hooks). Backs\r\n// ReferentielField / FormikReferentielField, which are flat-exported\r\n// above via ./v2/patterns - the field UI was reachable while the data\r\n// layer that feeds it was not, so consumers had to either import from\r\n// the deprecated `@portnet/ui/v2` surface or rebuild the client\r\n// locally. Added while auditing mjcc-fe, which had done the latter.\r\n//\r\n// Named re-export, not a wildcard, for the SAME reason as\r\n// personalization above: ./v2/referentiel also re-exports\r\n// ReferentielSearchArgs, ReferentielSearchResult and\r\n// ReferentielSearchFn for its own use, and those names are already\r\n// flat-exported canonically through ./v2/models below. A wildcard\r\n// would export them twice and collide (TS2308). The type layer keeps\r\n// its single canonical route.\r\nexport {\r\n ReferentielProvider,\r\n ReferentielContext,\r\n useReferentielClient,\r\n useReferentielOptions,\r\n useReferentielSearch,\r\n} from \"./v2/referentiel\";\r\n\r\n// Domain models & shared UI/structural types, flat + type-only (no\r\n// runtime cost). Mirrors the same re-export already done inside\r\n// ./v2 — added here so consumers migrating off `v2.*` have a route\r\n// to the shared types the platform guidelines require reusing\r\n// (DrawerProps, FavoriteItem, SavedFilter, GridColumn, GridAction,\r\n// WizardStep, Notification, FormField, etc.) without falling back to\r\n// the deprecated namespace just to get a type import. Conflicting\r\n// names across the two star-exports are omitted per ES module\r\n// semantics rather than erroring.\r\nexport * from \"./v2/models\";\r\nexport * from \"./v2/types\";\r\n\r\n// Transitional alias — the `@portnet/ui/v2` subpath kept resolving\r\n// for one minor cycle so consuming modules can migrate their\r\n// imports independently of the package bump. Scheduled removal:\r\n// 0.4.0. Track removal in the Batch 5 cleanup ticket.\r\n//\r\n// POLICY: capability added after this deprecation is surfaced on the\r\n// flat root ONLY and is not added to ./v2/index.ts — adding it there\r\n// would hand consumers a new reason to stay on a retiring surface.\r\n// downloadBase64File / getMimeType are the first such additions.\r\n//\r\n// COROLLARY, learned the hard way (G-34): the policy only works if the flat\r\n// root is a strict SUPERSET of ./v2. It was not — downloadBlobFile and\r\n// openBase64FileInTab were on v2 and missing here, so \"migrate off v2\" meant\r\n// \"lose two utilities\". Before deprecating anything further, diff the two\r\n// surfaces; `npm run api:check` covers the root but does not compare it against\r\n// ./v2.\r\nexport * as v2 from \"./v2\";\r\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc,iBAAiB;AAC/B,cAAc,eAAe;AAC7B,cAAc,YAAY;AAC1B,SACEA,kBAAkB,EAClBC,eAAe,EACfC,kBAAkB,EAClBC,kBAAkB,QACb,YAAY;;AAEnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,EAAE,EACFC,QAAQ,EACRC,QAAQ,EACRC,kBAAkB,EAClBC,WAAW,EACXC,mBAAmB,EACnBC,gBAAgB,QACX,YAAY;;AAEnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,kBAAkB,EAClBC,iBAAiB,EACjBC,iBAAiB,EACjBC,yBAAyB,EACzBC,aAAa,EACbC,mBAAmB,QACd,YAAY;AAGnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,OAAO,IAAIC,iBAAiB,QAAQ,iCAAiC;AAM9E;AACA;AACA;AACA,SAASC,kBAAkB,EAAEC,YAAY,QAAQ,mBAAmB;;AAEpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,2BAA2B,EAC3BC,yBAAyB,EACzBC,mBAAmB,EACnBC,kBAAkB,EAClBC,YAAY,EACZC,aAAa,EACbC,gBAAgB,EAChBC,qBAAqB,EACrBC,mBAAmB,EACnBC,gBAAgB,EAChBC,cAAc,EACdC,eAAe,QACV,sBAAsB;;AAE7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,mBAAmB,EACnBC,kBAAkB,EAClBC,oBAAoB,EACpBC,qBAAqB,EACrBC,oBAAoB,QACf,kBAAkB;;AAEzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B,cAAc,YAAY;;AAE1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA,YAAAC,EAAA,MACoB,MAAM;AAAA,SAAAA,EAAA,IAAdC,EAAE","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"index.js","names":["createPortnetTheme","getCssVariables","injectCssVariables","removeCssVariables","cx","fmtMoney","contrast","downloadBase64File","getMimeType","openBase64FileInTab","downloadBlobFile","getApiErrorMessage","getApiErrorDetail","getApiErrorStatus","DEFAULT_API_ERROR_MESSAGE","networkStatus","createNetworkStatus","default","SkeletonPageShell","MODAL_POPPER_PROPS","MODAL_ZINDEX","UserPersonalizationProvider","createLocalStorageAdapter","createMemoryAdapter","registerCapability","useFavorites","useIsFavorite","useFavoritesView","filterRowsByFavorites","sortRowsByFavorites","FAVORITES_FILTER","FAVORITES_SORT","useSavedFilters","useGridPreferences","GRID_PREFERENCES_CAPABILITY_ID","ReferentielProvider","ReferentielContext","useReferentielClient","useReferentielOptions","useReferentielSearch","_v","v2"],"sources":["../../src/lib/index.ts"],"sourcesContent":["/**\r\n * @portnet/ui — root entry (MUI v9)\r\n *\r\n * The v1 Pui* surface was REMOVED in 0.3.0-mui9. The package now\r\n * exposes the modernised v2 Design System directly at its root.\r\n *\r\n * Imports\r\n * -------\r\n *\r\n * // Components (flat — preferred)\r\n * import { Button, Modal, FormGrid, Combobox, ... } from \"@portnet/ui\";\r\n *\r\n * // Theme + CSS variables + tokens + hooks (flat, or subpath entrypoints)\r\n * import { createPortnetTheme, injectCssVariables } from \"@portnet/ui\";\r\n * import { palette, spacing } from \"@portnet/ui/tokens\";\r\n * import { useFocusTrap } from \"@portnet/ui/hooks\";\r\n *\r\n * // Misc utils (flat) — class names, money formatting, WCAG contrast,\r\n * // and base64 → browser download\r\n * import { cx, fmtMoney, contrast, downloadBase64File } from \"@portnet/ui\";\r\n *\r\n * // Modal layering constants (flat)\r\n * import { MODAL_POPPER_PROPS, MODAL_ZINDEX } from \"@portnet/ui\";\r\n *\r\n * // Personalization foundation (flat, or \"@portnet/ui/personalization\")\r\n * import { UserPersonalizationProvider, createLocalStorageAdapter } from \"@portnet/ui\";\r\n *\r\n * // Referentiel data layer (flat) — backs ReferentielField /\r\n * // FormikReferentielField. Mount the provider once at the app root.\r\n * import { ReferentielProvider, useReferentielSearch } from \"@portnet/ui\";\r\n *\r\n * // Domain models + UI types (flat, type-only)\r\n * import type { FavoriteItem, SavedFilter, GridColumn } from \"@portnet/ui\";\r\n *\r\n * // Legacy `@portnet/ui/v2/*` paths still resolve transitionally\r\n * // through the v2 re-export below, deprecated and scheduled for\r\n * // removal in 0.4.0. NOTE: capability added after that deprecation\r\n * // is surfaced flat ONLY and is deliberately absent from `v2.*`.\r\n *\r\n * No v1 surface remains. Consuming modules that still import any\r\n * Pui* component will fail to resolve and must migrate per the\r\n * table in docs/migration-mui9/03-batch3-purge-v1.md.\r\n */\r\n\r\n// Flat re-export of every v2 component, pattern, hook, and the\r\n// theme factory. This is what consuming modules pin against\r\n// (`import { Button } from \"@portnet/ui\"`).\r\nexport * from \"./v2/components\";\r\nexport * from \"./v2/patterns\";\r\nexport * from \"./v2/hooks\";\r\nexport {\r\n createPortnetTheme,\r\n getCssVariables,\r\n injectCssVariables,\r\n removeCssVariables,\r\n} from \"./v2/theme\";\r\n\r\n// Theme factory typed contract, flat + type-only (capability gap G-33).\r\n//\r\n// The theme VALUES were flat-exported above while the types describing their\r\n// options were reachable ONLY from the deprecated `@portnet/ui/v2` subpath.\r\n// Every host module wraps createPortnetTheme in a local `build<Module>Theme`\r\n// helper (see the GU modules' utils/appearance), so every host needs this\r\n// contract to type its own wrapper — and had exactly two ways to get it:\r\n// import from a surface scheduled for removal in 0.4.0, or fall back to\r\n// `brand?: object` + `as Parameters<typeof createPortnetTheme>[0]`, both on\r\n// the Platform TypeScript Guidelines FORBIDDEN list. ./v2/theme's own doc\r\n// comment already names that anti-pattern as the reason these types were\r\n// published; it just never reached the surface consumers are told to migrate\r\n// TO. Closing that here removes the last reason a module needs `@portnet/ui/v2`\r\n// for the theme layer.\r\n//\r\n// Flat-only, per the `v2` policy note at the bottom of this file.\r\n// Purely additive and type-only; nothing existing changed.\r\nexport type {\r\n CreatePortnetThemeOptions,\r\n BrandTokens,\r\n BrandSecondaryTokens,\r\n} from \"./v2/theme\";\r\n\r\n// Added while migrating ms-dfd-fe to TypeScript: cx / fmtMoney / contrast had\r\n// no flat or subpath route, only the deprecated `v2.*` namespace below.\r\n// downloadBase64File + getMimeType were added later (mjcc-fe Area 5) and are\r\n// flat-only by policy — see the note on `v2` at the bottom of this file.\r\n// Purely additive re-exports; nothing existing changed.\r\n//\r\n// G-34 (caution-ui Area 6) — openBase64FileInTab + downloadBlobFile added.\r\n// These two were NOT flat-only additions: ./v2/index.ts has exported them all\r\n// along, so the FLAT ROOT — the surface the policy at the bottom of this file\r\n// tells every module to migrate TO — was a strict SUBSET of the deprecated\r\n// surface it replaces. A module obeying the migration policy lost access to\r\n// both, which is how caution-ui's Area 6 build broke:\r\n//\r\n// Attempted import error: 'downloadBlobFile' is not exported\r\n// from '@portnet/ui'\r\n//\r\n// Root cause is mechanical, and worth fixing properly rather than name by name:\r\n// ./v2/components, ./v2/patterns and ./v2/hooks are re-exported above with\r\n// `export *`, so anything added to them surfaces automatically. ./v2/utils is\r\n// re-exported by EXPLICIT NAME LIST, so every new util silently fails to reach\r\n// consumers until someone hits a build error. A wildcard is not a safe swap\r\n// here — ./v2/utils also carries `mergeLabels`, an internal label-merging\r\n// helper with no host use case, and widening a certified public surface by\r\n// accident is worse than the drift. The list stays, and the invariant is stated\r\n// instead: ANY addition to ./v2/utils intended for hosts must be added here in\r\n// the same commit.\r\nexport {\r\n cx,\r\n fmtMoney,\r\n contrast,\r\n downloadBase64File,\r\n getMimeType,\r\n openBase64FileInTab,\r\n downloadBlobFile,\r\n} from \"./v2/utils\";\r\n\r\n// Error narrowing + the network-status channel. Flat-only per the `v2` policy\r\n// note at the bottom of this file. Both were added from the\r\n// procuration-portnet-fe Area 5 gap audit (G-04, G-03):\r\n//\r\n// - getApiErrorMessage / getApiErrorStatus close the hole between two rules\r\n// the platform imposes together — `useUnknownInCatchVariables` is mandatory\r\n// and `any` / unsafe assertions are forbidden, with nothing sanctioned in\r\n// between. Every audited module had bridged it with\r\n// `error as { response?: { data?: { message?: unknown } } }`.\r\n//\r\n// - networkStatus is the feed for NetworkErrorBanner, which shipped as a\r\n// presentational component with its trigger contract left to consumers.\r\n//\r\n// G-34 — getApiErrorDetail added. Its own docstring PRESCRIBES a pairing that\r\n// was impossible to write, because the companion was never surfaced:\r\n//\r\n// toast(getApiErrorMessage(err, \"Échec de l'envoi.\"), \"error\");\r\n// console.error(\"[submit]\", getApiErrorStatus(err), getApiErrorDetail(err));\r\n//\r\n// The split exists because getApiErrorMessage deliberately SUPPRESSES transport\r\n// phrasings (G-07) — right for a toast, wrong for a console or an APM\r\n// breadcrumb, where \"Request failed with status code 502\" is exactly what an\r\n// operator needs. Without the escape hatch a host either logs the sanitised\r\n// string (losing the diagnostic) or re-reads `error.message` by hand with an\r\n// assertion (the very thing G-04 exists to prevent).\r\n//\r\n// Purely additive; nothing existing changed.\r\nexport {\r\n getApiErrorMessage,\r\n getApiErrorDetail,\r\n getApiErrorStatus,\r\n DEFAULT_API_ERROR_MESSAGE,\r\n networkStatus,\r\n createNetworkStatus,\r\n} from \"./v2/utils\";\r\nexport type { NetworkStatus, NetworkStatusListener } from \"./v2/utils\";\r\n\r\n// Route-level Suspense fallback. R-P.1 MUST requires a \"fallback skeleton page\",\r\n// and the library had every skeleton PART plus PageBusy, but nothing satisfying\r\n// the rule: PageBusy is a full-screen spinner, which R-8.1 MUST forbids for\r\n// navigation. So every module hand-built one, and to avoid the CLS a skeleton\r\n// exists to prevent it had to copy ActionBar's and PageContent's padding\r\n// literals out of this library. Owning it here keeps the three in lockstep.\r\n// Capability gap G-02 (procuration-portnet-fe Area 5). Flat-only, per the policy\r\n// note below.\r\nexport { default as SkeletonPageShell } from \"./v2/patterns/SkeletonPageShell\";\r\nexport type {\r\n SkeletonPageShellProps,\r\n SkeletonPageShellVariant,\r\n} from \"./v2/patterns/SkeletonPageShell\";\r\n\r\n// Modal/Drawer/Combobox/DatePicker layering constants - needed by any\r\n// consumer stacking a popover-ish component (Combobox, DateTimePicker)\r\n// above a Modal or Drawer. Was v2-namespace-only.\r\nexport { MODAL_POPPER_PROPS, MODAL_ZINDEX } from \"./v2/modalContext\";\r\n\r\n// Personalization foundation runtime API (favorites, saved filters).\r\n// Named re-export, not a wildcard: ./v2/personalization also carries\r\n// the Scope/FavoriteRef/SavedFilter model types for its own internal\r\n// use, which are already flat-exported canonically via ./v2/models\r\n// below - a wildcard here would re-export them a second time and\r\n// collide (TS2308). Mirrors exactly what ./v2/index.ts already does\r\n// internally for the same reason.\r\nexport {\r\n UserPersonalizationProvider,\r\n createLocalStorageAdapter,\r\n createMemoryAdapter,\r\n registerCapability,\r\n useFavorites,\r\n useIsFavorite,\r\n useFavoritesView,\r\n filterRowsByFavorites,\r\n sortRowsByFavorites,\r\n FAVORITES_FILTER,\r\n FAVORITES_SORT,\r\n useSavedFilters,\r\n} from \"./v2/personalization\";\r\n\r\n// Grid preferences capability (gap G-35) — per-user, per-resource hidden\r\n// columns. FLAT-ONLY, per the policy note on `v2` at the bottom of this file:\r\n// deliberately NOT added to ./v2/index.ts.\r\n//\r\n// DataGridToolbar owns the column-visibility UI but holds no state, so hosts\r\n// persisted it themselves in a bare localStorage key — not user-scoped, unlike\r\n// this foundation's `…:{userId}:{capabilityId}` scheme, so on a shared\r\n// workstation one user inherited another's hidden columns. Observed in\r\n// onssa-export-fe (\"onssaCertificates:listing:hiddenColumns\").\r\n//\r\n// It had to land in the library rather than the host: `registerCapability` is\r\n// exported above as a seam \"for custom/host-defined capabilities\", but the two\r\n// primitives a capability hook needs (usePersonalizationStore,\r\n// useStoreSelector) are private by design (§2.8, minimum API surface), so a\r\n// host-registered capability could be declared and never read or written.\r\n// Completing that seam instead would widen the internal surface AND leave every\r\n// module re-authoring the same slice; owning the slice here does neither.\r\nexport {\r\n useGridPreferences,\r\n GRID_PREFERENCES_CAPABILITY_ID,\r\n} from \"./v2/personalization\";\r\nexport type {\r\n UseGridPreferencesResult,\r\n GridPreferencesEntry,\r\n} from \"./v2/personalization\";\r\n\r\n// Referentiel data layer runtime API (config provider + hooks). Backs\r\n// ReferentielField / FormikReferentielField, which are flat-exported\r\n// above via ./v2/patterns - the field UI was reachable while the data\r\n// layer that feeds it was not, so consumers had to either import from\r\n// the deprecated `@portnet/ui/v2` surface or rebuild the client\r\n// locally. Added while auditing mjcc-fe, which had done the latter.\r\n//\r\n// Named re-export, not a wildcard, for the SAME reason as\r\n// personalization above: ./v2/referentiel also re-exports\r\n// ReferentielSearchArgs, ReferentielSearchResult and\r\n// ReferentielSearchFn for its own use, and those names are already\r\n// flat-exported canonically through ./v2/models below. A wildcard\r\n// would export them twice and collide (TS2308). The type layer keeps\r\n// its single canonical route.\r\nexport {\r\n ReferentielProvider,\r\n ReferentielContext,\r\n useReferentielClient,\r\n useReferentielOptions,\r\n useReferentielSearch,\r\n} from \"./v2/referentiel\";\r\n\r\n// Domain models & shared UI/structural types, flat + type-only (no\r\n// runtime cost). Mirrors the same re-export already done inside\r\n// ./v2 — added here so consumers migrating off `v2.*` have a route\r\n// to the shared types the platform guidelines require reusing\r\n// (DrawerProps, FavoriteItem, SavedFilter, GridColumn, GridAction,\r\n// WizardStep, Notification, FormField, etc.) without falling back to\r\n// the deprecated namespace just to get a type import. Conflicting\r\n// names across the two star-exports are omitted per ES module\r\n// semantics rather than erroring.\r\nexport * from \"./v2/models\";\r\nexport * from \"./v2/types\";\r\n\r\n// Transitional alias — the `@portnet/ui/v2` subpath kept resolving\r\n// for one minor cycle so consuming modules can migrate their\r\n// imports independently of the package bump. Scheduled removal:\r\n// 0.4.0. Track removal in the Batch 5 cleanup ticket.\r\n//\r\n// POLICY: capability added after this deprecation is surfaced on the\r\n// flat root ONLY and is not added to ./v2/index.ts — adding it there\r\n// would hand consumers a new reason to stay on a retiring surface.\r\n// downloadBase64File / getMimeType are the first such additions.\r\n//\r\n// COROLLARY, learned the hard way (G-34): the policy only works if the flat\r\n// root is a strict SUPERSET of ./v2. It was not — downloadBlobFile and\r\n// openBase64FileInTab were on v2 and missing here, so \"migrate off v2\" meant\r\n// \"lose two utilities\". Before deprecating anything further, diff the two\r\n// surfaces; `npm run api:check` covers the root but does not compare it against\r\n// ./v2.\r\nexport * as v2 from \"./v2\";\r\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc,iBAAiB;AAC/B,cAAc,eAAe;AAC7B,cAAc,YAAY;AAC1B,SACEA,kBAAkB,EAClBC,eAAe,EACfC,kBAAkB,EAClBC,kBAAkB,QACb,YAAY;;AAEnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,EAAE,EACFC,QAAQ,EACRC,QAAQ,EACRC,kBAAkB,EAClBC,WAAW,EACXC,mBAAmB,EACnBC,gBAAgB,QACX,YAAY;;AAEnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,kBAAkB,EAClBC,iBAAiB,EACjBC,iBAAiB,EACjBC,yBAAyB,EACzBC,aAAa,EACbC,mBAAmB,QACd,YAAY;AAGnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,OAAO,IAAIC,iBAAiB,QAAQ,iCAAiC;AAM9E;AACA;AACA;AACA,SAASC,kBAAkB,EAAEC,YAAY,QAAQ,mBAAmB;;AAEpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,2BAA2B,EAC3BC,yBAAyB,EACzBC,mBAAmB,EACnBC,kBAAkB,EAClBC,YAAY,EACZC,aAAa,EACbC,gBAAgB,EAChBC,qBAAqB,EACrBC,mBAAmB,EACnBC,gBAAgB,EAChBC,cAAc,EACdC,eAAe,QACV,sBAAsB;;AAE7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,kBAAkB,EAClBC,8BAA8B,QACzB,sBAAsB;AAM7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SACEC,mBAAmB,EACnBC,kBAAkB,EAClBC,oBAAoB,EACpBC,qBAAqB,EACrBC,oBAAoB,QACf,kBAAkB;;AAEzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B,cAAc,YAAY;;AAE1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA,YAAAC,EAAA,MACoB,MAAM;AAAA,SAAAA,EAAA,IAAdC,EAAE","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldError.test.js","names":["React","render","screen","FieldError","Field","jsx","_jsx","Fragment","_Fragment","jsxs","_jsxs","describe","test","children","expect","getByRole","toHaveTextContent","id","toHaveAttribute","not","getByTestId","label","error","toHaveAccessibleDescription","toBeInvalid","input","_render","hint","rerender","queryByText","toBeInTheDocument","getByText","_render2","queryByRole","_render3"],"sources":["../../../../src/lib/v2/__tests__/FieldError.test.tsx"],"sourcesContent":["import \"@testing-library/jest-dom\";\r\nimport * as React from \"react\";\r\nimport { render, screen } from \"@testing-library/react\";\r\nimport FieldError from \"../components/FieldError\";\r\nimport Field from \"../components/Field\";\r\n\r\n/**\r\n * FieldError — contract tests, including its integration into Field.\r\n *\r\n * WHY THIS FILE EXISTS\r\n * --------------------\r\n * §10.3 R-G.6 MUST. But the real reason is narrower and worth stating: the\r\n * contract this component carries is INVISIBLE. `Field` supplies an id, and that\r\n * id must reach the control's `aria-describedby` so a screen reader announces\r\n * the error when focus lands on the input. Break that wiring and NOTHING\r\n * changes: same pixels, same screenshot, same visual review — only a blind user\r\n * silently stops being told why their submit failed. There is no way to catch\r\n * that except by asserting it.\r\n *\r\n * The assertions below use `toHaveAccessibleDescription`, not id comparisons, on\r\n * purpose. It resolves aria-describedby to the text a screen reader would\r\n * actually announce, so the test states the USER-FACING outcome and stays true\r\n * if the id-generation scheme ever changes.\r\n *\r\n * NOT COVERED — deliberately\r\n * R-4.1's visual half (\"sous le champ, en rouge, avec icône\") and the\r\n * first-line icon alignment on a wrapping message are layout and colour. jsdom\r\n * resolves neither CSS variables nor flexbox, so asserting them here would pass\r\n * while the message rendered black and centred. Those live in\r\n * FieldError.stories.tsx.\r\n */\r\ndescribe(\"FieldError — standalone\", () => {\r\n test(\"announces itself as an alert carrying the message\", () => {\r\n render(<FieldError>Ajoutez au moins un article avant de soumettre.</FieldError>);\r\n expect(screen.getByRole(\"alert\")).toHaveTextContent(\r\n \"Ajoutez au moins un article avant de soumettre.\"\r\n );\r\n });\r\n\r\n test(\"forwards id so a host can wire its own aria-describedby\", () => {\r\n render(<FieldError id=\"articles-error\">Ajoutez au moins un article.</FieldError>);\r\n expect(screen.getByRole(\"alert\")).toHaveAttribute(\"id\", \"articles-error\");\r\n });\r\n\r\n test(\"omits id when none is supplied\", () => {\r\n /* The list-level case usually has nothing to bind to. Emitting an empty or\r\n * generated id would create a describedby target nobody references. */\r\n render(<FieldError>Ajoutez au moins un article.</FieldError>);\r\n expect(screen.getByRole(\"alert\")).not.toHaveAttribute(\"id\");\r\n });\r\n\r\n test(\"the icon is hidden from assistive tech\", () => {\r\n /* R-Ar.2 — the message IS the announcement; an announced glyph would double\r\n * it. No semantic query reaches a role-less, name-less element, hence the\r\n * testid the component exposes for exactly this assertion. */\r\n render(<FieldError>Message</FieldError>);\r\n expect(screen.getByTestId(\"fielderror-icon\")).toHaveAttribute(\"aria-hidden\", \"true\");\r\n });\r\n});\r\n\r\ndescribe(\"FieldError — inside Field (the wiring that has no visual tell)\", () => {\r\n test(\"the error becomes the control's accessible description\", () => {\r\n render(\r\n <Field label=\"Numéro de titre\" error=\"Le numéro est invalide. Format : AAAA-IM-NNNNN.\">\r\n <input />\r\n </Field>\r\n );\r\n\r\n expect(screen.getByRole(\"textbox\")).toHaveAccessibleDescription(\r\n \"Le numéro est invalide. Format : AAAA-IM-NNNNN.\"\r\n );\r\n });\r\n\r\n test(\"the control is marked invalid\", () => {\r\n render(\r\n <Field label=\"Numéro\" error=\"Invalide.\">\r\n <input />\r\n </Field>\r\n );\r\n // jest-dom's toBeInvalid reads aria-invalid — R-I.3.\r\n expect(screen.getByRole(\"textbox\")).toBeInvalid();\r\n });\r\n\r\n test(\"a host-supplied aria-describedby is PRESERVED, not replaced\", () => {\r\n /* The merge is the fragile part. Overwriting instead of merging silently\r\n * drops a consumer's external help text, and again nothing visual changes.\r\n * Asserted with two order-independent regexes rather than one exact string,\r\n * so the test does not encode which fragment Field puts first. */\r\n render(\r\n <>\r\n <span id=\"host-help\">Format attendu : AAAA-IM-NNNNN.</span>\r\n <Field label=\"Numéro\" error=\"Le numéro est incomplet.\">\r\n <input aria-describedby=\"host-help\" />\r\n </Field>\r\n </>\r\n );\r\n\r\n const input = screen.getByRole(\"textbox\");\r\n expect(input).toHaveAccessibleDescription(/Format attendu/);\r\n expect(input).toHaveAccessibleDescription(/incomplet/);\r\n });\r\n\r\n test(\"the hint is suppressed while an error is shown\", () => {\r\n /* Two competing descriptions under one field is worse than one. The hint\r\n * returns once the error clears — verified by the rerender below. */\r\n const { rerender } = render(\r\n <Field label=\"Numéro\" hint=\"Format : AAAA-IM-NNNNN\" error=\"Le numéro est incomplet.\">\r\n <input />\r\n </Field>\r\n );\r\n\r\n expect(screen.getByRole(\"alert\")).toHaveTextContent(\"Le numéro est incomplet.\");\r\n expect(screen.queryByText(\"Format : AAAA-IM-NNNNN\")).not.toBeInTheDocument();\r\n\r\n rerender(\r\n <Field label=\"Numéro\" hint=\"Format : AAAA-IM-NNNNN\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.getByText(\"Format : AAAA-IM-NNNNN\")).toBeInTheDocument();\r\n });\r\n});\r\n\r\ndescribe(\"FieldError — live-region mounting contract\", () => {\r\n test(\"no alert exists until an error appears\", () => {\r\n /* THE contract. `role=\\\"alert\\\"` announces on INSERTION: an element mounted\r\n * permanently with empty content announces nothing when it later fills, so\r\n * a well-meaning refactor that always renders the container — to \\\"avoid\r\n * layout shift\\\", say — silently kills the announcement while every pixel\r\n * and every screenshot stays identical. */\r\n const { rerender } = render(\r\n <Field label=\"Numéro\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.queryByRole(\"alert\")).not.toBeInTheDocument();\r\n\r\n rerender(\r\n <Field label=\"Numéro\" error=\"Le numéro est incomplet.\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.getByRole(\"alert\")).toBeInTheDocument();\r\n });\r\n\r\n test(\"the alert unmounts when the error clears\", () => {\r\n const { rerender } = render(\r\n <Field label=\"Numéro\" error=\"Le numéro est incomplet.\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.getByRole(\"alert\")).toBeInTheDocument();\r\n\r\n rerender(\r\n <Field label=\"Numéro\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.queryByRole(\"alert\")).not.toBeInTheDocument();\r\n expect(screen.getByRole(\"textbox\")).not.toBeInvalid();\r\n });\r\n});\r\n"],"mappings":"AAAA,OAAO,2BAA2B;AAClC,OAAO,KAAKA,KAAK,MAAM,OAAO;AAC9B,SAASC,MAAM,EAAEC,MAAM,QAAQ,wBAAwB;AACvD,OAAOC,UAAU,MAAM,0BAA0B;AACjD,OAAOC,KAAK,MAAM,qBAAqB;;AAEvC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAxBA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,QAAA,IAAAC,SAAA,EAAAC,IAAA,IAAAC,KAAA;AAyBAC,QAAQ,CAAC,yBAAyB,EAAE,YAAM;EACxCC,IAAI,CAAC,mDAAmD,EAAE,YAAM;IAC9DX,MAAM,cAACK,IAAA,CAACH,UAAU;MAAAU,QAAA,EAAC;IAA+C,CAAY,CAAC,CAAC;IAChFC,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,OAAO,CAAC,CAAC,CAACC,iBAAiB,CACjD,iDACF,CAAC;EACH,CAAC,CAAC;EAEFJ,IAAI,CAAC,yDAAyD,EAAE,YAAM;IACpEX,MAAM,cAACK,IAAA,CAACH,UAAU;MAACc,EAAE,EAAC,gBAAgB;MAAAJ,QAAA,EAAC;IAA4B,CAAY,CAAC,CAAC;IACjFC,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,OAAO,CAAC,CAAC,CAACG,eAAe,CAAC,IAAI,EAAE,gBAAgB,CAAC;EAC3E,CAAC,CAAC;EAEFN,IAAI,CAAC,gCAAgC,EAAE,YAAM;IAC3C;AACJ;IACIX,MAAM,cAACK,IAAA,CAACH,UAAU;MAAAU,QAAA,EAAC;IAA4B,CAAY,CAAC,CAAC;IAC7DC,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,OAAO,CAAC,CAAC,CAACI,GAAG,CAACD,eAAe,CAAC,IAAI,CAAC;EAC7D,CAAC,CAAC;EAEFN,IAAI,CAAC,wCAAwC,EAAE,YAAM;IACnD;AACJ;AACA;IACIX,MAAM,cAACK,IAAA,CAACH,UAAU;MAAAU,QAAA,EAAC;IAAO,CAAY,CAAC,CAAC;IACxCC,MAAM,CAACZ,MAAM,CAACkB,WAAW,CAAC,iBAAiB,CAAC,CAAC,CAACF,eAAe,CAAC,aAAa,EAAE,MAAM,CAAC;EACtF,CAAC,CAAC;AACJ,CAAC,CAAC;AAEFP,QAAQ,CAAC,gEAAgE,EAAE,YAAM;EAC/EC,IAAI,CAAC,wDAAwD,EAAE,YAAM;IACnEX,MAAM,cACJK,IAAA,CAACF,KAAK;MAACiB,KAAK,EAAC,oBAAiB;MAACC,KAAK,EAAC,oDAAiD;MAAAT,QAAA,eACpFP,IAAA,YAAQ;IAAC,CACJ,CACT,CAAC;IAEDQ,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,SAAS,CAAC,CAAC,CAACQ,2BAA2B,CAC7D,iDACF,CAAC;EACH,CAAC,CAAC;EAEFX,IAAI,CAAC,+BAA+B,EAAE,YAAM;IAC1CX,MAAM,cACJK,IAAA,CAACF,KAAK;MAACiB,KAAK,EAAC,WAAQ;MAACC,KAAK,EAAC,WAAW;MAAAT,QAAA,eACrCP,IAAA,YAAQ;IAAC,CACJ,CACT,CAAC;IACD;IACAQ,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,SAAS,CAAC,CAAC,CAACS,WAAW,CAAC,CAAC;EACnD,CAAC,CAAC;EAEFZ,IAAI,CAAC,6DAA6D,EAAE,YAAM;IACxE;AACJ;AACA;AACA;IACIX,MAAM,cACJS,KAAA,CAAAF,SAAA;MAAAK,QAAA,gBACEP,IAAA;QAAMW,EAAE,EAAC,WAAW;QAAAJ,QAAA,EAAC;MAA+B,CAAM,CAAC,eAC3DP,IAAA,CAACF,KAAK;QAACiB,KAAK,EAAC,WAAQ;QAACC,KAAK,EAAC,6BAA0B;QAAAT,QAAA,eACpDP,IAAA;UAAO,oBAAiB;QAAW,CAAE;MAAC,CACjC,CAAC;IAAA,CACR,CACJ,CAAC;IAED,IAAMmB,KAAK,GAAGvB,MAAM,CAACa,SAAS,CAAC,SAAS,CAAC;IACzCD,MAAM,CAACW,KAAK,CAAC,CAACF,2BAA2B,CAAC,gBAAgB,CAAC;IAC3DT,MAAM,CAACW,KAAK,CAAC,CAACF,2BAA2B,CAAC,WAAW,CAAC;EACxD,CAAC,CAAC;EAEFX,IAAI,CAAC,gDAAgD,EAAE,YAAM;IAC3D;AACJ;IACI,IAAAc,OAAA,GAAqBzB,MAAM,cACzBK,IAAA,CAACF,KAAK;QAACiB,KAAK,EAAC,WAAQ;QAACM,IAAI,EAAC,wBAAwB;QAACL,KAAK,EAAC,6BAA0B;QAAAT,QAAA,eAClFP,IAAA,YAAQ;MAAC,CACJ,CACT,CAAC;MAJOsB,QAAQ,GAAAF,OAAA,CAARE,QAAQ;IAMhBd,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,OAAO,CAAC,CAAC,CAACC,iBAAiB,CAAC,0BAA0B,CAAC;IAC/EF,MAAM,CAACZ,MAAM,CAAC2B,WAAW,CAAC,wBAAwB,CAAC,CAAC,CAACV,GAAG,CAACW,iBAAiB,CAAC,CAAC;IAE5EF,QAAQ,cACNtB,IAAA,CAACF,KAAK;MAACiB,KAAK,EAAC,WAAQ;MAACM,IAAI,EAAC,wBAAwB;MAAAd,QAAA,eACjDP,IAAA,YAAQ;IAAC,CACJ,CACT,CAAC;IACDQ,MAAM,CAACZ,MAAM,CAAC6B,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAACD,iBAAiB,CAAC,CAAC;EACxE,CAAC,CAAC;AACJ,CAAC,CAAC;AAEFnB,QAAQ,CAAC,4CAA4C,EAAE,YAAM;EAC3DC,IAAI,CAAC,wCAAwC,EAAE,YAAM;IACnD;AACJ;AACA;AACA;AACA;IACI,IAAAoB,QAAA,GAAqB/B,MAAM,cACzBK,IAAA,CAACF,KAAK;QAACiB,KAAK,EAAC,WAAQ;QAAAR,QAAA,eACnBP,IAAA,YAAQ;MAAC,CACJ,CACT,CAAC;MAJOsB,QAAQ,GAAAI,QAAA,CAARJ,QAAQ;IAKhBd,MAAM,CAACZ,MAAM,CAAC+B,WAAW,CAAC,OAAO,CAAC,CAAC,CAACd,GAAG,CAACW,iBAAiB,CAAC,CAAC;IAE3DF,QAAQ,cACNtB,IAAA,CAACF,KAAK;MAACiB,KAAK,EAAC,WAAQ;MAACC,KAAK,EAAC,6BAA0B;MAAAT,QAAA,eACpDP,IAAA,YAAQ;IAAC,CACJ,CACT,CAAC;IACDQ,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,OAAO,CAAC,CAAC,CAACe,iBAAiB,CAAC,CAAC;EACvD,CAAC,CAAC;EAEFlB,IAAI,CAAC,0CAA0C,EAAE,YAAM;IACrD,IAAAsB,QAAA,GAAqBjC,MAAM,cACzBK,IAAA,CAACF,KAAK;QAACiB,KAAK,EAAC,WAAQ;QAACC,KAAK,EAAC,6BAA0B;QAAAT,QAAA,eACpDP,IAAA,YAAQ;MAAC,CACJ,CACT,CAAC;MAJOsB,QAAQ,GAAAM,QAAA,CAARN,QAAQ;IAKhBd,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,OAAO,CAAC,CAAC,CAACe,iBAAiB,CAAC,CAAC;IAErDF,QAAQ,cACNtB,IAAA,CAACF,KAAK;MAACiB,KAAK,EAAC,WAAQ;MAAAR,QAAA,eACnBP,IAAA,YAAQ;IAAC,CACJ,CACT,CAAC;IACDQ,MAAM,CAACZ,MAAM,CAAC+B,WAAW,CAAC,OAAO,CAAC,CAAC,CAACd,GAAG,CAACW,iBAAiB,CAAC,CAAC;IAC3DhB,MAAM,CAACZ,MAAM,CAACa,SAAS,CAAC,SAAS,CAAC,CAAC,CAACI,GAAG,CAACK,WAAW,CAAC,CAAC;EACvD,CAAC,CAAC;AACJ,CAAC,CAAC","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"FieldError.test.js","names":["render","screen","FieldError","Field","jsx","_jsx","Fragment","_Fragment","jsxs","_jsxs","describe","test","children","expect","getByRole","toHaveTextContent","id","toHaveAttribute","not","getByTestId","label","error","toHaveAccessibleDescription","toBeInvalid","input","_render","hint","rerender","queryByText","toBeInTheDocument","getByText","_render2","queryByRole","_render3"],"sources":["../../../../src/lib/v2/__tests__/FieldError.test.tsx"],"sourcesContent":["import \"@testing-library/jest-dom\";\r\nimport { render, screen } from \"@testing-library/react\";\r\nimport FieldError from \"../components/FieldError\";\r\nimport Field from \"../components/Field\";\r\n\r\n/**\r\n * FieldError — contract tests, including its integration into Field.\r\n *\r\n * WHY THIS FILE EXISTS\r\n * --------------------\r\n * §10.3 R-G.6 MUST. But the real reason is narrower and worth stating: the\r\n * contract this component carries is INVISIBLE. `Field` supplies an id, and that\r\n * id must reach the control's `aria-describedby` so a screen reader announces\r\n * the error when focus lands on the input. Break that wiring and NOTHING\r\n * changes: same pixels, same screenshot, same visual review — only a blind user\r\n * silently stops being told why their submit failed. There is no way to catch\r\n * that except by asserting it.\r\n *\r\n * The assertions below use `toHaveAccessibleDescription`, not id comparisons, on\r\n * purpose. It resolves aria-describedby to the text a screen reader would\r\n * actually announce, so the test states the USER-FACING outcome and stays true\r\n * if the id-generation scheme ever changes.\r\n *\r\n * NOT COVERED — deliberately\r\n * R-4.1's visual half (\"sous le champ, en rouge, avec icône\") and the\r\n * first-line icon alignment on a wrapping message are layout and colour. jsdom\r\n * resolves neither CSS variables nor flexbox, so asserting them here would pass\r\n * while the message rendered black and centred. Those live in\r\n * FieldError.stories.tsx.\r\n */\r\ndescribe(\"FieldError — standalone\", () => {\r\n test(\"announces itself as an alert carrying the message\", () => {\r\n render(<FieldError>Ajoutez au moins un article avant de soumettre.</FieldError>);\r\n expect(screen.getByRole(\"alert\")).toHaveTextContent(\r\n \"Ajoutez au moins un article avant de soumettre.\"\r\n );\r\n });\r\n\r\n test(\"forwards id so a host can wire its own aria-describedby\", () => {\r\n render(<FieldError id=\"articles-error\">Ajoutez au moins un article.</FieldError>);\r\n expect(screen.getByRole(\"alert\")).toHaveAttribute(\"id\", \"articles-error\");\r\n });\r\n\r\n test(\"omits id when none is supplied\", () => {\r\n /* The list-level case usually has nothing to bind to. Emitting an empty or\r\n * generated id would create a describedby target nobody references. */\r\n render(<FieldError>Ajoutez au moins un article.</FieldError>);\r\n expect(screen.getByRole(\"alert\")).not.toHaveAttribute(\"id\");\r\n });\r\n\r\n test(\"the icon is hidden from assistive tech\", () => {\r\n /* R-Ar.2 — the message IS the announcement; an announced glyph would double\r\n * it. No semantic query reaches a role-less, name-less element, hence the\r\n * testid the component exposes for exactly this assertion. */\r\n render(<FieldError>Message</FieldError>);\r\n expect(screen.getByTestId(\"fielderror-icon\")).toHaveAttribute(\"aria-hidden\", \"true\");\r\n });\r\n});\r\n\r\ndescribe(\"FieldError — inside Field (the wiring that has no visual tell)\", () => {\r\n test(\"the error becomes the control's accessible description\", () => {\r\n render(\r\n <Field label=\"Numéro de titre\" error=\"Le numéro est invalide. Format : AAAA-IM-NNNNN.\">\r\n <input />\r\n </Field>\r\n );\r\n\r\n expect(screen.getByRole(\"textbox\")).toHaveAccessibleDescription(\r\n \"Le numéro est invalide. Format : AAAA-IM-NNNNN.\"\r\n );\r\n });\r\n\r\n test(\"the control is marked invalid\", () => {\r\n render(\r\n <Field label=\"Numéro\" error=\"Invalide.\">\r\n <input />\r\n </Field>\r\n );\r\n // jest-dom's toBeInvalid reads aria-invalid — R-I.3.\r\n expect(screen.getByRole(\"textbox\")).toBeInvalid();\r\n });\r\n\r\n test(\"a host-supplied aria-describedby is PRESERVED, not replaced\", () => {\r\n /* The merge is the fragile part. Overwriting instead of merging silently\r\n * drops a consumer's external help text, and again nothing visual changes.\r\n * Asserted with two order-independent regexes rather than one exact string,\r\n * so the test does not encode which fragment Field puts first. */\r\n render(\r\n <>\r\n <span id=\"host-help\">Format attendu : AAAA-IM-NNNNN.</span>\r\n <Field label=\"Numéro\" error=\"Le numéro est incomplet.\">\r\n <input aria-describedby=\"host-help\" />\r\n </Field>\r\n </>\r\n );\r\n\r\n const input = screen.getByRole(\"textbox\");\r\n expect(input).toHaveAccessibleDescription(/Format attendu/);\r\n expect(input).toHaveAccessibleDescription(/incomplet/);\r\n });\r\n\r\n test(\"the hint is suppressed while an error is shown\", () => {\r\n /* Two competing descriptions under one field is worse than one. The hint\r\n * returns once the error clears — verified by the rerender below. */\r\n const { rerender } = render(\r\n <Field label=\"Numéro\" hint=\"Format : AAAA-IM-NNNNN\" error=\"Le numéro est incomplet.\">\r\n <input />\r\n </Field>\r\n );\r\n\r\n expect(screen.getByRole(\"alert\")).toHaveTextContent(\"Le numéro est incomplet.\");\r\n expect(screen.queryByText(\"Format : AAAA-IM-NNNNN\")).not.toBeInTheDocument();\r\n\r\n rerender(\r\n <Field label=\"Numéro\" hint=\"Format : AAAA-IM-NNNNN\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.getByText(\"Format : AAAA-IM-NNNNN\")).toBeInTheDocument();\r\n });\r\n});\r\n\r\ndescribe(\"FieldError — live-region mounting contract\", () => {\r\n test(\"no alert exists until an error appears\", () => {\r\n /* THE contract. `role=\\\"alert\\\"` announces on INSERTION: an element mounted\r\n * permanently with empty content announces nothing when it later fills, so\r\n * a well-meaning refactor that always renders the container — to \\\"avoid\r\n * layout shift\\\", say — silently kills the announcement while every pixel\r\n * and every screenshot stays identical. */\r\n const { rerender } = render(\r\n <Field label=\"Numéro\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.queryByRole(\"alert\")).not.toBeInTheDocument();\r\n\r\n rerender(\r\n <Field label=\"Numéro\" error=\"Le numéro est incomplet.\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.getByRole(\"alert\")).toBeInTheDocument();\r\n });\r\n\r\n test(\"the alert unmounts when the error clears\", () => {\r\n const { rerender } = render(\r\n <Field label=\"Numéro\" error=\"Le numéro est incomplet.\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.getByRole(\"alert\")).toBeInTheDocument();\r\n\r\n rerender(\r\n <Field label=\"Numéro\">\r\n <input />\r\n </Field>\r\n );\r\n expect(screen.queryByRole(\"alert\")).not.toBeInTheDocument();\r\n expect(screen.getByRole(\"textbox\")).not.toBeInvalid();\r\n });\r\n});\r\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import Field, { FIELD_MAX_WIDTH } from "../components/Field";
|
|
4
|
+
import Input from "../components/Input";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Field — largeur semantique (gap G-H) : tests de contrat.
|
|
8
|
+
*
|
|
9
|
+
* CE QUE JSDOM PEUT ET NE PEUT PAS VERIFIER
|
|
10
|
+
* -----------------------------------------
|
|
11
|
+
* Le plafond n'est pose qu'a partir du palier `sm` — le standard exigeant
|
|
12
|
+
* « 100% grid width » sur mobile. Il passe donc par une requete media
|
|
13
|
+
* qu'`emotion` injecte dans la feuille de style, et jsdom ne resout pas les
|
|
14
|
+
* requetes media : `getComputedStyle` ne rendra jamais la largeur effective au
|
|
15
|
+
* palier desktop. Un test de rendu ne PEUT pas l'assertir, et pretendre le
|
|
16
|
+
* faire donnerait un test qui passe sans rien verifier.
|
|
17
|
+
*
|
|
18
|
+
* Ce fichier epingle donc ce qui est verifiable et qui compte :
|
|
19
|
+
*
|
|
20
|
+
* 1. LES CINQ VALEURS. C'est le point sensible du tour : elles ne se lisent
|
|
21
|
+
* pas dans le standard, qui donne des FOURCHETTES — quelqu'un a choisi, et
|
|
22
|
+
* ce choix doit etre visible et protege. Si l'equipe design les revoit,
|
|
23
|
+
* c'est ce test qui doit etre mis a jour sciemment, pas un ecran qui doit
|
|
24
|
+
* bouger par surprise.
|
|
25
|
+
*
|
|
26
|
+
* 2. QUE `width` NE CASSE PAS LE CABLAGE D'ACCESSIBILITE. `Field` associe
|
|
27
|
+
* libelle, indication et erreur au controle ; ajouter une contrainte de
|
|
28
|
+
* mise en page ne doit rien y toucher. C'est la regression la plus
|
|
29
|
+
* probable d'un changement qui passe par `sx`.
|
|
30
|
+
*
|
|
31
|
+
* 3. QUE L'OMISSION RESTE NEUTRE. Sans `width`, aucun plafond : les
|
|
32
|
+
* appelants d'avant G-H ne doivent pas bouger d'un pixel.
|
|
33
|
+
*
|
|
34
|
+
* Le comportement responsive lui-meme se verifie dans la story, en
|
|
35
|
+
* redimensionnant la fenetre — c'est la que le contrat est observable.
|
|
36
|
+
*/
|
|
37
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
38
|
+
describe("Field — les cinq gabarits (G-H)", function () {
|
|
39
|
+
test("les valeurs sont celles retenues dans la fourchette du standard", function () {
|
|
40
|
+
/* Fourchettes du standard, et valeur retenue (le HAUT, ces largeurs etant
|
|
41
|
+
* des plafonds) :
|
|
42
|
+
* xs ~80-120 -> 120 s ~160-200 -> 200
|
|
43
|
+
* m ~320-400 -> 400 l ~500-600 -> 600
|
|
44
|
+
* xl 100%
|
|
45
|
+
* Chaque valeur passe par une variable CSS pour etre revue sans toucher au
|
|
46
|
+
* code : c'est le repli qui est epingle ici. */
|
|
47
|
+
expect(FIELD_MAX_WIDTH.xs).toBe("var(--field-w-xs, 120px)");
|
|
48
|
+
expect(FIELD_MAX_WIDTH.s).toBe("var(--field-w-s, 200px)");
|
|
49
|
+
expect(FIELD_MAX_WIDTH.m).toBe("var(--field-w-m, 400px)");
|
|
50
|
+
expect(FIELD_MAX_WIDTH.l).toBe("var(--field-w-l, 600px)");
|
|
51
|
+
expect(FIELD_MAX_WIDTH.xl).toBe("100%");
|
|
52
|
+
});
|
|
53
|
+
test("les cinq gabarits du standard sont couverts, ni plus ni moins", function () {
|
|
54
|
+
/* Un gabarit ajoute ou retire est un ecart au standard, pas un detail
|
|
55
|
+
* d'implementation. */
|
|
56
|
+
expect(Object.keys(FIELD_MAX_WIDTH).sort()).toEqual(["l", "m", "s", "xl", "xs"]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("Field — `width` ne touche pas au cablage d'accessibilite", function () {
|
|
60
|
+
test("le libelle nomme toujours le controle", function () {
|
|
61
|
+
render(/*#__PURE__*/_jsx(Field, {
|
|
62
|
+
label: "Identifiant fiscal",
|
|
63
|
+
htmlFor: "ifu",
|
|
64
|
+
width: "s",
|
|
65
|
+
children: /*#__PURE__*/_jsx(Input, {})
|
|
66
|
+
}));
|
|
67
|
+
expect(screen.getByRole("textbox", {
|
|
68
|
+
name: /Identifiant fiscal/
|
|
69
|
+
})).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
test("l'erreur reste la description accessible du controle", function () {
|
|
72
|
+
render(/*#__PURE__*/_jsx(Field, {
|
|
73
|
+
label: "Identifiant fiscal",
|
|
74
|
+
htmlFor: "ifu",
|
|
75
|
+
width: "s",
|
|
76
|
+
error: "Quinze caract\xE8res maximum.",
|
|
77
|
+
children: /*#__PURE__*/_jsx(Input, {})
|
|
78
|
+
}));
|
|
79
|
+
expect(screen.getByRole("textbox")).toHaveAccessibleDescription("Quinze caractères maximum.");
|
|
80
|
+
expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
|
|
81
|
+
});
|
|
82
|
+
test("l'indication reste la description accessible du controle", function () {
|
|
83
|
+
render(/*#__PURE__*/_jsx(Field, {
|
|
84
|
+
label: "Adresse",
|
|
85
|
+
htmlFor: "adresse",
|
|
86
|
+
width: "l",
|
|
87
|
+
hint: "Voie, code postal et ville.",
|
|
88
|
+
children: /*#__PURE__*/_jsx(Input, {})
|
|
89
|
+
}));
|
|
90
|
+
expect(screen.getByRole("textbox")).toHaveAccessibleDescription("Voie, code postal et ville.");
|
|
91
|
+
});
|
|
92
|
+
test("`required` reste annonce comme tel", function () {
|
|
93
|
+
render(/*#__PURE__*/_jsx(Field, {
|
|
94
|
+
label: "Marque",
|
|
95
|
+
htmlFor: "marque",
|
|
96
|
+
width: "m",
|
|
97
|
+
required: true,
|
|
98
|
+
children: /*#__PURE__*/_jsx(Input, {})
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
/* `toBeRequired` accepte l'attribut NATIF `required` comme `aria-required` :
|
|
102
|
+
* c'est volontaire, et c'est la bonne assertion ici.
|
|
103
|
+
*
|
|
104
|
+
* Les deux mecanismes coexistent dans la library, par des chemins
|
|
105
|
+
* differents, et viser l'un des deux nommement rendrait le test faux sans
|
|
106
|
+
* que le comportement change :
|
|
107
|
+
* — sur un controle MUI (`Input`, `Textarea`), `Field` passe `required`
|
|
108
|
+
* au `FormControl`, dont le CONTEXTE le propage jusqu'a l'input, qui
|
|
109
|
+
* porte alors l'attribut natif ;
|
|
110
|
+
* — sur un groupe (`RadioGroup`), aucun contexte ne s'applique : `Field`
|
|
111
|
+
* injecte `aria-required` sur l'enfant, que le groupe transmet a son
|
|
112
|
+
* `role="radiogroup"` (voir le test de `FormikRadioGroup`).
|
|
113
|
+
*
|
|
114
|
+
* Ce qui compte pour R-I.2 est qu'une technologie d'assistance l'annonce,
|
|
115
|
+
* pas quel attribut le porte. */
|
|
116
|
+
expect(screen.getByRole("textbox")).toBeRequired();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("Field — l'omission reste neutre", function () {
|
|
120
|
+
test("sans `width`, le champ se rend et se nomme comme avant G-H", function () {
|
|
121
|
+
/* Ce que ce test prouve, et ce qu'il ne prouve pas.
|
|
122
|
+
*
|
|
123
|
+
* L'absence de plafond n'est PAS assertable : elle se lit dans la feuille
|
|
124
|
+
* de style generee par `emotion`, que jsdom ne resout pas plus qu'une
|
|
125
|
+
* requete media. Une premiere version comparait le nombre de noeuds avec
|
|
126
|
+
* et sans `width` — ce qui exigeait un acces direct au DOM, interdit a
|
|
127
|
+
* juste titre par `testing-library/no-node-access` : compter des `<label>`
|
|
128
|
+
* teste l'implementation, pas le comportement.
|
|
129
|
+
*
|
|
130
|
+
* Ce qui EST verifiable et suffit a la retrocompatibilite : sans `width`,
|
|
131
|
+
* le controle est rendu, nomme par son libelle, et n'a herite d'aucun etat
|
|
132
|
+
* qu'il n'a pas demande. Le plafond, lui, se verifie dans la story. */
|
|
133
|
+
render(/*#__PURE__*/_jsx(Field, {
|
|
134
|
+
label: "Marque",
|
|
135
|
+
htmlFor: "sans-gabarit",
|
|
136
|
+
children: /*#__PURE__*/_jsx(Input, {})
|
|
137
|
+
}));
|
|
138
|
+
var champ = screen.getByRole("textbox", {
|
|
139
|
+
name: /Marque/
|
|
140
|
+
});
|
|
141
|
+
expect(champ).toBeInTheDocument();
|
|
142
|
+
expect(champ).not.toBeRequired();
|
|
143
|
+
expect(champ).not.toBeInvalid();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=FieldWidth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldWidth.test.js","names":["render","screen","Field","FIELD_MAX_WIDTH","Input","jsx","_jsx","describe","test","expect","xs","toBe","s","m","l","xl","Object","keys","sort","toEqual","label","htmlFor","width","children","getByRole","name","toBeInTheDocument","error","toHaveAccessibleDescription","toHaveAttribute","hint","required","toBeRequired","champ","not","toBeInvalid"],"sources":["../../../../src/lib/v2/__tests__/FieldWidth.test.tsx"],"sourcesContent":["import \"@testing-library/jest-dom\";\r\nimport { render, screen } from \"@testing-library/react\";\r\nimport Field, { FIELD_MAX_WIDTH } from \"../components/Field\";\r\nimport Input from \"../components/Input\";\r\n\r\n/**\r\n * Field — largeur semantique (gap G-H) : tests de contrat.\r\n *\r\n * CE QUE JSDOM PEUT ET NE PEUT PAS VERIFIER\r\n * -----------------------------------------\r\n * Le plafond n'est pose qu'a partir du palier `sm` — le standard exigeant\r\n * « 100% grid width » sur mobile. Il passe donc par une requete media\r\n * qu'`emotion` injecte dans la feuille de style, et jsdom ne resout pas les\r\n * requetes media : `getComputedStyle` ne rendra jamais la largeur effective au\r\n * palier desktop. Un test de rendu ne PEUT pas l'assertir, et pretendre le\r\n * faire donnerait un test qui passe sans rien verifier.\r\n *\r\n * Ce fichier epingle donc ce qui est verifiable et qui compte :\r\n *\r\n * 1. LES CINQ VALEURS. C'est le point sensible du tour : elles ne se lisent\r\n * pas dans le standard, qui donne des FOURCHETTES — quelqu'un a choisi, et\r\n * ce choix doit etre visible et protege. Si l'equipe design les revoit,\r\n * c'est ce test qui doit etre mis a jour sciemment, pas un ecran qui doit\r\n * bouger par surprise.\r\n *\r\n * 2. QUE `width` NE CASSE PAS LE CABLAGE D'ACCESSIBILITE. `Field` associe\r\n * libelle, indication et erreur au controle ; ajouter une contrainte de\r\n * mise en page ne doit rien y toucher. C'est la regression la plus\r\n * probable d'un changement qui passe par `sx`.\r\n *\r\n * 3. QUE L'OMISSION RESTE NEUTRE. Sans `width`, aucun plafond : les\r\n * appelants d'avant G-H ne doivent pas bouger d'un pixel.\r\n *\r\n * Le comportement responsive lui-meme se verifie dans la story, en\r\n * redimensionnant la fenetre — c'est la que le contrat est observable.\r\n */\r\n\r\ndescribe(\"Field — les cinq gabarits (G-H)\", () => {\r\n test(\"les valeurs sont celles retenues dans la fourchette du standard\", () => {\r\n /* Fourchettes du standard, et valeur retenue (le HAUT, ces largeurs etant\r\n * des plafonds) :\r\n * xs ~80-120 -> 120 s ~160-200 -> 200\r\n * m ~320-400 -> 400 l ~500-600 -> 600\r\n * xl 100%\r\n * Chaque valeur passe par une variable CSS pour etre revue sans toucher au\r\n * code : c'est le repli qui est epingle ici. */\r\n expect(FIELD_MAX_WIDTH.xs).toBe(\"var(--field-w-xs, 120px)\");\r\n expect(FIELD_MAX_WIDTH.s).toBe(\"var(--field-w-s, 200px)\");\r\n expect(FIELD_MAX_WIDTH.m).toBe(\"var(--field-w-m, 400px)\");\r\n expect(FIELD_MAX_WIDTH.l).toBe(\"var(--field-w-l, 600px)\");\r\n expect(FIELD_MAX_WIDTH.xl).toBe(\"100%\");\r\n });\r\n\r\n test(\"les cinq gabarits du standard sont couverts, ni plus ni moins\", () => {\r\n /* Un gabarit ajoute ou retire est un ecart au standard, pas un detail\r\n * d'implementation. */\r\n expect(Object.keys(FIELD_MAX_WIDTH).sort()).toEqual([\r\n \"l\",\r\n \"m\",\r\n \"s\",\r\n \"xl\",\r\n \"xs\",\r\n ]);\r\n });\r\n});\r\n\r\ndescribe(\"Field — `width` ne touche pas au cablage d'accessibilite\", () => {\r\n test(\"le libelle nomme toujours le controle\", () => {\r\n render(\r\n <Field label=\"Identifiant fiscal\" htmlFor=\"ifu\" width=\"s\">\r\n <Input />\r\n </Field>\r\n );\r\n\r\n expect(\r\n screen.getByRole(\"textbox\", { name: /Identifiant fiscal/ })\r\n ).toBeInTheDocument();\r\n });\r\n\r\n test(\"l'erreur reste la description accessible du controle\", () => {\r\n render(\r\n <Field\r\n label=\"Identifiant fiscal\"\r\n htmlFor=\"ifu\"\r\n width=\"s\"\r\n error=\"Quinze caractères maximum.\"\r\n >\r\n <Input />\r\n </Field>\r\n );\r\n\r\n expect(screen.getByRole(\"textbox\")).toHaveAccessibleDescription(\r\n \"Quinze caractères maximum.\"\r\n );\r\n expect(screen.getByRole(\"textbox\")).toHaveAttribute(\r\n \"aria-invalid\",\r\n \"true\"\r\n );\r\n });\r\n\r\n test(\"l'indication reste la description accessible du controle\", () => {\r\n render(\r\n <Field\r\n label=\"Adresse\"\r\n htmlFor=\"adresse\"\r\n width=\"l\"\r\n hint=\"Voie, code postal et ville.\"\r\n >\r\n <Input />\r\n </Field>\r\n );\r\n\r\n expect(screen.getByRole(\"textbox\")).toHaveAccessibleDescription(\r\n \"Voie, code postal et ville.\"\r\n );\r\n });\r\n\r\n test(\"`required` reste annonce comme tel\", () => {\r\n render(\r\n <Field label=\"Marque\" htmlFor=\"marque\" width=\"m\" required>\r\n <Input />\r\n </Field>\r\n );\r\n\r\n /* `toBeRequired` accepte l'attribut NATIF `required` comme `aria-required` :\r\n * c'est volontaire, et c'est la bonne assertion ici.\r\n *\r\n * Les deux mecanismes coexistent dans la library, par des chemins\r\n * differents, et viser l'un des deux nommement rendrait le test faux sans\r\n * que le comportement change :\r\n * — sur un controle MUI (`Input`, `Textarea`), `Field` passe `required`\r\n * au `FormControl`, dont le CONTEXTE le propage jusqu'a l'input, qui\r\n * porte alors l'attribut natif ;\r\n * — sur un groupe (`RadioGroup`), aucun contexte ne s'applique : `Field`\r\n * injecte `aria-required` sur l'enfant, que le groupe transmet a son\r\n * `role=\"radiogroup\"` (voir le test de `FormikRadioGroup`).\r\n *\r\n * Ce qui compte pour R-I.2 est qu'une technologie d'assistance l'annonce,\r\n * pas quel attribut le porte. */\r\n expect(screen.getByRole(\"textbox\")).toBeRequired();\r\n });\r\n});\r\n\r\ndescribe(\"Field — l'omission reste neutre\", () => {\r\n test(\"sans `width`, le champ se rend et se nomme comme avant G-H\", () => {\r\n /* Ce que ce test prouve, et ce qu'il ne prouve pas.\r\n *\r\n * L'absence de plafond n'est PAS assertable : elle se lit dans la feuille\r\n * de style generee par `emotion`, que jsdom ne resout pas plus qu'une\r\n * requete media. Une premiere version comparait le nombre de noeuds avec\r\n * et sans `width` — ce qui exigeait un acces direct au DOM, interdit a\r\n * juste titre par `testing-library/no-node-access` : compter des `<label>`\r\n * teste l'implementation, pas le comportement.\r\n *\r\n * Ce qui EST verifiable et suffit a la retrocompatibilite : sans `width`,\r\n * le controle est rendu, nomme par son libelle, et n'a herite d'aucun etat\r\n * qu'il n'a pas demande. Le plafond, lui, se verifie dans la story. */\r\n render(\r\n <Field label=\"Marque\" htmlFor=\"sans-gabarit\">\r\n <Input />\r\n </Field>\r\n );\r\n\r\n const champ = screen.getByRole(\"textbox\", { name: /Marque/ });\r\n expect(champ).toBeInTheDocument();\r\n expect(champ).not.toBeRequired();\r\n expect(champ).not.toBeInvalid();\r\n });\r\n});\r\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileCard.test.js","names":["React","render","screen","FileCard","jsx","_jsx","describe","test","name","expect","getByText","toHaveAttribute","meta","toBeInTheDocument","_render","rerender","queryByTestId","not","getByTestId","toHaveTextContent","actions","type","children","getByRole","queryByRole","icon"],"sources":["../../../../src/lib/v2/__tests__/FileCard.test.tsx"],"sourcesContent":["import \"@testing-library/jest-dom\";\r\nimport * as React from \"react\";\r\nimport { render, screen } from \"@testing-library/react\";\r\nimport FileCard from \"../components/FileCard\";\r\n\r\n/**\r\n * FileCard — contract tests.\r\n *\r\n * WHY THIS FILE EXISTS\r\n * --------------------\r\n * §10.3 R-G.6 MUST — \"Snapshot Storybook + tests Jest/Testing-Library\". The\r\n * component was extracted from THREE hand-written copies found in addpe-fe\r\n * (Attachment, AutorisationPreviewDrawer, AutorisationDetail), which had drifted\r\n * to three different paddings for one concept. Consolidating them means a single\r\n * change now moves every attachment row in the fleet, so the contracts below are\r\n * the ones that must not move silently.\r\n *\r\n * NOT COVERED — deliberately\r\n * The width negotiation is the most fragile part of this component and it is\r\n * NOT tested here: `text-overflow: ellipsis`, `minWidth: 0` on the identity\r\n * block and `flexShrink: 0` on the action cluster are LAYOUT, and jsdom computes\r\n * no layout. A test asserting them would pass while the card is broken on a\r\n * phone — worse than no coverage, because it would look like coverage. That\r\n * contract lives in FileCard.stories.tsx (\"Long filename\"), which is where a\r\n * browser can actually check it. Same reasoning as MobileRowCard.test.tsx\r\n * records for R-G.3.\r\n */\r\ndescribe(\"FileCard — identity\", () => {\r\n test(\"renders the filename and mirrors it into title for the truncated case\", () => {\r\n const name =\r\n \"autorisation-de-depot-prealable-a-l-exportation-2025-IM-0042-revision-finale.pdf\";\r\n render(<FileCard name={name} />);\r\n\r\n /* The title attribute is the ONLY reason `name` is typed `string` rather\r\n * than ReactNode: once the CSS ellipsis hides the tail, the attribute is\r\n * what keeps the full filename reachable. Losing it is invisible in every\r\n * screenshot and only hurts the user with the long filename. */\r\n expect(screen.getByText(name)).toHaveAttribute(\"title\", name);\r\n });\r\n\r\n test(\"renders the meta line when supplied\", () => {\r\n render(<FileCard name=\"facture.pdf\" meta=\"1,24 Mo\" />);\r\n expect(screen.getByText(\"1,24 Mo\")).toBeInTheDocument();\r\n });\r\n\r\n test(\"omits the meta line entirely when absent or empty\", () => {\r\n /* Not rendered rather than rendered blank: an empty second line reads as\r\n * missing data, and it changes the row height, which breaks the alignment\r\n * of a list of files. Both `undefined` and `\"\"` are covered because a host\r\n * formatting a byte count from an optional field produces either.\r\n *\r\n * Queried by testid, not by walking parentElement: \"absent, not empty\"\r\n * cannot be expressed as a text query, and the repo's eslint config forbids\r\n * direct node access — correctly, since a structural child count would also\r\n * break the moment the component gains an unrelated third line. */\r\n const { rerender } = render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.getByText(\"a.pdf\")).toBeInTheDocument();\r\n expect(screen.queryByTestId(\"filecard-meta\")).not.toBeInTheDocument();\r\n\r\n rerender(<FileCard name=\"a.pdf\" meta=\"\" />);\r\n expect(screen.queryByTestId(\"filecard-meta\")).not.toBeInTheDocument();\r\n\r\n rerender(<FileCard name=\"a.pdf\" meta=\"248 Ko\" />);\r\n expect(screen.getByTestId(\"filecard-meta\")).toHaveTextContent(\"248 Ko\");\r\n });\r\n});\r\n\r\ndescribe(\"FileCard — actions slot\", () => {\r\n test(\"renders host-supplied actions\", () => {\r\n render(\r\n <FileCard\r\n name=\"a.pdf\"\r\n actions={<button type=\"button\">Télécharger</button>}\r\n />\r\n );\r\n expect(screen.getByRole(\"button\", { name: \"Télécharger\" })).toBeInTheDocument();\r\n });\r\n\r\n test(\"renders no action cluster when actions is absent\", () => {\r\n render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.queryByRole(\"button\")).not.toBeInTheDocument();\r\n });\r\n\r\n test(\"does not invent any action of its own\", () => {\r\n /* Regression guard on the component's SCOPE, not its rendering. FileCard is\r\n * presentational: a download link, a delete confirmation and a blob URL all\r\n * belong to the host. If a future change adds a built-in button, this fails\r\n * — which is the intent. */\r\n render(<FileCard name=\"a.pdf\" meta=\"1 Ko\" />);\r\n expect(screen.queryByRole(\"button\")).not.toBeInTheDocument();\r\n expect(screen.queryByRole(\"link\")).not.toBeInTheDocument();\r\n });\r\n});\r\n\r\ndescribe(\"FileCard — accessibility\", () => {\r\n test(\"the leading icon is hidden from assistive tech\", () => {\r\n /* R-Ar.2 — the filename IS the accessible name; a decorative glyph carrying\r\n * one would make a screen reader announce the row twice. The wrapper has no\r\n * role and no name by design, so there is no semantic query for it: the\r\n * component exposes a stable testid for exactly this assertion. */\r\n render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.getByTestId(\"filecard-icon\")).toHaveAttribute(\"aria-hidden\", \"true\");\r\n });\r\n\r\n test(\"a custom icon replaces the default and stays hidden\", () => {\r\n /* The second assertion is the one that matters: a host passing its own glyph\r\n * must not be able to leak it into the accessibility tree. */\r\n render(<FileCard name=\"a.pdf\" icon={<svg data-testid=\"custom\" />} />);\r\n expect(screen.getByTestId(\"custom\")).toBeInTheDocument();\r\n expect(screen.getByTestId(\"filecard-icon\")).toHaveAttribute(\"aria-hidden\", \"true\");\r\n });\r\n\r\n test(\"carries no role of its own\", () => {\r\n /* The card is not interactive — only its actions are. Giving it role=button\r\n * or wrapping it in a group without an accessible name would add noise to\r\n * the SR tree. Asserted so a future \"make the whole row clickable\" change\r\n * has to be a deliberate decision rather than a drive-by. */\r\n render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.queryByRole(\"button\")).not.toBeInTheDocument();\r\n expect(screen.queryByRole(\"group\")).not.toBeInTheDocument();\r\n });\r\n});\r\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"FileCard.test.js","names":["render","screen","FileCard","jsx","_jsx","describe","test","name","expect","getByText","toHaveAttribute","meta","toBeInTheDocument","_render","rerender","queryByTestId","not","getByTestId","toHaveTextContent","actions","type","children","getByRole","queryByRole","icon"],"sources":["../../../../src/lib/v2/__tests__/FileCard.test.tsx"],"sourcesContent":["import \"@testing-library/jest-dom\";\r\nimport { render, screen } from \"@testing-library/react\";\r\nimport FileCard from \"../components/FileCard\";\r\n\r\n/**\r\n * FileCard — contract tests.\r\n *\r\n * WHY THIS FILE EXISTS\r\n * --------------------\r\n * §10.3 R-G.6 MUST — \"Snapshot Storybook + tests Jest/Testing-Library\". The\r\n * component was extracted from THREE hand-written copies found in addpe-fe\r\n * (Attachment, AutorisationPreviewDrawer, AutorisationDetail), which had drifted\r\n * to three different paddings for one concept. Consolidating them means a single\r\n * change now moves every attachment row in the fleet, so the contracts below are\r\n * the ones that must not move silently.\r\n *\r\n * NOT COVERED — deliberately\r\n * The width negotiation is the most fragile part of this component and it is\r\n * NOT tested here: `text-overflow: ellipsis`, `minWidth: 0` on the identity\r\n * block and `flexShrink: 0` on the action cluster are LAYOUT, and jsdom computes\r\n * no layout. A test asserting them would pass while the card is broken on a\r\n * phone — worse than no coverage, because it would look like coverage. That\r\n * contract lives in FileCard.stories.tsx (\"Long filename\"), which is where a\r\n * browser can actually check it. Same reasoning as MobileRowCard.test.tsx\r\n * records for R-G.3.\r\n */\r\ndescribe(\"FileCard — identity\", () => {\r\n test(\"renders the filename and mirrors it into title for the truncated case\", () => {\r\n const name =\r\n \"autorisation-de-depot-prealable-a-l-exportation-2025-IM-0042-revision-finale.pdf\";\r\n render(<FileCard name={name} />);\r\n\r\n /* The title attribute is the ONLY reason `name` is typed `string` rather\r\n * than ReactNode: once the CSS ellipsis hides the tail, the attribute is\r\n * what keeps the full filename reachable. Losing it is invisible in every\r\n * screenshot and only hurts the user with the long filename. */\r\n expect(screen.getByText(name)).toHaveAttribute(\"title\", name);\r\n });\r\n\r\n test(\"renders the meta line when supplied\", () => {\r\n render(<FileCard name=\"facture.pdf\" meta=\"1,24 Mo\" />);\r\n expect(screen.getByText(\"1,24 Mo\")).toBeInTheDocument();\r\n });\r\n\r\n test(\"omits the meta line entirely when absent or empty\", () => {\r\n /* Not rendered rather than rendered blank: an empty second line reads as\r\n * missing data, and it changes the row height, which breaks the alignment\r\n * of a list of files. Both `undefined` and `\"\"` are covered because a host\r\n * formatting a byte count from an optional field produces either.\r\n *\r\n * Queried by testid, not by walking parentElement: \"absent, not empty\"\r\n * cannot be expressed as a text query, and the repo's eslint config forbids\r\n * direct node access — correctly, since a structural child count would also\r\n * break the moment the component gains an unrelated third line. */\r\n const { rerender } = render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.getByText(\"a.pdf\")).toBeInTheDocument();\r\n expect(screen.queryByTestId(\"filecard-meta\")).not.toBeInTheDocument();\r\n\r\n rerender(<FileCard name=\"a.pdf\" meta=\"\" />);\r\n expect(screen.queryByTestId(\"filecard-meta\")).not.toBeInTheDocument();\r\n\r\n rerender(<FileCard name=\"a.pdf\" meta=\"248 Ko\" />);\r\n expect(screen.getByTestId(\"filecard-meta\")).toHaveTextContent(\"248 Ko\");\r\n });\r\n});\r\n\r\ndescribe(\"FileCard — actions slot\", () => {\r\n test(\"renders host-supplied actions\", () => {\r\n render(\r\n <FileCard\r\n name=\"a.pdf\"\r\n actions={<button type=\"button\">Télécharger</button>}\r\n />\r\n );\r\n expect(screen.getByRole(\"button\", { name: \"Télécharger\" })).toBeInTheDocument();\r\n });\r\n\r\n test(\"renders no action cluster when actions is absent\", () => {\r\n render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.queryByRole(\"button\")).not.toBeInTheDocument();\r\n });\r\n\r\n test(\"does not invent any action of its own\", () => {\r\n /* Regression guard on the component's SCOPE, not its rendering. FileCard is\r\n * presentational: a download link, a delete confirmation and a blob URL all\r\n * belong to the host. If a future change adds a built-in button, this fails\r\n * — which is the intent. */\r\n render(<FileCard name=\"a.pdf\" meta=\"1 Ko\" />);\r\n expect(screen.queryByRole(\"button\")).not.toBeInTheDocument();\r\n expect(screen.queryByRole(\"link\")).not.toBeInTheDocument();\r\n });\r\n});\r\n\r\ndescribe(\"FileCard — accessibility\", () => {\r\n test(\"the leading icon is hidden from assistive tech\", () => {\r\n /* R-Ar.2 — the filename IS the accessible name; a decorative glyph carrying\r\n * one would make a screen reader announce the row twice. The wrapper has no\r\n * role and no name by design, so there is no semantic query for it: the\r\n * component exposes a stable testid for exactly this assertion. */\r\n render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.getByTestId(\"filecard-icon\")).toHaveAttribute(\"aria-hidden\", \"true\");\r\n });\r\n\r\n test(\"a custom icon replaces the default and stays hidden\", () => {\r\n /* The second assertion is the one that matters: a host passing its own glyph\r\n * must not be able to leak it into the accessibility tree. */\r\n render(<FileCard name=\"a.pdf\" icon={<svg data-testid=\"custom\" />} />);\r\n expect(screen.getByTestId(\"custom\")).toBeInTheDocument();\r\n expect(screen.getByTestId(\"filecard-icon\")).toHaveAttribute(\"aria-hidden\", \"true\");\r\n });\r\n\r\n test(\"carries no role of its own\", () => {\r\n /* The card is not interactive — only its actions are. Giving it role=button\r\n * or wrapping it in a group without an accessible name would add noise to\r\n * the SR tree. Asserted so a future \"make the whole row clickable\" change\r\n * has to be a deliberate decision rather than a drive-by. */\r\n render(<FileCard name=\"a.pdf\" />);\r\n expect(screen.queryByRole(\"button\")).not.toBeInTheDocument();\r\n expect(screen.queryByRole(\"group\")).not.toBeInTheDocument();\r\n });\r\n});\r\n"],"mappings":"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","ignoreList":[]}
|