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(); }); });