Files
food-tracer/server/src/tests/workoutComposition.test.ts
T

248 lines
11 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 { applyWorkoutTemplate, deleteWorkoutTemplate, getWorkoutTemplates, saveWorkoutTemplate } from "../workouts";
import { getActivityDay } from "../activities";
import { getSettings, saveBasalCalories, saveProfile } from "../settings";
import { computeBasalCalories } from "../profile";
import { getDayOverview } from "../dayOverview";
import { saveSleep } from "../sleep";
import { resetMemoryStorage } from "../storage/memory";
import { ActivityUnit, Pohlavi, WorkoutItemKind } from "../../../types/gen/types.gen";
const LOGIN = 'tester';
const DATE = '2025-03-03';
/** Cvik jako položka šablony. */
const cvik = (name: string, quantity: number, rate: number) => ({
kind: WorkoutItemKind.CVIK,
name,
unit: ActivityUnit.OPAKOVANI,
quantity,
caloriesPer100Units: rate,
});
/** Odkaz na jinou šablonu. */
const odkaz = (templateRefId: string) => ({ kind: WorkoutItemKind.SABLONA, templateRefId });
beforeEach(() => resetMemoryStorage());
describe('skládání šablon z jiných šablon', () => {
it('šablona složená z jiné sečte kalorie obou', async () => {
const [rozcvicka] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [cvik('Dřepy', 40, 40)]);
const templates = await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', [
odkaz(rozcvicka.id),
cvik('Kliky', 60, 50),
]);
const workout = templates.find(t => t.name === 'Workout day 1')!;
// 40 dřepů × 0,4 kcal + 60 kliků × 0,5 kcal
expect(workout.estimatedCalories).toBe(46);
expect(workout.hasNested).toBe(true);
});
it('doplní název a kalorie vnořené šablony', async () => {
const [rozcvicka] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [cvik('Dřepy', 40, 40)]);
await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', [odkaz(rozcvicka.id)]);
const workout = (await getWorkoutTemplates(LOGIN)).find(t => t.name === 'Workout day 1')!;
expect(workout.items[0].templateRefName).toBe('Rozcvička');
expect(workout.items[0].templateRefCalories).toBe(16);
});
it('použití rozbalí vnořenou šablonu na její cviky', async () => {
const [rozcvicka] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [
cvik('Dřepy', 40, 40),
cvik('Výpady', 20, 30),
]);
const templates = await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', [
odkaz(rozcvicka.id),
cvik('Kliky', 60, 50),
]);
const workout = templates.find(t => t.name === 'Workout day 1')!;
await applyWorkoutTemplate(LOGIN, workout.id, DATE);
const day = await getActivityDay(LOGIN, DATE);
expect(day.entries.map(e => e.name)).toEqual(['Dřepy', 'Výpady', 'Kliky']);
expect(day.totalCalories).toBe(52);
});
it('počítá, kolik jiných šablon danou šablonu používá', async () => {
const [rozcvicka] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [cvik('Dřepy', 40, 40)]);
await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', [odkaz(rozcvicka.id)]);
await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 2', [odkaz(rozcvicka.id)]);
const updated = (await getWorkoutTemplates(LOGIN)).find(t => t.name === 'Rozcvička')!;
expect(updated.usedByCount).toBe(2);
});
it('nedovolí zanoření o dvě úrovně', async () => {
const [zaklad] = await saveWorkoutTemplate(LOGIN, undefined, 'Základ', [cvik('Dřepy', 40, 40)]);
const templates = await saveWorkoutTemplate(LOGIN, undefined, 'Střední', [odkaz(zaklad.id)]);
const stredni = templates.find(t => t.name === 'Střední')!;
// "Střední" už sama skládá jinou šablonu, takže ji nelze vnořit dál
await expect(saveWorkoutTemplate(LOGIN, undefined, 'Vrchní', [odkaz(stredni.id)]))
.rejects.toThrow(/jen na jednu úroveň/);
});
it('nedovolí začít vnořovat v šabloně, kterou už někdo skládá', async () => {
const [rozcvicka] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [cvik('Dřepy', 40, 40)]);
const [jiny] = await saveWorkoutTemplate(LOGIN, undefined, 'Jiný blok', [cvik('Kliky', 60, 50)]);
await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', [odkaz(rozcvicka.id)]);
// Rozcvičku už skládá Workout day 1, takže do ní nelze vnořit další šablonu
await expect(saveWorkoutTemplate(LOGIN, rozcvicka.id, 'Rozcvička', [odkaz(jiny.id)]))
.rejects.toThrow(/jen na jednu úroveň/);
});
it('nedovolí odkaz sám na sebe', async () => {
const [sablona] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [cvik('Dřepy', 40, 40)]);
await expect(saveWorkoutTemplate(LOGIN, sablona.id, 'Rozcvička', [odkaz(sablona.id)]))
.rejects.toThrow(/sama na sebe/);
});
it('nedovolí odkaz na neexistující šablonu', async () => {
await expect(saveWorkoutTemplate(LOGIN, undefined, 'Workout', [odkaz('neexistuje')]))
.rejects.toThrow(/nebyla nalezena/);
});
it('nedovolí smazat šablonu, kterou skládá jiná', async () => {
const [rozcvicka] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [cvik('Dřepy', 40, 40)]);
await saveWorkoutTemplate(LOGIN, undefined, 'Workout day 1', [odkaz(rozcvicka.id)]);
await expect(deleteWorkoutTemplate(LOGIN, rozcvicka.id)).rejects.toThrow(/nelze smazat/);
});
it('šablonu bez vazeb smazat lze', async () => {
const [rozcvicka] = await saveWorkoutTemplate(LOGIN, undefined, 'Rozcvička', [cvik('Dřepy', 40, 40)]);
expect(await deleteWorkoutTemplate(LOGIN, rozcvicka.id)).toEqual([]);
});
it('šablonám uloženým bez druhu položky doplní CVIK', async () => {
// Podoba dat z doby, kdy skládání z jiných šablon ještě neexistovalo
const { default: getStorage } = await import("../storage");
await getStorage().setData('workouts:' + LOGIN, [{
id: 'stara', name: 'Stará šablona',
items: [{ name: 'Kliky', unit: ActivityUnit.OPAKOVANI, quantity: 60, caloriesPer100Units: 50 }],
}]);
const [template] = await getWorkoutTemplates(LOGIN);
expect(template.items[0].kind).toBe(WorkoutItemKind.CVIK);
expect(template.estimatedCalories).toBe(30);
expect(template.hasNested).toBe(false);
});
});
describe('tělesné údaje a bazální metabolismus', () => {
/** Muž, 90 kg, 185 cm, ročník 1990. */
const muz = { sex: Pohlavi.MUZ, birthYear: 1990, heightCm: 185, weightGrams: 90_000 };
it('spočítá klidový výdej rovnicí Mifflin–St Jeor', () => {
const age = new Date().getFullYear() - 1990;
// 10 × 90 + 6,25 × 185 − 5 × věk + 5
const expected = Math.round(900 + 1156.25 - 5 * age + 5);
expect(computeBasalCalories(muz)).toBe(expected);
});
it('u ženy odečítá 161 místo přičtení 5', () => {
const zena = { ...muz, sex: Pohlavi.ZENA };
expect(computeBasalCalories(muz)! - computeBasalCalories(zena)!).toBe(166);
});
it('neúplný profil klidový výdej nespočítá', () => {
expect(computeBasalCalories(undefined)).toBeUndefined();
expect(computeBasalCalories({ sex: Pohlavi.MUZ, heightCm: 185 })).toBeUndefined();
});
it('uložený profil se propíše do nastavení i do bilance dne', async () => {
await saveProfile(LOGIN, muz);
const settings = await getSettings(LOGIN);
expect(settings.computedBasalCalories).toBe(computeBasalCalories(muz));
expect(settings.effectiveBasalCalories).toBe(computeBasalCalories(muz));
const overview = await getDayOverview(LOGIN, DATE);
expect(overview.energy.basal).toBe(computeBasalCalories(muz));
expect(overview.energy.hasBasal).toBe(true);
});
it('ručně zadaný klidový výdej má přednost před spočítaným', async () => {
await saveProfile(LOGIN, muz);
await saveBasalCalories(LOGIN, 1800);
const settings = await getSettings(LOGIN);
expect(settings.computedBasalCalories).toBe(computeBasalCalories(muz));
expect(settings.effectiveBasalCalories).toBe(1800);
expect((await getDayOverview(LOGIN, DATE)).energy.basal).toBe(1800);
});
it('po smazání ručního údaje se vrátí spočítaný z profilu', async () => {
await saveProfile(LOGIN, muz);
await saveBasalCalories(LOGIN, 1800);
await saveBasalCalories(LOGIN, 0);
expect((await getSettings(LOGIN)).effectiveBasalCalories).toBe(computeBasalCalories(muz));
});
it('odmítne nesmyslné tělesné údaje', async () => {
await expect(saveProfile(LOGIN, { ...muz, heightCm: 5 })).rejects.toThrow(/mimo rozsah/);
await expect(saveProfile(LOGIN, { ...muz, weightGrams: 5_000_000 })).rejects.toThrow(/mimo rozsah/);
await expect(saveProfile(LOGIN, { ...muz, sex: 'JINE' })).rejects.toThrow(/pohlaví/);
});
it('uložení profilu nepřepíše sazby podniků', async () => {
const { saveSourceRate } = await import("../settings");
await saveSourceRate(LOGIN, 'TechTower', 4400);
await saveProfile(LOGIN, muz);
expect((await getSettings(LOGIN)).sourceRates).toHaveLength(1);
});
});
describe('spánek', () => {
it('uloží se ke dni a vrátí v přehledu', async () => {
await saveSleep(LOGIN, DATE, 450);
expect((await getDayOverview(LOGIN, DATE)).sleepMinutes).toBe(450);
});
it('nulová hodnota záznam odstraní', async () => {
await saveSleep(LOGIN, DATE, 450);
await saveSleep(LOGIN, DATE, 0);
expect((await getDayOverview(LOGIN, DATE)).sleepMinutes).toBeUndefined();
});
it('do energetické bilance nevstupuje', async () => {
await saveProfile(LOGIN, { sex: Pohlavi.MUZ, birthYear: 1990, heightCm: 185, weightGrams: 90_000 });
const bezSpanku = await getDayOverview(LOGIN, DATE);
await saveSleep(LOGIN, DATE, 450);
const seSpankem = await getDayOverview(LOGIN, DATE);
// Klidový výdej pokrývá celých 24 hodin včetně spánku, bilance se měnit nesmí
expect(seSpankem.energy).toEqual(bezSpanku.energy);
});
it('odmítne nesmyslnou délku', async () => {
await expect(saveSleep(LOGIN, DATE, 2000)).rejects.toThrow(/mimo rozsah/);
await expect(saveSleep(LOGIN, 'včera', 450)).rejects.toThrow(/platné datum/);
});
it('je oddělený mezi uživateli', async () => {
await saveSleep(LOGIN, DATE, 450);
expect((await getDayOverview('nekdo-jiny', DATE)).sleepMinutes).toBeUndefined();
});
});