feat: zlepšení šablon, přidány osobní údaje a spánkový režim
This commit is contained in:
1 parent
a5711f3e1e
commit
21b01528e9
51 files changed
+3739
-504
No files matched your search
@@ -0,0 +1,247 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user