feat: předělání historie a profilu
This commit is contained in:
1 parent
9e72f431a1
commit
8fa7f601f2
97 files changed
+7502
-1034
No files matched your search
@@ -190,10 +190,14 @@ describe('denní detail výdeje', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('vazba jídla na šablonu', () => {
|
||||
describe('vazba jídla na recept', () => {
|
||||
|
||||
it('jídlo ze šablony si pamatuje, ze které vzniklo', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, { name: 'Štrůdl', mealType: MealType.PULNOCNI_SVACINA });
|
||||
it('jídlo z receptu si pamatuje, ze kterého vzniklo', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, {
|
||||
name: 'Štrůdl',
|
||||
mealType: MealType.PULNOCNI_SVACINA,
|
||||
ingredients: [{ name: 'Těsto', weight: 300, calories: 900 }],
|
||||
});
|
||||
await applyMealTemplate(LOGIN, template.id, DATE);
|
||||
|
||||
const [meal] = (await getDay(LOGIN, DATE)).entries;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import ExcelJS from 'exceljs';
|
||||
import { importLuncherExport } from "../luncherImport";
|
||||
import { getDay } from "../meals";
|
||||
import { addMeal, getDay } from "../meals";
|
||||
import { resetMemoryStorage } from "../storage/memory";
|
||||
import { MealType } from "../../../types/gen/types.gen";
|
||||
|
||||
@@ -278,3 +278,82 @@ describe('import přehledu z Luncheru', () => {
|
||||
.rejects.toThrow(/nepodařilo najít očekávané sloupce/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('doplnění energie a živin z historie při importu', () => {
|
||||
|
||||
const svickova = {
|
||||
date: '2025-02-10', mealType: MealType.OBED, name: 'Svíčková na smetaně', source: 'Sladovnická',
|
||||
weight: 400, caloriesPer100g: 150, nutrientsPer100g: { protein: 8, fat: 9.5 },
|
||||
};
|
||||
|
||||
/** Naimportovaný záznam daného dne. */
|
||||
const importedOn = async (date: string) => {
|
||||
await importLuncherExport(LOGIN, 'export.json', buildLuncherJson());
|
||||
return (await getDay(LOGIN, date)).entries.find(entry => entry.importSource === 'luncher')!;
|
||||
};
|
||||
|
||||
it('převezme energii, živiny i gramáž jídla stejného názvu', async () => {
|
||||
await addMeal(LOGIN, svickova);
|
||||
|
||||
const entry = await importedOn('2025-03-03');
|
||||
|
||||
expect(entry).toMatchObject({
|
||||
weight: 400, caloriesPer100g: 150, calories: 600, nutrientsPer100g: { protein: 8, fat: 9.5 },
|
||||
});
|
||||
// Cena zůstane z importu
|
||||
expect(entry.price).toBe(15900);
|
||||
});
|
||||
|
||||
it('u stejného zdroje převezme sazbu za 100 g a gramáž dopočte z nové ceny', async () => {
|
||||
await addMeal(LOGIN, {
|
||||
date: '2025-02-10', mealType: MealType.OBED, name: 'Kuřecí burger', source: 'Bolt Food',
|
||||
price: 20000, pricePer100g: 5000, caloriesPer100g: 250,
|
||||
});
|
||||
|
||||
const entry = await importedOn('2025-03-05');
|
||||
|
||||
// 249 Kč za 50 Kč/100 g = 498 g, × 250 kcal/100 g
|
||||
expect(entry).toMatchObject({ pricePer100g: 5000, weight: 498, calories: 1245 });
|
||||
});
|
||||
|
||||
it('z jiného zdroje sazbu nepřebírá a použije gramáž porce', async () => {
|
||||
await addMeal(LOGIN, {
|
||||
date: '2025-02-10', mealType: MealType.OBED, name: 'Kuřecí burger', source: 'Jinde',
|
||||
price: 20000, pricePer100g: 5000, caloriesPer100g: 250,
|
||||
});
|
||||
|
||||
const entry = await importedOn('2025-03-05');
|
||||
|
||||
expect(entry.pricePer100g).toBeUndefined();
|
||||
expect(entry).toMatchObject({ weight: 400, calories: 1000 });
|
||||
});
|
||||
|
||||
it('má přednost jídlo ze stejného zdroje, i když je starší', async () => {
|
||||
await addMeal(LOGIN, svickova);
|
||||
await addMeal(LOGIN, { ...svickova, date: '2025-02-20', source: 'Jinde', caloriesPer100g: 200 });
|
||||
|
||||
expect((await importedOn('2025-03-03')).caloriesPer100g).toBe(150);
|
||||
});
|
||||
|
||||
it('jídlo stejného názvu bez kalorií vzorem není, vezme se starší s kaloriemi', async () => {
|
||||
await addMeal(LOGIN, svickova);
|
||||
await addMeal(LOGIN, { date: '2025-02-20', mealType: MealType.OBED, name: 'Svíčková na smetaně', price: 15900 });
|
||||
|
||||
expect((await importedOn('2025-03-03')).calories).toBe(600);
|
||||
});
|
||||
|
||||
it('bez historie zůstane jídlo bez kalorií', async () => {
|
||||
const entry = await importedOn('2025-03-03');
|
||||
|
||||
expect(entry.calories).toBeUndefined();
|
||||
expect(entry).not.toHaveProperty('weight');
|
||||
});
|
||||
|
||||
it('doplněné hodnoty ukáže už náhled', async () => {
|
||||
await addMeal(LOGIN, svickova);
|
||||
|
||||
const result = await importLuncherExport(LOGIN, 'export.json', buildLuncherJson(), undefined, true);
|
||||
|
||||
expect(result.entries.find(entry => entry.date === '2025-03-03')?.calories).toBe(600);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
process.env.CALORIE_PROVIDER = 'none';
|
||||
|
||||
import { addMeal, fillSameNameMeals, getDay, getMealHistory, getMealSources, updateMeal } from "../meals";
|
||||
import { getPortionNutrients, sumNutrients } from "../nutrients";
|
||||
import { resetMemoryStorage } from "../storage/memory";
|
||||
import { MealType } from "../../../types/gen/types.gen";
|
||||
|
||||
const LOGIN = 'tester';
|
||||
const DATE = '2025-03-03';
|
||||
|
||||
beforeEach(() => resetMemoryStorage());
|
||||
|
||||
describe('návrhy jídel z historie', () => {
|
||||
|
||||
const gyros = {
|
||||
date: DATE, mealType: MealType.OBED, name: 'Kuřecí gyros',
|
||||
source: 'TechTower', price: 17600, pricePer100g: 4400, caloriesPer100g: 172,
|
||||
};
|
||||
|
||||
it('každý název nabídne jednou, i když se jedl vícekrát', async () => {
|
||||
await addMeal(LOGIN, gyros);
|
||||
await addMeal(LOGIN, { ...gyros, date: '2025-03-04' });
|
||||
await addMeal(LOGIN, { ...gyros, date: '2025-03-05', name: 'kureci GYROS' });
|
||||
await addMeal(LOGIN, { ...gyros, name: 'Polévka' });
|
||||
|
||||
const history = await getMealHistory(LOGIN);
|
||||
|
||||
expect(history.map(item => item.name)).toEqual(['kureci GYROS', 'Polévka']);
|
||||
expect(history[0].count).toBe(3);
|
||||
});
|
||||
|
||||
it('hodnoty bere z posledního záznamu', async () => {
|
||||
await addMeal(LOGIN, { ...gyros, date: '2025-03-10', price: 22000 });
|
||||
await addMeal(LOGIN, gyros);
|
||||
|
||||
const [item] = await getMealHistory(LOGIN);
|
||||
|
||||
expect(item.price).toBe(22000);
|
||||
expect(item.lastDate).toBe('2025-03-10');
|
||||
});
|
||||
|
||||
it('nepřenáší chod, násobek ani poznámku', async () => {
|
||||
await addMeal(LOGIN, { ...gyros, mealType: MealType.VECERE, multiplier: 2, note: 's kolegy' });
|
||||
|
||||
const [item] = await getMealHistory(LOGIN);
|
||||
|
||||
expect(item).not.toHaveProperty('mealType');
|
||||
expect(item).not.toHaveProperty('multiplier');
|
||||
expect(item).not.toHaveProperty('note');
|
||||
});
|
||||
|
||||
it('gramáž dopočtenou z ceny vynechá, ručně zadanou ponechá', async () => {
|
||||
await addMeal(LOGIN, gyros);
|
||||
await addMeal(LOGIN, { ...gyros, name: 'Salát', weight: 250 });
|
||||
|
||||
const history = await getMealHistory(LOGIN);
|
||||
|
||||
expect(history.find(item => item.name === 'Kuřecí gyros')?.weight).toBeUndefined();
|
||||
expect(history.find(item => item.name === 'Salát')?.weight).toBe(250);
|
||||
});
|
||||
|
||||
it('kalorie dopočtené z gramáže vynechá, ručně zadané ponechá', async () => {
|
||||
await addMeal(LOGIN, gyros);
|
||||
await addMeal(LOGIN, { date: DATE, mealType: MealType.SNIDANE, name: 'Jablko', calories: 80 });
|
||||
|
||||
const history = await getMealHistory(LOGIN);
|
||||
|
||||
expect(history.find(item => item.name === 'Kuřecí gyros')).toMatchObject({ caloriesPer100g: 172 });
|
||||
expect(history.find(item => item.name === 'Kuřecí gyros')?.calories).toBeUndefined();
|
||||
expect(history.find(item => item.name === 'Jablko')?.calories).toBe(80);
|
||||
});
|
||||
|
||||
it('řadí od nejčastějších, při shodě od naposledy jedených', async () => {
|
||||
await addMeal(LOGIN, { ...gyros, name: 'Starší' });
|
||||
await addMeal(LOGIN, { ...gyros, name: 'Novější', date: '2025-03-04' });
|
||||
await addMeal(LOGIN, { ...gyros, name: 'Častější' });
|
||||
await addMeal(LOGIN, { ...gyros, name: 'Častější', date: '2025-03-02' });
|
||||
|
||||
const history = await getMealHistory(LOGIN);
|
||||
|
||||
expect(history.map(item => item.name)).toEqual(['Častější', 'Novější', 'Starší']);
|
||||
});
|
||||
|
||||
it('historie je oddělená mezi uživateli', async () => {
|
||||
await addMeal(LOGIN, gyros);
|
||||
|
||||
expect(await getMealHistory('nekdo-jiny')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nutriční hodnoty', () => {
|
||||
|
||||
const tycinka = {
|
||||
date: DATE, mealType: MealType.DOPOLEDNI_SVACINA, name: 'Proteinová tyčinka', weight: 50,
|
||||
nutrientsPer100g: { protein: 30, carbohydrates: 40.04, sugars: 5, fat: 12.5, salt: 0.44 },
|
||||
};
|
||||
|
||||
it('uloží hodnoty na 100 g zaokrouhlené na desetiny', async () => {
|
||||
const day = await addMeal(LOGIN, tycinka);
|
||||
|
||||
expect(day.entries[0].nutrientsPer100g).toEqual({
|
||||
protein: 30, carbohydrates: 40, sugars: 5, fat: 12.5, salt: 0.4,
|
||||
});
|
||||
});
|
||||
|
||||
it('den sečte živiny porcí včetně násobku', async () => {
|
||||
await addMeal(LOGIN, tycinka);
|
||||
await addMeal(LOGIN, { ...tycinka, multiplier: 2 });
|
||||
|
||||
const day = await getDay(LOGIN, DATE);
|
||||
|
||||
// 50 g = polovina hodnot na 100 g; jednou ×1 a jednou ×2
|
||||
expect(day.totalNutrients).toEqual({
|
||||
protein: 45, carbohydrates: 60, sugars: 7.5, fat: 18.8, salt: 0.6,
|
||||
});
|
||||
});
|
||||
|
||||
it('jídlo bez gramáže nebo bez živin do součtu nevstupuje', async () => {
|
||||
await addMeal(LOGIN, { ...tycinka, weight: undefined });
|
||||
await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'Guláš', weight: 400, calories: 600 });
|
||||
|
||||
expect((await getDay(LOGIN, DATE)).totalNutrients).toBeUndefined();
|
||||
});
|
||||
|
||||
it('úprava umí živiny smazat', async () => {
|
||||
const day = await addMeal(LOGIN, tycinka);
|
||||
const { nutrientsPer100g, ...bezZivin } = tycinka;
|
||||
|
||||
const updated = await updateMeal(LOGIN, day.entries[0].id, bezZivin);
|
||||
|
||||
expect(nutrientsPer100g).toBeDefined();
|
||||
expect(updated.entries[0].nutrientsPer100g).toBeUndefined();
|
||||
expect(updated.totalNutrients).toBeUndefined();
|
||||
});
|
||||
|
||||
it('odmítne zápornou nebo nečíselnou hodnotu', async () => {
|
||||
await expect(addMeal(LOGIN, { ...tycinka, nutrientsPer100g: { fat: -1 } })).rejects.toThrow(/záporné/);
|
||||
await expect(addMeal(LOGIN, { ...tycinka, nutrientsPer100g: { fat: 'hodně' } })).rejects.toThrow(/číslo/);
|
||||
});
|
||||
|
||||
it('prázdné hodnoty se neukládají', async () => {
|
||||
const day = await addMeal(LOGIN, { ...tycinka, nutrientsPer100g: {} });
|
||||
|
||||
expect(day.entries[0].nutrientsPer100g).toBeUndefined();
|
||||
});
|
||||
|
||||
it('přenesou se do návrhu z historie', async () => {
|
||||
await addMeal(LOGIN, tycinka);
|
||||
|
||||
const [item] = await getMealHistory(LOGIN);
|
||||
|
||||
expect(item.nutrientsPer100g?.protein).toBe(30);
|
||||
});
|
||||
|
||||
it('porce a součet počítají jen vyplněné živiny', () => {
|
||||
expect(getPortionNutrients({ protein: 10 }, 250, 0.5)).toEqual({ protein: 12.5 });
|
||||
expect(getPortionNutrients({ protein: 10 }, undefined)).toBeUndefined();
|
||||
expect(sumNutrients([{ protein: 1.1 }, undefined, { protein: 2.2, fat: 1 }])).toEqual({ protein: 3.3, fat: 1 });
|
||||
expect(sumNutrients([undefined])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('zdroje jídel', () => {
|
||||
|
||||
it('vrátí každý zdroj jednou, od nejčastějšího, v posledním zápisu', async () => {
|
||||
await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'A', source: 'techtower' });
|
||||
await addMeal(LOGIN, { date: '2025-03-04', mealType: MealType.OBED, name: 'B', source: 'TechTower' });
|
||||
await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'C', source: 'Chefie' });
|
||||
await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'D' });
|
||||
|
||||
expect(await getMealSources(LOGIN)).toEqual(['TechTower', 'Chefie']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('doplnění do jídel stejného názvu', () => {
|
||||
|
||||
const salat = { mealType: MealType.OBED, name: 'Těstovinový salát', source: 'Chefie', price: 12900 };
|
||||
|
||||
/** Založí jídla bez kalorií a jedno s nimi; vrátí to s kaloriemi. */
|
||||
const seed = async () => {
|
||||
await addMeal(LOGIN, { ...salat, date: '2025-03-01' });
|
||||
await addMeal(LOGIN, { ...salat, date: '2025-03-02', name: 'těstovinový SALÁT' });
|
||||
await addMeal(LOGIN, { ...salat, date: '2025-03-02', name: 'Jiné jídlo' });
|
||||
await addMeal(LOGIN, { ...salat, date: '2025-03-04', calories: 300, caloriesPer100g: 120 });
|
||||
const day = await addMeal(LOGIN, {
|
||||
...salat, date: DATE, weight: 350, caloriesPer100g: 160, nutrientsPer100g: { protein: 6 },
|
||||
});
|
||||
return day.entries[0];
|
||||
};
|
||||
|
||||
it('náhled spočítá jídla bez kalorií a nic nezmění', async () => {
|
||||
const source = await seed();
|
||||
|
||||
const result = await fillSameNameMeals(LOGIN, source.id, DATE, true);
|
||||
|
||||
expect(result).toEqual({ count: 2, dates: ['2025-03-01', '2025-03-02'] });
|
||||
expect((await getDay(LOGIN, '2025-03-01')).totalCalories).toBe(0);
|
||||
});
|
||||
|
||||
it('doplní energii, živiny a gramáž, jídla s kaloriemi nechá', async () => {
|
||||
const source = await seed();
|
||||
|
||||
await fillSameNameMeals(LOGIN, source.id, DATE);
|
||||
|
||||
const [first] = (await getDay(LOGIN, '2025-03-01')).entries;
|
||||
expect(first).toMatchObject({ weight: 350, calories: 560, nutrientsPer100g: { protein: 6 }, price: 12900 });
|
||||
const second = (await getDay(LOGIN, '2025-03-02')).entries;
|
||||
expect(second.find(entry => entry.name === 'těstovinový SALÁT')?.calories).toBe(560);
|
||||
expect(second.find(entry => entry.name === 'Jiné jídlo')?.calories).toBeUndefined();
|
||||
expect((await getDay(LOGIN, '2025-03-04')).entries[0].calories).toBe(300);
|
||||
});
|
||||
|
||||
it('odmítne jídlo bez kalorií i živin', async () => {
|
||||
const day = await addMeal(LOGIN, { ...salat, date: DATE });
|
||||
|
||||
await expect(fillSameNameMeals(LOGIN, day.entries[0].id, DATE)).rejects.toThrow(/není co doplnit/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('návrh z historie s posledním záznamem bez kalorií', () => {
|
||||
|
||||
it('převezme kalorie z dřívějšího záznamu, cenu z posledního', async () => {
|
||||
await addMeal(LOGIN, {
|
||||
date: '2025-03-01', mealType: MealType.OBED, name: 'Salát', source: 'Chefie',
|
||||
price: 12900, weight: 350, caloriesPer100g: 160,
|
||||
});
|
||||
await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'Salát', source: 'Chefie', price: 13900 });
|
||||
|
||||
const [item] = await getMealHistory(LOGIN);
|
||||
|
||||
expect(item).toMatchObject({ price: 13900, weight: 350, caloriesPer100g: 160, lastDate: DATE });
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { applyWorkoutTemplate, getWorkoutTemplates, saveWorkoutTemplate } from "
|
||||
import { applyMealTemplate, deleteMealTemplate, getMealTemplates, saveMealTemplate } from "../mealTemplates";
|
||||
import { getDay } from "../meals";
|
||||
import { resetMemoryStorage } from "../storage/memory";
|
||||
import getStorage from "../storage";
|
||||
import { ActivityUnit, MealType, WorkoutItemKind } from "../../../types/gen/types.gen";
|
||||
|
||||
const LOGIN = 'tester';
|
||||
@@ -115,74 +116,67 @@ describe('série u cviků', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('šablony jídel', () => {
|
||||
|
||||
const salat = {
|
||||
name: 'Těstovinový salát',
|
||||
source: 'Chefie',
|
||||
mealType: MealType.OBED,
|
||||
weight: 350,
|
||||
caloriesPer100g: 160,
|
||||
price: 12900,
|
||||
describe('recepty', () => {
|
||||
|
||||
const rizoto = {
|
||||
name: 'Kuřecí rizoto',
|
||||
source: 'doma',
|
||||
mealType: MealType.VECERE,
|
||||
servings: 2,
|
||||
cookedWeight: 1000,
|
||||
ingredients: [
|
||||
{ name: 'Rýže', weight: 200, caloriesPer100g: 350, price: 3000 },
|
||||
{ name: 'Kuřecí prsa', weight: 300, caloriesPer100g: 110, price: 9000 },
|
||||
],
|
||||
note: 'Rýži orestovat, podlít vývarem.',
|
||||
};
|
||||
|
||||
it('uloží šablonu a vrátí ji v seznamu', async () => {
|
||||
const templates = await saveMealTemplate(LOGIN, salat);
|
||||
it('uloží recept a dopočte hodnoty jedné porce', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, rizoto);
|
||||
|
||||
// 700 + 330 kcal na 1000 g hotového jídla, rozděleno na dvě porce
|
||||
expect(template).toMatchObject({
|
||||
name: 'Kuřecí rizoto',
|
||||
weight: 500,
|
||||
calories: 515,
|
||||
caloriesPer100g: 103,
|
||||
price: 6000,
|
||||
});
|
||||
});
|
||||
|
||||
it('úprava podle id recept přepíše, nezaloží druhý', async () => {
|
||||
const [created] = await saveMealTemplate(LOGIN, rizoto);
|
||||
|
||||
const templates = await saveMealTemplate(LOGIN, { ...rizoto, id: created.id, name: 'Rizoto velké' });
|
||||
|
||||
expect(templates).toHaveLength(1);
|
||||
expect(templates[0].name).toBe('Těstovinový salát');
|
||||
expect(templates[0].source).toBe('Chefie');
|
||||
expect(templates[0].name).toBe('Rizoto velké');
|
||||
});
|
||||
|
||||
it('úprava podle id šablonu přepíše, nezaloží druhou', async () => {
|
||||
const [created] = await saveMealTemplate(LOGIN, salat);
|
||||
|
||||
const templates = await saveMealTemplate(LOGIN, { ...salat, id: created.id, name: 'Salát velký' });
|
||||
|
||||
expect(templates).toHaveLength(1);
|
||||
expect(templates[0].name).toBe('Salát velký');
|
||||
});
|
||||
|
||||
it('použití založí jídlo do dne a dopočte kalorie', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, salat);
|
||||
it('použití založí porci do dne, bez postupu', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, rizoto);
|
||||
|
||||
await applyMealTemplate(LOGIN, template.id, DATE);
|
||||
|
||||
const day = await getDay(LOGIN, DATE);
|
||||
expect(day.entries).toHaveLength(1);
|
||||
expect(day.entries[0].name).toBe('Těstovinový salát');
|
||||
expect(day.entries[0].source).toBe('Chefie');
|
||||
// 350 g × 160 kcal/100 g
|
||||
expect(day.entries[0].calories).toBe(560);
|
||||
expect(day.totalPrice).toBe(12900);
|
||||
const [entry] = (await getDay(LOGIN, DATE)).entries;
|
||||
expect(entry).toMatchObject({ name: 'Kuřecí rizoto', mealType: MealType.VECERE, weight: 500, calories: 515 });
|
||||
expect(entry.note).toBeUndefined();
|
||||
});
|
||||
|
||||
it('zvolený chod přebije ten ze šablony', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, salat);
|
||||
it('zvolený chod přebije ten z receptu a bez obou se založí oběd', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, rizoto);
|
||||
const [bezChodu] = await saveMealTemplate(LOGIN, { ...rizoto, name: 'Bez chodu', mealType: undefined });
|
||||
|
||||
await applyMealTemplate(LOGIN, template.id, DATE, MealType.VECERE);
|
||||
await applyMealTemplate(LOGIN, template.id, DATE, MealType.SNIDANE);
|
||||
await applyMealTemplate(LOGIN, bezChodu.id, '2025-03-04');
|
||||
|
||||
expect((await getDay(LOGIN, DATE)).entries[0].mealType).toBe(MealType.VECERE);
|
||||
expect((await getDay(LOGIN, DATE)).entries[0].mealType).toBe(MealType.SNIDANE);
|
||||
expect((await getDay(LOGIN, '2025-03-04')).entries[0].mealType).toBe(MealType.OBED);
|
||||
});
|
||||
|
||||
it('bez chodu ve šabloně i v požadavku se založí jako oběd', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, { name: 'Jablko', calories: 80 });
|
||||
|
||||
await applyMealTemplate(LOGIN, template.id, DATE);
|
||||
|
||||
expect((await getDay(LOGIN, DATE)).entries[0].mealType).toBe(MealType.OBED);
|
||||
});
|
||||
|
||||
it('šablona jen s absolutními kaloriemi funguje taky', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, { name: 'Jablko', calories: 80 });
|
||||
|
||||
await applyMealTemplate(LOGIN, template.id, DATE);
|
||||
|
||||
expect((await getDay(LOGIN, DATE)).totalCalories).toBe(80);
|
||||
});
|
||||
|
||||
it('založené jídlo je samostatné — smazání šablony ho nesmaže', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, salat);
|
||||
it('založené jídlo je samostatné — smazání receptu ho nesmaže', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, rizoto);
|
||||
await applyMealTemplate(LOGIN, template.id, DATE);
|
||||
|
||||
await deleteMealTemplate(LOGIN, template.id);
|
||||
@@ -191,25 +185,66 @@ describe('šablony jídel', () => {
|
||||
expect((await getDay(LOGIN, DATE)).entries).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('unese poznámku s postupem přípravy', async () => {
|
||||
const recept = 'Těstoviny uvařit al dente.\nZeleninu nakrájet.\nSmíchat s dresinkem.';
|
||||
const [template] = await saveMealTemplate(LOGIN, { name: 'Domácí salát', note: recept });
|
||||
|
||||
expect(template.note).toBe(recept);
|
||||
|
||||
await applyMealTemplate(LOGIN, template.id, DATE);
|
||||
expect((await getDay(LOGIN, DATE)).entries[0].note).toBe(recept);
|
||||
it('odmítne recept bez názvu, bez surovin a neexistující id', async () => {
|
||||
await expect(saveMealTemplate(LOGIN, { ...rizoto, name: ' ' })).rejects.toThrow(/název receptu/);
|
||||
await expect(saveMealTemplate(LOGIN, { ...rizoto, ingredients: [] })).rejects.toThrow(/surovinu/);
|
||||
await expect(applyMealTemplate(LOGIN, 'neexistuje', DATE)).rejects.toThrow(/nebyl nalezen/);
|
||||
await expect(deleteMealTemplate(LOGIN, 'neexistuje')).rejects.toThrow(/nebyl nalezen/);
|
||||
});
|
||||
|
||||
it('odmítne šablonu bez názvu a neexistující id', async () => {
|
||||
await expect(saveMealTemplate(LOGIN, { name: ' ' })).rejects.toThrow(/název šablony/);
|
||||
await expect(applyMealTemplate(LOGIN, 'neexistuje', DATE)).rejects.toThrow(/nebyla nalezena/);
|
||||
await expect(deleteMealTemplate(LOGIN, 'neexistuje')).rejects.toThrow(/nebyla nalezena/);
|
||||
});
|
||||
|
||||
it('šablony jídel jsou oddělené mezi uživateli', async () => {
|
||||
await saveMealTemplate(LOGIN, salat);
|
||||
it('recepty jsou oddělené mezi uživateli', async () => {
|
||||
await saveMealTemplate(LOGIN, rizoto);
|
||||
|
||||
expect(await getMealTemplates('nekdo-jiny')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('zaniklé šablony hotových jídel', () => {
|
||||
|
||||
const KEY = `mealTemplates:${LOGIN}`;
|
||||
|
||||
/** Úložiště tak, jak vypadalo před zrušením hotových jídel. */
|
||||
const seedLegacy = async () => {
|
||||
await getStorage().setData(KEY, [
|
||||
{ id: 'stara', name: 'Sušenka', calories: 80 },
|
||||
{ id: 'hotove', kind: 'HOTOVE', name: 'Salát', weight: 350, caloriesPer100g: 160, pricePer100g: 3700 },
|
||||
{
|
||||
id: 'recept', kind: 'RECEPT', name: 'Guláš', weight: 400, calories: 600,
|
||||
ingredients: [{ name: 'Hovězí', weight: 400, calories: 600 }],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
it('se nevracejí, recepty zůstanou bez starého druhu', async () => {
|
||||
await seedLegacy();
|
||||
|
||||
const templates = await getMealTemplates(LOGIN);
|
||||
|
||||
expect(templates.map(item => item.id)).toEqual(['recept']);
|
||||
expect(templates[0]).not.toHaveProperty('kind');
|
||||
});
|
||||
|
||||
it('zápis je z úložiště odklidí', async () => {
|
||||
await seedLegacy();
|
||||
|
||||
await saveMealTemplate(LOGIN, { name: 'Polévka', ingredients: [{ name: 'Mrkev', weight: 100, calories: 40 }] });
|
||||
|
||||
const stored = await getStorage().getData<{ id: string, name: string, kind?: string }[]>(KEY) ?? [];
|
||||
expect(stored.map(item => item.name).sort()).toEqual(['Guláš', 'Polévka']);
|
||||
expect(stored.some(item => item.kind)).toBe(false);
|
||||
});
|
||||
|
||||
it('jídla z nich založená ve dnech zůstanou', async () => {
|
||||
await seedLegacy();
|
||||
await getStorage().setData(`meals:${LOGIN}:${DATE}`, [{
|
||||
id: 'zeSablony', date: DATE, mealType: MealType.OBED, name: 'Salát',
|
||||
calories: 560, templateId: 'hotove', createdAt: '2025-03-03T10:00:00.000Z',
|
||||
}]);
|
||||
|
||||
await deleteMealTemplate(LOGIN, 'recept');
|
||||
|
||||
const day = await getDay(LOGIN, DATE);
|
||||
expect(day.entries).toHaveLength(1);
|
||||
expect(day.totalCalories).toBe(560);
|
||||
});
|
||||
});
|
||||
@@ -1,78 +1,15 @@
|
||||
process.env.CALORIE_PROVIDER = 'none';
|
||||
|
||||
import { addActivity, getActivityDay } from "../activities";
|
||||
import { addMeal, getDay } from "../meals";
|
||||
import { createMealTemplateFromMeal, getMealTemplates } from "../mealTemplates";
|
||||
import { createWorkoutTemplateFromActivities, getWorkoutTemplates } from "../workouts";
|
||||
import { resetMemoryStorage } from "../storage/memory";
|
||||
import { ActivityUnit, MealType } from "../../../types/gen/types.gen";
|
||||
import { ActivityUnit } from "../../../types/gen/types.gen";
|
||||
|
||||
const LOGIN = 'tester';
|
||||
const DATE = '2025-03-03';
|
||||
|
||||
beforeEach(() => resetMemoryStorage());
|
||||
|
||||
describe('překlopení jídla do šablony', () => {
|
||||
|
||||
const gyros = {
|
||||
date: DATE, mealType: MealType.OBED, name: 'Kuřecí gyros',
|
||||
source: 'TechTower', price: 17600, pricePer100g: 4400, caloriesPer100g: 172,
|
||||
};
|
||||
|
||||
it('převezme název, zdroj, gramáž i jednotkové hodnoty', async () => {
|
||||
const day = await addMeal(LOGIN, gyros);
|
||||
|
||||
const templates = await createMealTemplateFromMeal(LOGIN, DATE, day.entries[0].id);
|
||||
|
||||
expect(templates).toHaveLength(1);
|
||||
expect(templates[0]).toMatchObject({
|
||||
name: 'Kuřecí gyros',
|
||||
source: 'TechTower',
|
||||
mealType: MealType.OBED,
|
||||
weight: 400,
|
||||
caloriesPer100g: 172,
|
||||
price: 17600,
|
||||
pricePer100g: 4400,
|
||||
});
|
||||
});
|
||||
|
||||
it('jídlo v dni zůstane beze změny', async () => {
|
||||
const day = await addMeal(LOGIN, gyros);
|
||||
|
||||
await createMealTemplateFromMeal(LOGIN, DATE, day.entries[0].id);
|
||||
|
||||
expect((await getDay(LOGIN, DATE)).entries).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('opakované uložení téhož jídla šablonu přepíše, nezaloží druhou', async () => {
|
||||
const day = await addMeal(LOGIN, gyros);
|
||||
await createMealTemplateFromMeal(LOGIN, DATE, day.entries[0].id);
|
||||
|
||||
// Totéž jídlo jindy a s jinou gramáží
|
||||
const later = await addMeal(LOGIN, { ...gyros, date: '2025-03-10', price: 22000 });
|
||||
const templates = await createMealTemplateFromMeal(LOGIN, '2025-03-10', later.entries[0].id);
|
||||
|
||||
expect(templates).toHaveLength(1);
|
||||
expect(templates[0].weight).toBe(500);
|
||||
});
|
||||
|
||||
it('u jídla s ručními kaloriemi uloží absolutní hodnotu', async () => {
|
||||
const day = await addMeal(LOGIN, {
|
||||
date: DATE, mealType: MealType.SNIDANE, name: 'Jablko', calories: 80,
|
||||
});
|
||||
|
||||
const templates = await createMealTemplateFromMeal(LOGIN, DATE, day.entries[0].id);
|
||||
|
||||
expect(templates[0].calories).toBe(80);
|
||||
expect(templates[0].caloriesPer100g).toBeUndefined();
|
||||
});
|
||||
|
||||
it('hlásí chybu u neexistujícího jídla', async () => {
|
||||
await expect(createMealTemplateFromMeal(LOGIN, DATE, 'neexistuje'))
|
||||
.rejects.toThrow(/nebylo nalezeno/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('překlopení aktivit do šablony tréninku', () => {
|
||||
|
||||
const kliky = {
|
||||
@@ -163,6 +100,5 @@ describe('překlopení aktivit do šablony tréninku', () => {
|
||||
await createWorkoutTemplateFromActivities(LOGIN, DATE, 'Silový blok');
|
||||
|
||||
expect(await getWorkoutTemplates('nekdo-jiny')).toEqual([]);
|
||||
expect(await getMealTemplates('nekdo-jiny')).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user