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.
101 lines
4.1 KiB
JavaScript
101 lines
4.1 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|
||
});
|