Files
food-tracer/server/src/tests/activityStats.test.ts
T
Ondřej Anděl e13f7ce8e5 fix: recepty přesnuty k jídlům
feat: přidána podpora fotek
2026-10-12 10:20:12 +02:00

213 lines
9.2 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 { 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<string> {
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();
});
});