213 lines
9.2 KiB
TypeScript
213 lines
9.2 KiB
TypeScript
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();
|
||
});
|
||
});
|