diff --git a/README.md b/README.md index 170184f..13e6ea1 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,11 @@ > Fie-toi toujours à tes analyses de sang et aux consignes de ton > endocrinologue. -- **Statut** : web v1.7.0 — parité fonctionnelle avec l'Android v1.7.0 - (unités des axes portées ; l'auto-backup journalier Android est - structurellement non porté, cf §12 — l'export manuel couvre la donnée) · - **140 tests verts** · E2E navigateur ✅ · conteneur testé en +- **Statut** : web v1.7.1 — parité fonctionnelle avec l'Android v1.7.1 + (unités des axes + fix notes des analyses ; l'auto-backup journalier + Android est structurellement non porté, cf §12 — l'export manuel couvre + la donnée) · + **145 tests verts** · E2E navigateur ✅ · conteneur testé en configuration durcie ✅ · lint/i18n ✅ - **Journal des versions web** : [docs/CHANGELOG.md](docs/CHANGELOG.md) - **Releases** : [gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases](https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ce89349..1f54e4e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,24 @@ > section la plus récente EN PREMIER ; ce fichier est lu par le dialog > « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`). +## [1.7.1] — 2026-09-16 + +### Corrigé — une seule note sur deux s'affichait dans les Analyses + +- **Bug remonté** (identique Android) : dans une prise de sang E2+T, quand + les deux entrées portent des notes **distinctes** (ex. une note clinique + sur l'E2, une note « DHT » sur la T), une seule s'affichait — l'autre + était bien conservée mais perdue à l'affichage. +- **Cause** : l'ancien code ne prenait que la première note non vide du + groupe (hypothèse historique « toutes identiques » — vraie à l'époque du + dialog de création, fausse depuis l'édition unitaire). +- **Fix** : `labNotesForDisplay` (helper PUR dans `format.js`, miroir du + Kotlin) — notes distinctes → une ligne par note préfixée du marqueur + (« E2 : … » / « T : … ») ; note identique → une seule ligne ; vides ignorées. +- Épinglé par : 5 tests unitaires (miroir du Kotlin) + **scénario E2E** (la + dernière paire de démo porte des notes distinctes, les deux s'affichent + sur #labs) + régression data-driven Android sur l'export réel. + ## [1.7.0] — 2026-09-13 ### Unités des axes du graphique — portage de l'Android v1.7.0 (versions sync) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index a99ccd8..3c4d86a 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -243,6 +243,8 @@ HormoneTrack-web/ (dépôt séparé) │ ├── pk-engine.test.js ← PharmacokineticEngineTest.kt (+ #35, §6.bis) │ ├── calibration.test.js ← CalibrationPerModel/ScaleFactorWhsahRepro/ │ │ V120Features (fixes #60/#61, périodes d'ester) +│ ├── lab-notes.test.js ← labNotesForDisplay (v1.7.1 : notes d'une +│ │ prise E2+T — les DEUX distinctes affichées) │ ├── lab-trajectory-model.test.js ← LabTrajectoryModelTest.kt (v1.5.0 │ │ « Tracé labs » + v1.6.0 prolongation — │ │ passage exact, garde #61, horizon cutoff) diff --git a/js/ui/app.js b/js/ui/app.js index ef82af7..386c727 100644 --- a/js/ui/app.js +++ b/js/ui/app.js @@ -347,12 +347,22 @@ function maybeLoadDemoData() { esterType: null, }); } - // Labs E2/T plausibles (1 prise de sang toutes les 2 semaines) + // Labs E2/T plausibles (1 prise de sang toutes les 2 semaines). + // v1.7.1 : la DERNIÈRE paire porte des notes DISTINCTES (E2 ≠ T) — cas + // réel du bug « une seule note sur deux s'affichait » (fix labNotesForDisplay) + // et fixture du scénario E2E correspondant. const labs = [[280, 0.35], [300, 0.31], [260, 0.4], [290, 0.33], [275, 0.36], [305, 0.3]]; labs.forEach(([e2, tVal], i) => { const ts = now - (11 - i * 2) * 7 * DAY; - store.upsertLabResult({ marker: 'E2', value: e2, unit: 'pg/mL', timestamp: ts, notes: null }); - store.upsertLabResult({ marker: 'T', value: Math.round(tVal * 100), unit: 'ng/dL', timestamp: ts, notes: null }); + const isLastPair = i === labs.length - 1; + store.upsertLabResult({ + marker: 'E2', value: e2, unit: 'pg/mL', timestamp: ts, + notes: isLastPair ? 'Prise de sang après des maux de tête.' : null, + }); + store.upsertLabResult({ + marker: 'T', value: Math.round(tVal * 100), unit: 'ng/dL', timestamp: ts, + notes: isLastPair ? 'DHT : 12 ng/dL' : null, + }); }); AppLog.log('demo', 'données de démonstration chargées (?demo=1)'); } diff --git a/js/ui/labs.js b/js/ui/labs.js index 60f8056..8788ec1 100644 --- a/js/ui/labs.js +++ b/js/ui/labs.js @@ -14,7 +14,7 @@ import { store } from '../data/store.js'; import { t } from '../util/i18n.js'; -import { formatLabValue, formatDateTime } from '../util/format.js'; +import { formatLabValue, formatDateTime, labNotesForDisplay } from '../util/format.js'; import { el, confirmDialog, fab, showDialog } from './components.js'; import { openLabDialog } from './dialogs.js'; @@ -70,8 +70,14 @@ export function renderLabs(container) { const main = el('div', { class: 'main' }); main.appendChild(el('div', { class: 'title' }, headline)); main.appendChild(el('div', { class: 'sub' }, formatDateTime(ts))); - const firstNotes = group.map((g) => g.notes).find((n) => n && n.trim() !== ''); - if (firstNotes) main.appendChild(el('div', { class: 'sub' }, firstNotes)); + // v1.7.1 : TOUTES les notes distinctes du groupe (préfixées du marqueur + // quand elles diffèrent) — l'ancien code ne prenait que la PREMIÈRE note + // non vide du groupe (« une note sur deux » : la note T disparaissait de + // l'affichage quand la note E2 existait aussi, bien qu'elle fût + // sauvegardée). Même fix que labNotesForDisplay côté Android. + for (const note of labNotesForDisplay(group)) { + main.appendChild(el('div', { class: 'sub' }, note)); + } item.appendChild(main); // Tap → sélecteur E2/T si paire, édition directe sinon diff --git a/js/ui/settings.js b/js/ui/settings.js index 78ff862..286546e 100644 --- a/js/ui/settings.js +++ b/js/ui/settings.js @@ -31,7 +31,7 @@ import { el, showDialog, field, switchRow, filterChip, toast } from './component * (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`). * v1.4.10 = portage initial de l'Android v1.4.10. */ -export const WEB_VERSION = '1.7.0'; +export const WEB_VERSION = '1.7.1'; /** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */ function parseNumOrNull(text) { diff --git a/js/util/format.js b/js/util/format.js index 1a587b9..88f4bfb 100644 --- a/js/util/format.js +++ b/js/util/format.js @@ -172,3 +172,44 @@ export function inputValuesToMs(dateISO, timeHM) { const ms = new Date(y, mo - 1, d, h, mi, 0, 0).getTime(); return Number.isFinite(ms) ? ms : null; } + +/** + * Notes à AFFICHER pour un groupe de labs (v1.7.1 — miroir de + * `labNotesForDisplay` Android, fix du bug « une seule note sur deux + * s'affiche »). Historique : le dialog de création mettait LA MÊME note aux + * deux entrées d'une prise E2+T ; l'édition unitaire a cassé cette + * hypothèse → l'ancien affichage (`.find()` première non vide) perdait la + * note de la seconde entrée (conservée en base, juste non affichée). + * + * Règles (identiques à l'Android) : + * - notes vides/blanches ignorées ; ordre du groupe préservé (E2 avant T) ; + * - texte identique sur plusieurs entrées → une seule ligne (dédupliqué) ; + * - notes distinctes → une ligne par note, préfixée du marqueur + * (« E2 : … » / « T : … ») ; + * - une seule note distincte → brute (comportement historique préservé). + * + * PUR (aucun DOM) → testé en Node (tests/lab-notes.test.js). + * + * @param {{marker:string, notes?:string|null}[]} group labs du même timestamp + * @returns {string[]} lignes de notes à afficher (vide si aucune) + */ +export function labNotesForDisplay(group) { + const notes = group + .map((lab) => (lab.notes && String(lab.notes).trim() !== '' + ? [String(lab.marker).toUpperCase(), String(lab.notes).trim()] + : null)) + .filter(Boolean); + if (notes.length === 0) return []; + // Déduplication (première occurrence de chaque texte, marqueur de sa + // propre entrée conservé pour le préfixe) + const seen = new Set(); + const distinctPairs = []; + for (const [marker, text] of notes) { + if (!seen.has(text)) { + seen.add(text); + distinctPairs.push([marker, text]); + } + } + if (distinctPairs.length === 1) return [distinctPairs[0][1]]; + return distinctPairs.map(([marker, text]) => `${marker} : ${text}`); +} diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index fd3cb23..1262c08 100644 --- a/scripts/e2e.mjs +++ b/scripts/e2e.mjs @@ -131,7 +131,7 @@ async function main() { ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)'); if (dialog) { const dlgText = await dialog.textContent(); - ok(dlgText.includes('Version 1.7.0'), 'dialog : section de la version courante'); + ok(dlgText.includes('Version 1.7.1'), 'dialog : section de la version courante'); const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong'); ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut'); await page.click('.dialog .actions button'); @@ -271,6 +271,13 @@ async function main() { await page.waitForTimeout(300); text = await page.textContent('main.content'); ok(text.includes('E2') && text.includes('ng/dL'), 'Labs : prise groupée E2 + T (ng/dL converti au rendu seulement)'); + // v1.7.1 : la dernière paire de démo porte des notes DISTINCTES (E2 ≠ T) + // → les DEUX doivent s'afficher, préfixées de leur marqueur (fix du bug + // « une seule note sur deux ») + ok(text.includes('E2 : Prise de sang après des maux de tête.'), + 'Labs : note E2 affichée (préfixée)'); + ok(text.includes('T : DHT : 12 ng/dL'), + 'Labs : note T affichée (préfixée) — les deux notes coexistent'); await page.goto(`${base}/#treatments`, { waitUntil: 'load' }); await page.waitForTimeout(300); @@ -282,7 +289,7 @@ async function main() { await page.waitForTimeout(300); text = await page.textContent('main.content'); ok(text.includes('Langue'), 'Paramètres : carte langue'); - ok(text.includes('Version 1.7.0 (web)'), 'Paramètres : version affichée'); + ok(text.includes('Version 1.7.1 (web)'), 'Paramètres : version affichée'); ok(text.includes('100 % local'), 'Paramètres : note vie privée'); await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') }); diff --git a/tests/lab-notes.test.js b/tests/lab-notes.test.js new file mode 100644 index 0000000..8535c5d --- /dev/null +++ b/tests/lab-notes.test.js @@ -0,0 +1,45 @@ +/** + * Tests des NOTES d'un groupe de labs (v1.7.1 — miroir de `LabsGroupingTest` + * Android, [labNotesForDisplay]) — fix du bug « une seule note sur deux + * s'affiche » : quand E2 et T portent des notes DISTINCTES, les deux doivent + * apparaître (préfixées du marqueur) ; la note identique (dialog commun) ne + * s'affiche qu'une fois ; les notes vides/malformées sont ignorées. + */ + +import { test, describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { labNotesForDisplay } from '../js/util/format.js'; + +const lab = (marker, notes) => ({ marker, notes }); + +describe('labNotesForDisplay (miroir Android v1.7.1)', () => { + test('les notes distinctes de E2 et T sont TOUTES affichées, préfixées', () => { + const group = [ + lab('E2', 'Analyse faite suite à des maux de tête.'), + lab('T', 'DHT : 12 ng/dL'), + ]; + assert.deepEqual(labNotesForDisplay(group), [ + 'E2 : Analyse faite suite à des maux de tête.', + 'T : DHT : 12 ng/dL', + ]); + }); + + test('la note identique sur les deux entrées n\'est affichée qu\'une fois', () => { + const group = [lab('E2', 'Prise à jeun'), lab('T', 'Prise à jeun')]; + assert.deepEqual(labNotesForDisplay(group), ['Prise à jeun']); + }); + + test('une seule note reste brute (comportement historique préservé)', () => { + const group = [lab('E2', 'Seule note'), lab('T', null)]; + assert.deepEqual(labNotesForDisplay(group), ['Seule note']); + }); + + test('notes vides/blanches ignorées, ordre du groupe préservé', () => { + const group = [lab('E2', ' '), lab('T', 'note T'), lab('PRL', 'note PRL')]; + assert.deepEqual(labNotesForDisplay(group), ['T : note T', 'PRL : note PRL']); + }); + + test('aucune note → aucune ligne', () => { + assert.deepEqual(labNotesForDisplay([lab('E2', null), lab('T', '')]), []); + }); +});