HormoneTrack-web/tests/pk-profiles.test.js
Siphonight 491efef8eb v1.4.10 — portage initial de l'app Android v1.4.10
Portage navigateur complet de l'app Android HormoneTrack v1.4.10 :
- moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C,
  WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android
  préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés
- calibration par période d'ester et par modèle affiché (#60/#61)
- UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire,
  prévision, pics/creux, fuseau configurable), rappels web
  (Notification API), seuils d'alerte, i18n FR/EN
- sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1)
- 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie
- processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur,
  smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG)
- dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions
  alignées sur l'Android porté, changelogs indépendants

Non porté (impossible dans un navigateur, documenté §12) : agenda
récurrent, notifications onglet fermé, montre.
2026-09-08 14:34:58 +02:00

101 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Tests de fidélité de l'asset pk_profiles.json + échantillonnage
* (miroir de PKProfileStoreTest.kt côté Android).
*
* ⚠️ L'asset web est une COPIE de l'asset Android : ces tests garantissent
* que les deux apps lisent EXACTEMENT les mêmes tables (toute divergence =
* courbes différentes entre Android et web).
*/
import { test, describe, before } from 'node:test';
import assert from 'node:assert/strict';
import { initProfiles, rawProfiles } from './helpers.js';
import * as PKProfileStore from '../js/pk/pk-profile-store.js';
describe('pk_profiles.json (asset partagé avec Android)', () => {
before(() => initProfiles());
test('6 profils présents × 8001 points', () => {
const raw = rawProfiles();
const expected = ['EV_ese', 'EU_ese', 'EEn_ese', 'EV_tfs', 'EU_tfs', 'EEn_tfs'];
for (const key of expected) {
assert.ok(raw.profiles[key], `profil manquant : ${key}`);
assert.equal(raw.profiles[key].length, 8001, `${key} doit avoir 8001 points`);
}
});
test('pics == valeurs ODS de référence (pg/mL par mg)', () => {
// Mêmes valeurs épinglées côté Android (doc §7.1 / PKProfileStoreTest)
const refs = [
['EV', 'ESE', 61.12],
['EU', 'ESE', 3.44],
['EEn', 'ESE', 31.35],
['EV', 'TFS', 58.96],
['EU', 'TFS', 10.11],
['EEn', 'TFS', 31.97],
];
for (const [ester, mod, peak] of refs) {
let max = 0;
for (let h = 0; h <= 8000; h++) {
const v = PKProfileStore.sample(ester, mod, h);
if (v > max) max = v;
}
assert.ok(Math.abs(max - peak) < 0.02, `pic ${ester}_${mod} = ${max}, attendu ${peak}`);
}
});
test('EV_ese : pic à ~45 h', () => {
let peakH = 0;
let peakV = 0;
for (let h = 0; h <= 500; h++) {
const v = PKProfileStore.sample('EV', 'ESE', h);
if (v > peakV) {
peakV = v;
peakH = h;
}
}
assert.ok(peakH >= 40 && peakH <= 50, `Tmax EV_ese = ${peakH} h, attendu ~45 h`);
});
test('zéro avant injection / à dt=0', () => {
assert.equal(PKProfileStore.sample('EV', 'ESE', 0), 0);
assert.equal(PKProfileStore.sample('EV', 'ESE', -5), 0);
});
test('modèle STRICT : modèle inconnu → 0 (jamais de fallback silencieux, bug §14 #21)', () => {
assert.equal(PKProfileStore.sample('EV', 'WHS', 100), 0); // WHS n'a pas de tables ODS
assert.equal(PKProfileStore.sample('EV', 'XXX', 100), 0);
});
test('lookup INSENSIBLE À LA CASSE (bug #22 — courbes EEn vides)', () => {
// Esters.EEN = "EEN" (constante app) vs "EEn" (casse biologique de l'asset)
assert.equal(PKProfileStore.sample('EEN', 'ESE', 152), PKProfileStore.sample('EEn', 'ESE', 152));
assert.ok(PKProfileStore.sample('EEN', 'ESE', 152) > 1);
});
test('extrapolation terminale : au-delà de la table, décroissante (EV) et jamais croissante (EU)', () => {
// La table fait 8001 h : l'extrapolation ne s'applique qu'AU-DELÀ.
// EV (t½ ~4 j ODS) : décroissance exponentielle mesurable après 8000 h.
const ev8500 = PKProfileStore.sample('EV', 'ESE', 8500);
const ev9000 = PKProfileStore.sample('EV', 'ESE', 9000);
assert.ok(ev8500 > 0, 'queue extrapolée mesurable');
assert.ok(ev9000 < ev8500, `décroissance attendue (${ev8500} → ${ev9000})`);
// EU (ester ultra-lent, tail plate au plancher 0,04) : taux log ≈ 0 →
// plateau constant — en tout cas JAMAIS croissante (bug #20 : extrapoler
// depuis le plancher 0,01/0,00 donnait 0 à vie ou une constante plate).
const eu8000 = PKProfileStore.sample('EU', 'ESE', 8000);
const eu8500 = PKProfileStore.sample('EU', 'ESE', 8500);
assert.ok(eu8000 > 0, 'EU mesurable au-delà de la table');
assert.ok(eu8500 <= eu8000, 'jamais croissante');
});
test('interpolation linéaire entre points entiers', () => {
// Le plateau 45/46 h d'EV_ese est plat (piège du test Android) — prendre
// une heure en montée : la valeur à 100.5 h = moyenne de 100 et 101
const raw = rawProfiles();
const arr = raw.profiles.EV_ese;
const expected = (arr[100] + arr[101]) / 2;
assert.ok(Math.abs(PKProfileStore.sample('EV', 'ESE', 100.5) - expected) < 1e-9);
});
});