feat: předělání historie a profilu

This commit is contained in:
Ondřej Anděl committed 2026-10-07 14:48:21 +02:00
1 parent 9e72f431a1
commit 8fa7f601f2
97 files changed
+7502 -1034

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
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);
});
});