process.env.CALORIE_PROVIDER = 'none'; import { addActivity, deleteActivity, getActivityDay, setActivityMultiplier, updateActivity } from "../activities"; import { applyWorkoutTemplate, deleteWorkoutTemplate, saveWorkoutTemplate } from "../workouts"; import { getActivityStats } from "../statsService"; import { getWorkoutPlans } from "../workoutPlans"; import { resetMemoryStorage } from "../storage/memory"; import { applyMealTemplate, saveMealTemplate } from "../mealTemplates"; import { addMeal, getDay } from "../meals"; import { ActivityUnit, MealType, PlanDeviationKind, WorkoutItemKind } from "../../../types/gen/types.gen"; const LOGIN = 'tester'; const DATE = '2025-03-03'; const NEXT_DATE = '2025-03-04'; /** Cvik jako položka šablony. */ const cvik = (name: string, quantity: number, rate: number) => ({ kind: WorkoutItemKind.CVIK, name, unit: ActivityUnit.OPAKOVANI, quantity, caloriesPer100Units: rate, }); /** Založí šablonu Kliky 100 × 0,5 kcal + Dřepy 100 × 0,3 kcal, tedy 80 kcal. */ async function createWorkout(): Promise { const [template] = await saveWorkoutTemplate(LOGIN, undefined, 'Workout', [ cvik('Kliky', 100, 50), cvik('Dřepy', 100, 30), ]); return template.id; } const stats = () => getActivityStats(LOGIN, '2025-03-01', '2025-03-31'); beforeEach(() => resetMemoryStorage()); describe('statistiky výdeje', () => { it('odcvičená šablona se shoduje s plánem', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); const result = await stats(); expect(result.templateCalories).toBe(80); expect(result.plannedCalories).toBe(80); expect(result.extraCalories).toBe(0); expect(result.byTemplate).toEqual([expect.objectContaining({ name: 'Workout', dayCount: 1, plannedCalories: 80, actualCalories: 80, plannedItemCount: 2, doneItemCount: 2, })]); }); it('smazaný cvik ze šablony se projeví jako odchylka', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); const drepy = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Dřepy')!; await deleteActivity(LOGIN, drepy.id, DATE); const result = await stats(); expect(result.templateCalories).toBe(50); expect(result.plannedCalories).toBe(80); expect(result.byTemplate[0]).toEqual(expect.objectContaining({ plannedItemCount: 2, doneItemCount: 1 })); }); it('zkrácený cvik ze šablony se projeví jako odchylka', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); const kliky = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Kliky')!; await setActivityMultiplier(LOGIN, kliky.id, DATE, 0.5); const result = await stats(); expect(result.templateCalories).toBe(55); expect(result.plannedCalories).toBe(80); }); it('smazání všech cviků šablony vrátí její použití, nepočítá se jako odchylka', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); for (const entry of (await getActivityDay(LOGIN, DATE)).entries) { await deleteActivity(LOGIN, entry.id, DATE); } expect(await getWorkoutPlans(LOGIN, DATE)).toEqual([]); const result = await stats(); expect(result.plannedCalories).toBe(0); expect(result.byTemplate).toEqual([]); }); it('plán se drží podoby šablony v době použití, ne její pozdější úpravy', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); await saveWorkoutTemplate(LOGIN, id, 'Workout v2', [cvik('Kliky', 200, 50)]); const result = await stats(); expect(result.plannedCalories).toBe(80); expect(result.byTemplate[0].name).toBe('Workout'); }); it('aktivity mimo šablonu se sčítají zvlášť podle názvu', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); await addActivity(LOGIN, { date: DATE, name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4 }); await addActivity(LOGIN, { date: NEXT_DATE, name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4 }); const result = await stats(); expect(result.totalCalories).toBe(480); expect(result.extraCalories).toBe(400); expect(result.extras).toEqual([{ key: 'Chůze', calories: 400, count: 2 }]); expect(result.dayCount).toBe(2); expect(result.byDay).toEqual([ { period: DATE, templateCalories: 80, plannedCalories: 80, extraCalories: 200 }, { period: NEXT_DATE, templateCalories: 0, plannedCalories: 0, extraCalories: 200 }, ]); }); it('cvik přesunutý do jiného dne chybí v původním a v novém se bere bez odchylky', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); const kliky = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Kliky')!; await updateActivity(LOGIN, kliky.id, { ...kliky, date: NEXT_DATE }); const result = await stats(); // Původní den: plán 80, zůstaly dřepy za 30; nový den: kliky 50 bez plánu expect(result.templateCalories).toBe(80); expect(result.plannedCalories).toBe(130); }); it('šablona smazaná po použití se ve statistikách drží názvu z plánu', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); await deleteWorkoutTemplate(LOGIN, id); const result = await stats(); expect(result.byTemplate[0].name).toBe('Workout'); }); it('odmítne obrácený rozsah', async () => { await expect(getActivityStats(LOGIN, '2025-03-31', '2025-03-01')).rejects.toThrow(); }); }); describe('denní detail výdeje', () => { it('ukáže, jaká šablona se použila a který cvik z ní chybí', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); const drepy = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Dřepy')!; await deleteActivity(LOGIN, drepy.id, DATE); const [day] = (await stats()).days; expect(day.date).toBe(DATE); expect(day.templates).toEqual([expect.objectContaining({ name: 'Workout', plannedCalories: 80, actualCalories: 50, hasPlan: true, deviations: [{ kind: PlanDeviationKind.ODEBRANO, planned: { name: 'Dřepy', unit: ActivityUnit.OPAKOVANI, quantity: 100, calories: 30 }, }], })]); }); it('upravený cvik ukáže plán i skutečnost', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); const kliky = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Kliky')!; await updateActivity(LOGIN, kliky.id, { ...kliky, quantity: 40 }); const [usage] = (await stats()).days[0].templates; expect(usage.deviations).toEqual([{ kind: PlanDeviationKind.UPRAVENO, planned: expect.objectContaining({ name: 'Kliky', quantity: 100, calories: 50 }), actual: expect.objectContaining({ name: 'Kliky', quantity: 40, calories: 20 }), }]); }); it('odcvičená šablona nemá žádnou odchylku', async () => { const id = await createWorkout(); await applyWorkoutTemplate(LOGIN, id, DATE); expect((await stats()).days[0].templates[0].deviations).toEqual([]); }); it('aktivity mimo šablonu vrátí jednotlivě i se součtem', async () => { await addActivity(LOGIN, { date: DATE, name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4 }); await addActivity(LOGIN, { date: DATE, name: 'Běh', unit: ActivityUnit.MINUTY, quantity: 20, caloriesPer100Units: 1000 }); const [day] = (await stats()).days; expect(day.templates).toEqual([]); expect(day.extraCalories).toBe(400); expect(day.extras.map(item => item.name)).toEqual(['Chůze', 'Běh']); }); }); describe('vazba jídla na recept', () => { it('jídlo z receptu si pamatuje, ze kterého vzniklo', async () => { const [template] = await saveMealTemplate(LOGIN, { name: 'Štrůdl', ingredients: [{ name: 'Těsto', weight: 300, calories: 900 }], }); await applyMealTemplate(LOGIN, template.id, DATE, MealType.PULNOCNI_SVACINA); const [meal] = (await getDay(LOGIN, DATE)).entries; expect(meal.templateId).toBe(template.id); expect(meal.mealType).toBe(MealType.PULNOCNI_SVACINA); }); it('ručně zadané jídlo vazbu nemá a klient ji podstrčit nemůže', async () => { await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'Guláš', templateId: 'podvrh' }); const [meal] = (await getDay(LOGIN, DATE)).entries; expect(meal.templateId).toBeUndefined(); }); });