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