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:
parent
d822af2dd2
commit
1957b73e3c
@ -17,10 +17,11 @@
|
|||||||
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
|
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
|
||||||
> endocrinologue.
|
> endocrinologue.
|
||||||
|
|
||||||
- **Statut** : web v1.7.0 — parité fonctionnelle avec l'Android v1.7.0
|
- **Statut** : web v1.7.1 — parité fonctionnelle avec l'Android v1.7.1
|
||||||
(unités des axes portées ; l'auto-backup journalier Android est
|
(unités des axes + fix notes des analyses ; l'auto-backup journalier
|
||||||
structurellement non porté, cf §12 — l'export manuel couvre la donnée) ·
|
Android est structurellement non porté, cf §12 — l'export manuel couvre
|
||||||
**140 tests verts** · E2E navigateur ✅ · conteneur testé en
|
la donnée) ·
|
||||||
|
**145 tests verts** · E2E navigateur ✅ · conteneur testé en
|
||||||
configuration durcie ✅ · lint/i18n ✅
|
configuration durcie ✅ · lint/i18n ✅
|
||||||
- **Journal des versions web** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
- **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)
|
- **Releases** : [gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases](https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases)
|
||||||
|
|||||||
@ -5,6 +5,24 @@
|
|||||||
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog
|
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog
|
||||||
> « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`).
|
> « 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
|
## [1.7.0] — 2026-09-13
|
||||||
|
|
||||||
### Unités des axes du graphique — portage de l'Android v1.7.0 (versions sync)
|
### Unités des axes du graphique — portage de l'Android v1.7.0 (versions sync)
|
||||||
|
|||||||
@ -243,6 +243,8 @@ HormoneTrack-web/ (dépôt séparé)
|
|||||||
│ ├── pk-engine.test.js ← PharmacokineticEngineTest.kt (+ #35, §6.bis)
|
│ ├── pk-engine.test.js ← PharmacokineticEngineTest.kt (+ #35, §6.bis)
|
||||||
│ ├── calibration.test.js ← CalibrationPerModel/ScaleFactorWhsahRepro/
|
│ ├── calibration.test.js ← CalibrationPerModel/ScaleFactorWhsahRepro/
|
||||||
│ │ V120Features (fixes #60/#61, périodes d'ester)
|
│ │ 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
|
│ ├── lab-trajectory-model.test.js ← LabTrajectoryModelTest.kt (v1.5.0
|
||||||
│ │ « Tracé labs » + v1.6.0 prolongation —
|
│ │ « Tracé labs » + v1.6.0 prolongation —
|
||||||
│ │ passage exact, garde #61, horizon cutoff)
|
│ │ passage exact, garde #61, horizon cutoff)
|
||||||
|
|||||||
16
js/ui/app.js
16
js/ui/app.js
@ -347,12 +347,22 @@ function maybeLoadDemoData() {
|
|||||||
esterType: null,
|
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]];
|
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) => {
|
labs.forEach(([e2, tVal], i) => {
|
||||||
const ts = now - (11 - i * 2) * 7 * DAY;
|
const ts = now - (11 - i * 2) * 7 * DAY;
|
||||||
store.upsertLabResult({ marker: 'E2', value: e2, unit: 'pg/mL', timestamp: ts, notes: null });
|
const isLastPair = i === labs.length - 1;
|
||||||
store.upsertLabResult({ marker: 'T', value: Math.round(tVal * 100), unit: 'ng/dL', timestamp: ts, notes: null });
|
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)');
|
AppLog.log('demo', 'données de démonstration chargées (?demo=1)');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import { store } from '../data/store.js';
|
import { store } from '../data/store.js';
|
||||||
import { t } from '../util/i18n.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 { el, confirmDialog, fab, showDialog } from './components.js';
|
||||||
import { openLabDialog } from './dialogs.js';
|
import { openLabDialog } from './dialogs.js';
|
||||||
|
|
||||||
@ -70,8 +70,14 @@ export function renderLabs(container) {
|
|||||||
const main = el('div', { class: 'main' });
|
const main = el('div', { class: 'main' });
|
||||||
main.appendChild(el('div', { class: 'title' }, headline));
|
main.appendChild(el('div', { class: 'title' }, headline));
|
||||||
main.appendChild(el('div', { class: 'sub' }, formatDateTime(ts)));
|
main.appendChild(el('div', { class: 'sub' }, formatDateTime(ts)));
|
||||||
const firstNotes = group.map((g) => g.notes).find((n) => n && n.trim() !== '');
|
// v1.7.1 : TOUTES les notes distinctes du groupe (préfixées du marqueur
|
||||||
if (firstNotes) main.appendChild(el('div', { class: 'sub' }, firstNotes));
|
// 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);
|
item.appendChild(main);
|
||||||
|
|
||||||
// Tap → sélecteur E2/T si paire, édition directe sinon
|
// Tap → sélecteur E2/T si paire, édition directe sinon
|
||||||
|
|||||||
@ -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`).
|
* (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.
|
* 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. */
|
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
||||||
function parseNumOrNull(text) {
|
function parseNumOrNull(text) {
|
||||||
|
|||||||
@ -172,3 +172,44 @@ export function inputValuesToMs(dateISO, timeHM) {
|
|||||||
const ms = new Date(y, mo - 1, d, h, mi, 0, 0).getTime();
|
const ms = new Date(y, mo - 1, d, h, mi, 0, 0).getTime();
|
||||||
return Number.isFinite(ms) ? ms : null;
|
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}`);
|
||||||
|
}
|
||||||
|
|||||||
@ -131,7 +131,7 @@ async function main() {
|
|||||||
ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)');
|
ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)');
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
const dlgText = await dialog.textContent();
|
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');
|
const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong');
|
||||||
ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut');
|
ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut');
|
||||||
await page.click('.dialog .actions button');
|
await page.click('.dialog .actions button');
|
||||||
@ -271,6 +271,13 @@ async function main() {
|
|||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
text = await page.textContent('main.content');
|
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)');
|
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.goto(`${base}/#treatments`, { waitUntil: 'load' });
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
@ -282,7 +289,7 @@ async function main() {
|
|||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
text = await page.textContent('main.content');
|
text = await page.textContent('main.content');
|
||||||
ok(text.includes('Langue'), 'Paramètres : carte langue');
|
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');
|
ok(text.includes('100 % local'), 'Paramètres : note vie privée');
|
||||||
await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') });
|
await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') });
|
||||||
|
|
||||||
|
|||||||
45
tests/lab-notes.test.js
Normal file
45
tests/lab-notes.test.js
Normal 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', '')]), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user