process.env.CALORIE_PROVIDER = 'none'; import { computeBmi, computeBodyFat, computeBodyMetrics, getBmiCategory, normalizeGoals } from "../bodyMetrics"; import { getSettings, saveBasalCalories, saveGoals, saveProfile } from "../settings"; import { getWeights, saveWeight } from "../weights"; import { getDayOverview } from "../dayOverview"; import { addMeal } from "../meals"; import { resetMemoryStorage } from "../storage/memory"; import { BmiKategorie, MealType, Pohlavi } from "../../../types/gen/types.gen"; const LOGIN = 'tester'; const muz = { sex: Pohlavi.MUZ, birthYear: 1990, heightCm: 185 }; beforeEach(() => resetMemoryStorage()); describe('BMI a zdravá hmotnost', () => { it('spočítá BMI na desetiny', () => { // 90 / 1,85² = 26,296… expect(computeBmi(90_000, 185)).toBe(26.3); }); it('zařadí BMI do pásem WHO včetně hranic', () => { expect(getBmiCategory(18.4)).toBe(BmiKategorie.PODVAHA); expect(getBmiCategory(18.5)).toBe(BmiKategorie.NORMA); expect(getBmiCategory(24.9)).toBe(BmiKategorie.NORMA); expect(getBmiCategory(25)).toBe(BmiKategorie.NADVAHA); expect(getBmiCategory(30)).toBe(BmiKategorie.OBEZITA); }); it('rozmezí zdravé hmotnosti odpovídá BMI 18,5–25 pro výšku', () => { const metrics = computeBodyMetrics(muz); // 18,5 × 1,85² = 63,3 kg, 25 × 1,85² = 85,6 kg expect(metrics.healthyWeightMinGrams).toBe(63_300); expect(metrics.healthyWeightMaxGrams).toBe(85_600); expect(metrics.bmi).toBeUndefined(); }); }); describe('podíl tuku (US Navy)', () => { it('u muže počítá z pasu a krku', () => { expect(computeBodyFat({ ...muz, waistCm: 90, neckCm: 40 })).toBe(17.6); }); it('u ženy potřebuje i boky', () => { const zena = { sex: Pohlavi.ZENA, heightCm: 168, waistCm: 72, neckCm: 33 }; expect(computeBodyFat(zena)).toBeUndefined(); // 495 / (1,29579 − 0,35004 × log10(137) + 0,221 × log10(168)) − 450 expect(computeBodyFat({ ...zena, hipCm: 98 })).toBe(26.1); }); it('nesmyslné obvody nedají žádný odhad', () => { expect(computeBodyFat({ ...muz, waistCm: 40, neckCm: 45 })).toBeUndefined(); expect(computeBodyFat({ ...muz, waistCm: 90 })).toBeUndefined(); }); }); describe('cíle', () => { it('půl kila týdně dolů je denní bilance −550 kcal', () => { expect(computeBodyMetrics(muz, { weeklyChangeGrams: -500 }).targetDailyBalance).toBe(-550); }); it('bílkoviny počítá z hmotnosti a cílový termín jen ve směru tempa', () => { const metrics = computeBodyMetrics( muz, { weeklyChangeGrams: -500, targetWeightGrams: 85_000, proteinPerKg: 1.6 }, { date: '2026-10-01', weightGrams: 90_000 }); expect(metrics.proteinTargetGrams).toBe(144); expect(metrics.weeksToTarget).toBe(10); expect(computeBodyMetrics(muz, { weeklyChangeGrams: 250, targetWeightGrams: 85_000 }, { date: '2026-10-01', weightGrams: 90_000 }).weeksToTarget).toBeUndefined(); }); it('cíle se ukládají znormalizované a prázdné se odstraní', async () => { const settings = await saveGoals(LOGIN, { weeklyChangeGrams: -500, proteinPerKg: 1.64 }); expect(settings.goals).toEqual({ weeklyChangeGrams: -500, proteinPerKg: 1.6 }); expect((await saveGoals(LOGIN, {})).goals).toBeUndefined(); }); it('odmítne cíl mimo rozsah', () => { expect(() => normalizeGoals({ weeklyChangeGrams: -5000 })).toThrow(/mimo rozsah/); expect(() => normalizeGoals({ proteinPerKg: 10 })).toThrow(/mimo rozsah/); }); }); describe('vážení po dnech', () => { it('ukládá, přepisuje a maže vážení dne', async () => { await saveWeight(LOGIN, '2026-10-02', 89_500); await saveWeight(LOGIN, '2026-10-01', 90_000); await saveWeight(LOGIN, '2026-10-02', 89_400); expect(await getWeights(LOGIN)).toEqual([ { date: '2026-10-01', weightGrams: 90_000 }, { date: '2026-10-02', weightGrams: 89_400 }, ]); await saveWeight(LOGIN, '2026-10-02', null); expect(await getWeights(LOGIN)).toHaveLength(1); }); it('odmítne hmotnost mimo rozsah', async () => { await expect(saveWeight(LOGIN, '2026-10-01', 900)).rejects.toThrow(/mimo rozsah/); }); it('ukazatele v nastavení počítají s posledním vážením místo hmotnosti z profilu', async () => { await saveProfile(LOGIN, { ...muz, weightGrams: 95_000 }); await saveWeight(LOGIN, '2026-10-01', 90_000); const fromWeighing = await getSettings(LOGIN); await saveWeight(LOGIN, '2026-10-01', null); const fromProfile = await getSettings(LOGIN); expect(fromWeighing.bodyMetrics).toMatchObject({ weightGrams: 90_000, weightDate: '2026-10-01', bmi: 26.3 }); expect(fromProfile.bodyMetrics?.weightDate).toBeUndefined(); // 10 kcal na kilogram rozdílu expect(fromProfile.computedBasalCalories! - fromWeighing.computedBasalCalories!).toBe(50); }); it('bilance dne bere hmotnost platnou k tomu dni, ne poslední', async () => { await saveProfile(LOGIN, muz); await saveWeight(LOGIN, '2026-09-01', 95_000); await saveWeight(LOGIN, '2026-10-01', 90_000); const september = await getDayOverview(LOGIN, '2026-09-15'); const october = await getDayOverview(LOGIN, '2026-10-05'); expect(september.energy.basal - october.energy.basal).toBe(50); expect(september.weightGrams).toBeUndefined(); expect((await getDayOverview(LOGIN, '2026-10-01')).weightGrams).toBe(90_000); }); it('ruční klidový výdej má přednost i před vážením', async () => { await saveProfile(LOGIN, muz); await saveWeight(LOGIN, '2026-10-01', 90_000); await saveBasalCalories(LOGIN, 1500); expect((await getDayOverview(LOGIN, '2026-10-01')).energy.basal).toBe(1500); }); it('přehled dne nese cílovou bilanci a cíl bílkovin', async () => { await saveWeight(LOGIN, '2026-10-01', 90_000); await saveGoals(LOGIN, { weeklyChangeGrams: -500, proteinPerKg: 2 }); await addMeal(LOGIN, { date: '2026-10-01', mealType: MealType.OBED, name: 'Guláš', calories: 700 }); const overview = await getDayOverview(LOGIN, '2026-10-01'); expect(overview.energy.targetBalance).toBe(-550); expect(overview.proteinTargetGrams).toBe(180); }); });