Files
food-tracer/server/src/tests/bodyMetrics.test.ts
T

158 lines
6.4 KiB
TypeScript
Raw 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.
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);
});
});