v1.7.1 : fix « une seule note sur deux » dans les Analyses (miroir Android)

- format.js : labNotesForDisplay (helper PUR, miroir du Kotlin) — notes
  DISTINCTES d'une prise E2+T → une ligne par note préfixée du marqueur
  (« E2 : … » / « T : … ») ; note identique (dialog commun) → dédupliquée ;
  vides ignorées ; note seule → brute (v1.0 préservé). L'ancien code
  (.find() première non vide) perdait la note T à l'affichage quand la
  note E2 existait aussi (conservée en base, juste non montrée).
- labs.js : affiche TOUTES les lignes du helper (l'ancienne n'en rendait
  qu'une).
- app.js (seed ?demo=1) : la dernière paire E2/T porte des notes
  distinctes → fixture du scénario E2E.
- E2E : les deux notes préfixées s'affichent sur #labs.
- tests/lab-notes.test.js : 5 tests miroirs du Kotlin (145 verts au total).
- CHANGELOG [1.7.1] (l'oubli de cette entrée aurait laissé le dialog
  « Nouveautés » vide — attrapé par l'E2E, leçon #46 miroir), §4 arbre
  tests, README. WEB_VERSION 1.7.1.
This commit is contained in:
Siphonight 2026-09-17 12:30:59 +02:00
parent d822af2dd2
commit 1957b73e3c
9 changed files with 143 additions and 13 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)');
}

View File

@ -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

View File

@ -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) {

View File

@ -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}`);
}

View File

@ -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') });

45
tests/lab-notes.test.js Normal file
View File

@ -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', '')]), []);
});
});