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

135 lines
4.1 KiB
JavaScript

/**
* Helpers de test partagés (chargement de l'asset PK, fabriques de fixtures).
*
* Les tests tournent en Node pur (`node --test tests/`) — aucun DOM ni
* localStorage : la couche pure (pk/, backup) est testée directement, le
* store est testé avec un backend Map injectable.
*/
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { initWithJson } from '../js/pk/pk-profile-store.js';
/** Racine du dépôt web. */
export const WEB_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
/**
* Dossier des données de test RÉELLES (exports Android de l'utilisatrice) —
* HORS dépôt (données de santé, cf docs/DEVELOPPEMENT.md §8).
*
* Depuis la séparation des dépôts (v1.4.10), ce dossier vit dans le dépôt
* ANDROID voisin (`../HormoneTrack/local-test-data/`) : le web ne duplique
* rien, il ne fait que le CONSOMMER en lecture (skip des tests s'il est
* absent — clone neuf, CI, autre machine). Candidats par ordre de priorité :
* 1. le dépôt Android voisin (layout standard ~/projects/HormoneTrack*)
* 2. un sibling direct `../local-test-data` (ancien layout repo unique)
* 3. un dossier local-test-data/ dans le dépôt web lui-même (si l'utilisatrice
* préfère y déposer ses exports — gitigné)
*/
export function localTestDataDir() {
const candidates = [
join(WEB_ROOT, '..', 'HormoneTrack', 'local-test-data'),
join(WEB_ROOT, '..', 'local-test-data'),
join(WEB_ROOT, 'local-test-data'),
];
for (const dir of candidates) {
if (existsSync(dir)) return dir;
}
return null;
}
/** Candidats de fichiers de backup réels pour les régressions data-driven. */
export function localBackupPath(name) {
const dir = localTestDataDir();
return dir ? join(dir, name) : null;
}
/**
* Charge l'asset PK réel (le MÊME fichier que l'app Android embarque) et
* initialise PKProfileStore. À appeler dans les suites qui touchent aux
* profils ODS / au dispatch moteur.
*/
export function initProfiles() {
const json = readFileSync(join(WEB_ROOT, 'assets/pk_profiles.json'), 'utf8');
initWithJson(json);
return JSON.parse(json);
}
/** Contenu brut parsé de l'asset (sans initialiser le store). */
export function rawProfiles() {
return JSON.parse(readFileSync(join(WEB_ROOT, 'assets/pk_profiles.json'), 'utf8'));
}
/** Heure en ms. */
export const HOUR_MS = 3600000;
/** Jour en ms. */
export const DAY_MS = 86400000;
/**
* Traitement injectable E2 par défaut (EEn TFS 5 mg) — valeurs des presets
* `preset_een_tfs`. id=0 = « pas encore en base » (insertion côté store).
*/
export function makeTreatment(overrides = {}) {
return {
id: 0,
name: 'EEn test',
type: 'ESTRADIOL',
route: 'INJECTION_SUBCUT',
doseAmount: 5,
doseUnit: 'mg',
isActive: true,
notes: null,
esterType: 'EEN',
pkModel: 'TFS',
absorptionHours: 156,
eliminationHalfLifeHours: 110,
bioavailabilityFraction: 1,
scaleFactor: 1.0,
forecastIntervalDays: null,
reminderHour: null,
reminderMinute: null,
reminderEnabled: false,
calendarEventId: null,
createdAt: 0,
...overrides,
};
}
/** Dose simple rattachée au traitement par défaut (id 0 — celui de makeTreatment). */
export function makeDose(timestamp, doseAmount = 5, overrides = {}) {
return {
id: 0,
treatmentId: 0,
timestamp,
doseAmount,
notes: null,
esterType: null,
...overrides,
};
}
/** Lab simple. */
export function makeLab(marker, value, unit, timestamp, overrides = {}) {
return {
id: 0,
marker,
value,
unit,
timestamp,
notes: null,
...overrides,
};
}
/** assert proche (tolérance absolue ou relative) — les tests PK pin des valeurs d'article. */
export function assertClose(actual, expected, tolerance, message = '') {
const diff = Math.abs(actual - expected);
const tol = typeof tolerance === 'number' && tolerance <= 1 && tolerance > 0 && expected !== 0
? Math.abs(expected) * tolerance
: tolerance;
if (diff > tol) {
throw new Error(`${message}\n attendu ≈ ${expected} (±${tol}) — obtenu ${actual}`);
}
}