test: pin the surviving marked-t keys and document the known test limits (#437) #473

Open
maximus wants to merge 3 commits from issue-437-test-pins into issue-393-category-hierarchy
6 changed files with 32 additions and 7 deletions
Showing only changes of commit ae54406354 - Show all commits

View file

@ -58,7 +58,7 @@ Comme sqlx pour le profil par défaut. Une version qui échoue sur une clé étr
### 7. L'échec reste ouvert ### 7. L'échec reste ouvert
Un rattrapage interrompu est écrit au Journal et le profil s'ouvre comme avant #390. Les messages nomment des fichiers, jamais le dossier : il contient le nom d'utilisateur du système, et une ligne d'erreur du Journal peut être jointe à une rétroaction (Loi 25). Un rattrapage qui s'arrête après sa copie la nomme quand même : `catch_up_backup` (nom de fichier seul) est rempli dès que la copie existe, et la ligne d'erreur du Journal finit par « copy of the previous state kept as … » (#395). Côté TypeScript, `withoutFolders` réduit chaque chemin d'une ligne d'erreur, message de repli compris, à son dernier segment (le nom de fichier). Un chemin part d'un début reconnu (lecteur `C:\`, `/`, `~/`, `./`, partage `\\`, ou le `<profile folder>` de `scrub`) et court jusqu'à son dernier séparateur, espaces compris, pour qu'un dossier comme `C:\Users\Jean Dupont\` ou `/home/marie claire/` ne laisse aucun mot derrière lui ; il s'arrête à un saut de ligne, un guillemet double, une apostrophe inverse, `;`, `|`, une virgule ou un deux-points suivis d'un blanc, une parenthèse fermante qui n'est pas suivie d'un séparateur (`Program Files (x86)\` continue), ou un blanc suivi d'un autre chemin. Une apostrophe placée juste avant le chemin reste autour du nom de fichier (`'C:\Users\Bob\x.db'` donne `'x.db'`). Quand rien ne l'arrête, la règle est prudente : elle retire le texte qui suit jusqu'au dernier séparateur de la ligne plutôt que de laisser passer un dossier. Limite : un chemin relatif sans début reconnu (`dossier/sous dossier/x.db`) n'a que ses séparateurs remplacés par `-` ; il ne part pas du dossier personnel, donc n'en contient pas le nom. Tout autre séparateur devient `-` : aucune ligne d'erreur ne contient `/` ni `\`. Un rattrapage interrompu est écrit au Journal et le profil s'ouvre comme avant #390. Les messages nomment des fichiers, jamais le dossier : il contient le nom d'utilisateur du système, et une ligne d'erreur du Journal peut être jointe à une rétroaction (Loi 25). Un rattrapage qui s'arrête après sa copie la nomme quand même : `catch_up_backup` (nom de fichier seul) est rempli dès que la copie existe, et la ligne d'erreur du Journal finit par « copy of the previous state kept as … » (#395). Côté TypeScript, `withoutFolders` réduit chaque chemin d'une ligne d'erreur, message de repli compris, à son dernier segment (le nom de fichier). Un chemin part d'un début reconnu (lecteur `C:\`, `/`, `~/`, `./`, partage `\\`, ou le `<profile folder>` de `scrub`) et court jusqu'à son dernier séparateur, espaces compris, pour qu'un dossier comme `C:\Users\Jean Dupont\` ou `/home/marie claire/` ne laisse aucun mot derrière lui ; il s'arrête à un saut de ligne, un guillemet double, une apostrophe inverse, `;`, `|`, une virgule ou un deux-points suivis d'un blanc, une parenthèse fermante qui n'est pas suivie d'un séparateur (`Program Files (x86)\` continue), ou un blanc suivi d'un autre chemin. Une apostrophe placée juste avant le chemin reste autour du nom de fichier (`'C:\Users\Bob\x.db'` donne `'x.db'`). Quand rien ne l'arrête, la règle est prudente : elle retire le texte qui suit jusqu'au dernier séparateur de la ligne plutôt que de laisser passer un dossier. Deux limites (précisées par #437). Un chemin relatif sans début reconnu garde l'ancien repli, d'un blanc à l'autre : un morceau qui porte deux séparateurs ou plus est réduit à son dernier segment (`dossier/sous/x.db` donne `x.db`), un morceau qui n'en porte qu'un voit son séparateur remplacé par `-`, si bien que les mots d'un dossier qui contient un blanc restent (`dossier/sous dossier/x.db` donne `dossier-sous dossier-x.db`) ; un tel chemin ne part pas du dossier personnel, donc n'en contient pas le nom. Et un nom de dossier qui contient l'un des arrêts (une apostrophe inverse, `;`, `|`, une parenthèse fermante suivie d'autre chose qu'un séparateur…) arrête le chemin là : ce nom, ou son début, reste dans la ligne, et ce peut être un nom d'utilisateur (`C:\Users\Jean (Work) Smith\AppData\x.db` donne `Jean (Work) x.db`). Tout autre séparateur devient `-` : aucune ligne d'erreur ne contient `/` ni `\`.
### 8. Un seul dossier des bases ### 8. Un seul dossier des bases

View file

@ -659,6 +659,12 @@ Garde : `src/__integration__/i18n-mock-guard.test.ts` lit tous les `*.test.ts(x)
`CategoryCombobox` rend sa liste ouverte dans un portail sur `document.body`, en position fixe au-dessus de toutes les fenêtres (`z-[300]`) : aucun conteneur défilant ne la coupe. Fermé, il ne rend que `<div class="relative"><input …/></div>`, sans portail, et son rendu statique n'en demande rien. La règle de placement (sous le champ, au-dessus quand la place manque en dessous et suffit au-dessus) et les écouteurs qui gardent la liste sur son champ pendant qu'elle est ouverte (`scroll` en capture, `resize`, retirés avec elle) sont des fonctions pures de `src/components/shared/placeListbox.ts`, testées sans DOM. Le composant ouvert, lui, ne se rend pas sans DOM : `renderToStaticMarkup` le rend fermé et le harnais refuse ses `useId` et `useLayoutEffect` ; le reste (portail, clic dans la liste, clavier) se vérifie dans l'application. La liste n'étant plus dans le DOM de son conteneur, un écouteur natif de clic extérieur posé par un conteneur la verrait dehors : celui du combobox la compte comme intérieure. `CategoryCombobox` rend sa liste ouverte dans un portail sur `document.body`, en position fixe au-dessus de toutes les fenêtres (`z-[300]`) : aucun conteneur défilant ne la coupe. Fermé, il ne rend que `<div class="relative"><input …/></div>`, sans portail, et son rendu statique n'en demande rien. La règle de placement (sous le champ, au-dessus quand la place manque en dessous et suffit au-dessus) et les écouteurs qui gardent la liste sur son champ pendant qu'elle est ouverte (`scroll` en capture, `resize`, retirés avec elle) sont des fonctions pures de `src/components/shared/placeListbox.ts`, testées sans DOM. Le composant ouvert, lui, ne se rend pas sans DOM : `renderToStaticMarkup` le rend fermé et le harnais refuse ses `useId` et `useLayoutEffect` ; le reste (portail, clic dans la liste, clavier) se vérifie dans l'application. La liste n'étant plus dans le DOM de son conteneur, un écouteur natif de clic extérieur posé par un conteneur la verrait dehors : celui du combobox la compte comme intérieure.
### Limites connues (#437)
- **Emplacements préréglés du harnais.** Le harnais donne un emplacement à chaque appel de hook, tous types confondus (`useState`, `useReducer`, `useRef`, `useMemo`, `useCallback`, `useEffect`), dans l'ordre des appels : c'est le helper `slot` de `fakeReact`. Un test qui prérègle un emplacement par sa position (`harness.slots[0] = …`) dépend donc de l'ordre des hooks du composant : un hook ajouté avant, de n'importe quel type, décale les emplacements sans rien dire, et la valeur préréglée tombe sur un autre hook. Préréglages actuels, chacun commenté sur place : `PriceFetchConsentToggle.test.tsx` (emplacements 0 à 2), `PriceFetchControl.test.tsx` (la fenêtre de consentement ouverte), `useOnboardingChecklist.wiring.test.ts` (`mountWith` et deux écritures directes) et `SystemsSettingsPage.test.tsx` ; `OnboardingStepCard.test.tsx` lit le `useRef` de l'emplacement 0. Qui ajoute un hook avant ces emplacements met le test à jour. Un test qui ouvre un état par un clic dans l'arbre rendu (`TransactionTable.wiring.test.ts`) n'a pas cette dépendance.
- **`withoutFolders`.** Ses arrêts et ses deux limites (chemin relatif sans début reconnu ; nom de dossier qui contient l'un des arrêts) sont dans l'[ADR 0021](adr/0021-migrations-profils-secondaires.md), § 7.
- **Mesure du rattrapage.** Le test `#[ignore]` `a_large_profile_catch_up_duration` (`profile_db.rs`) ne tourne pas en CI et n'asserte rien : il imprime une durée. Sa mesure, prise sur un SSD NVMe, et le risque qu'elle ne couvre pas (disque lent, antivirus qui inspecte la copie) sont dans l'[ADR 0021](adr/0021-migrations-profils-secondaires.md), « Négatives et points de vigilance », point « Le délai de démarrage couvre le rattrapage du profil actif ».
## CI/CD ## CI/CD
Quatre workflows Forgejo Actions dans `.forgejo/workflows/`. Le runner est à **capacité 1** : les jobs se suivent, ils ne tournent pas en parallèle. Quatre workflows Forgejo Actions dans `.forgejo/workflows/`. Le runner est à **capacité 1** : les jobs se suivent, ils ne tournent pas en parallèle.

View file

@ -226,7 +226,13 @@ describe("PriceFetchConsentToggle — displayed texts", () => {
harness.clear(); harness.clear();
}); });
/** One render once loaded: slots by call order, hasConsent, showConfirm, loading. */ /**
* One render once loaded: slots by call order, hasConsent, showConfirm, loading.
* The presets depend on the order of the toggle's hook calls: slots 0 to 2 are
* its first three hooks, and a hook of any kind added before them shifts the
* slots without a word, each preset then landing on another hook (one slot per
* hook call, every kind: the `slot` helper of `fakeReact`, src/test/hookHarness.ts).
*/
function markup(premium: boolean, consent: boolean, confirming: boolean): string { function markup(premium: boolean, consent: boolean, confirming: boolean): string {
setPremium(premium); setPremium(premium);
harness.slots[0] = consent; harness.slots[0] = consent;

View file

@ -70,7 +70,15 @@ const ready: OnboardingChecklistState = [
/** One render of the mounted provider. */ /** One render of the mounted provider. */
const render = () => harness.render(useOnboardingChecklist); const render = () => harness.render(useOnboardingChecklist);
/** The state the provider mounts with: its `useReducer` is its first React hook. */ /**
* The state the provider mounts with: its `useReducer` is its first React hook
* (`useEntitlement` and `useLocation` are mocked, so they take no slot). This
* preset, like the writes to `harness.slots[0]` further down, depends on the
* order of the hook's calls to React: a hook of any kind added before the
* reducer shifts the slots without a word, and the state lands on another hook
* (one slot per hook call, every kind: the `slot` helper of `fakeReact`,
* src/test/hookHarness.ts).
*/
const mountWith = (state: OnboardingChecklistState) => { const mountWith = (state: OnboardingChecklistState) => {
harness.slots[0] = state; harness.slots[0] = state;
}; };

View file

@ -59,6 +59,10 @@ beforeEach(() => {
describe("SystemsSettingsPage — order of the sections (#372)", () => { describe("SystemsSettingsPage — order of the sections (#372)", () => {
it("renders Version, Update, the Journal, then the version history and the data notice", () => { it("renders Version, Update, the Journal, then the version history and the data notice", () => {
vi.mocked(getVersion).mockResolvedValue("0.17.2"); vi.mocked(getVersion).mockResolvedValue("0.17.2");
// Slot 0 is the page's first hook, its `version` state. This preset, and the
// next test's, depend on the order of the page's hook calls: a hook of any
// kind added before it shifts the slots without a word (one slot per hook
// call, every kind: the `slot` helper of `fakeReact`, src/test/hookHarness.ts).
harness.slots[0] = "0.17.2"; // the version, already read harness.slots[0] = "0.17.2"; // the version, already read
const html = renderToStaticMarkup(render()); const html = renderToStaticMarkup(render());
const at = (marker: string) => { const at = (marker: string) => {

View file

@ -380,10 +380,11 @@ function lastSeparator(text: string, from: number): number {
* (Loi 25). A folder may contain blanks (`C:\Users\Jean Dupont\…`, * (Loi 25). A folder may contain blanks (`C:\Users\Jean Dupont\…`,
* `/home/marie claire/…`): from its start, a path runs to its LAST separator * `/home/marie claire/…`): from its start, a path runs to its LAST separator
* before a line break, a double quote, a backtick, `;`, `|`, a `,` or `:` * before a line break, a double quote, a backtick, `;`, `|`, a `,` or `:`
* followed by a blank, a closing parenthesis, or a blank followed by another * followed by a blank, a closing parenthesis not followed by a separator, or a
* path. When nothing ends it, the rule is conservative: text after a path is * blank followed by another path. When nothing ends it, the rule is
* dropped up to the last separator of the line (`/a/x.db then I/O` gives * conservative: text after a path is dropped up to the last separator of the
* `O`), rather than letting a folder through. A quote right before the path is * line (`/a/x.db then I/O` gives `O`), rather than letting a folder through.
* Limits in ADR 0021, section 7. A quote right before the path is
* not part of it, so both quotes stay around the file name * not part of it, so both quotes stay around the file name
* (`'C:\Users\Bob\x.db'` gives `'x.db'`). Any other separator becomes `-` (SQLite's "disk I/O error" reads * (`'C:\Users\Bob\x.db'` gives `'x.db'`). Any other separator becomes `-` (SQLite's "disk I/O error" reads
* "disk I-O error"), so an error line never contains a `/` nor a `\` (#395). * "disk I-O error"), so an error line never contains a `/` nor a `\` (#395).