fix: rozdílné jednotky u receptů
feat: vazby a trvání receptů
This commit is contained in:
1 parent
caffb6d8b4
commit
865c2c3b22
18 files changed
+952
-78
No files matched your search
+132
-25
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { MealTemplate, MealType, RecipeFlavor, RecipeIngredient } from "../../types/gen/types.gen";
|
||||
import { IngredientUnit, MealTemplate, MealType, RecipeFlavor, RecipeIngredient } from "../../types/gen/types.gen";
|
||||
import getStorage from "./storage";
|
||||
import { addMeal, getNameSourceKey, isMealType } from "./meals";
|
||||
import { rememberCalories } from "./calories";
|
||||
@@ -14,15 +14,34 @@ const MAX_NOTE_LENGTH = 4000;
|
||||
/** Maximální počet surovin receptu — pojistka proti nesmyslnému vstupu. */
|
||||
const MAX_INGREDIENTS = 50;
|
||||
|
||||
/** Maximální množství suroviny v jiné jednotce než gramech. */
|
||||
const MAX_AMOUNT = 100000;
|
||||
|
||||
/** Nejdelší čas přípravy v minutách (týden — kvašení, nakládání). */
|
||||
const MAX_PREP_MINUTES = 7 * 24 * 60;
|
||||
|
||||
/** Maximální počet vazeb jednoho druhu u receptu. */
|
||||
const MAX_LINKS = 50;
|
||||
|
||||
/** Vynechá nevyplněné hodnoty, aby se do úložiště nepsaly prázdné klíče. */
|
||||
const definedOnly = <T extends object>(value: T): T =>
|
||||
Object.fromEntries(Object.entries(value).filter(([, item]) => item != null)) as T;
|
||||
|
||||
/** Klíč úložiště s recepty jednoho uživatele. */
|
||||
const getKey = (login: string): string => `mealTemplates:${login}`;
|
||||
|
||||
/**
|
||||
* Recept s vazbami jen ze své strany, jak se ukládá. Přílohy nese hlavní jídlo
|
||||
* a podobnost jen jeden recept z dvojice; druhou stranu dopočte `withLinks`.
|
||||
*/
|
||||
type OwnRecipe = Omit<MealTemplate, 'sideDishOfIds'>;
|
||||
|
||||
/**
|
||||
* Záznam tak, jak může ležet v úložišti. Dřív se pod stejným klíčem ukládaly
|
||||
* i šablony hotových jídel (druh `HOTOVE`, nebo bez druhu), recepty nesly
|
||||
* `kind: 'RECEPT'` a měly i chod.
|
||||
*/
|
||||
type StoredMealTemplate = Omit<MealTemplate, 'ingredients' | 'flavor'> & {
|
||||
type StoredMealTemplate = Omit<OwnRecipe, 'ingredients' | 'flavor'> & {
|
||||
ingredients?: RecipeIngredient[];
|
||||
flavor?: RecipeFlavor;
|
||||
kind?: string;
|
||||
@@ -42,7 +61,7 @@ const isRecipe = (stored: StoredMealTemplate): stored is StoredRecipe => !!store
|
||||
* Odstraní z receptu pole, která patřila jen šablonám hotových jídel, a zaniklý chod.
|
||||
* Slaný recept je výchozí stav a v úložišti chuť nemá, doplní se tady.
|
||||
*/
|
||||
const toMealTemplate = (stored: StoredRecipe): MealTemplate => {
|
||||
const toOwnRecipe = (stored: StoredRecipe): OwnRecipe => {
|
||||
const { kind, pricePer100g, mealType, ...rest } = stored;
|
||||
return { ...rest, flavor: rest.flavor ?? RecipeFlavor.SLANE };
|
||||
};
|
||||
@@ -56,13 +75,41 @@ const normalizeFlavor = (value: unknown): RecipeFlavor => {
|
||||
return value;
|
||||
};
|
||||
|
||||
/** Recept tak, jak se ukládá — výchozí slanou chuť neukládá. */
|
||||
const toStored = ({ flavor, ...rest }: MealTemplate): StoredMealTemplate =>
|
||||
flavor === RecipeFlavor.SLANE ? rest : { ...rest, flavor };
|
||||
/** Seznam vazeb do úložiště i API — prázdný se vynechá. */
|
||||
const toLinkIds = (idList?: string[]): string[] | undefined => (idList?.length ? idList : undefined);
|
||||
|
||||
/** Recept tak, jak se ukládá — výchozí slanou chuť ani prázdné vazby neukládá. */
|
||||
const toStored = ({ flavor, sideDishIds, similarIds, ...rest }: OwnRecipe): StoredMealTemplate => definedOnly({
|
||||
...rest,
|
||||
flavor: flavor === RecipeFlavor.SLANE ? undefined : flavor,
|
||||
sideDishIds: toLinkIds(sideDishIds),
|
||||
similarIds: toLinkIds(similarIds),
|
||||
});
|
||||
|
||||
/** Načte recepty z úložiště; šablony hotových jídel vynechá. */
|
||||
const loadRecipes = (storedList: StoredMealTemplate[] | undefined): MealTemplate[] =>
|
||||
(storedList ?? []).filter(isRecipe).map(toMealTemplate);
|
||||
const loadRecipes = (storedList: StoredMealTemplate[] | undefined): OwnRecipe[] =>
|
||||
(storedList ?? []).filter(isRecipe).map(toOwnRecipe);
|
||||
|
||||
/**
|
||||
* Doplní receptům druhou stranu vazeb: přílohu všem jídlům, která ji uvádějí,
|
||||
* a podobnost oběma receptům dvojice. Vazby na smazané recepty vynechá,
|
||||
* ostatní seřadí podle názvu receptu.
|
||||
*/
|
||||
const withLinks = (recipeList: OwnRecipe[]): MealTemplate[] => {
|
||||
const nameMap = new Map(recipeList.map(recipe => [recipe.id, recipe.name]));
|
||||
const sortByName = (idList: string[]): string[] | undefined => toLinkIds([...new Set(idList)]
|
||||
.filter(id => nameMap.has(id))
|
||||
.sort((a, b) => nameMap.get(a)!.localeCompare(nameMap.get(b)!, 'cs')));
|
||||
const findLinking = (recipeId: string, getIds: (recipe: OwnRecipe) => string[] | undefined): string[] =>
|
||||
recipeList.filter(other => getIds(other)?.includes(recipeId)).map(other => other.id);
|
||||
|
||||
return recipeList.map(recipe => definedOnly({
|
||||
...recipe,
|
||||
sideDishIds: sortByName(recipe.sideDishIds ?? []),
|
||||
sideDishOfIds: sortByName(findLinking(recipe.id, other => other.sideDishIds)),
|
||||
similarIds: sortByName([...recipe.similarIds ?? [], ...findLinking(recipe.id, other => other.similarIds)]),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Vrátí recepty uživatele seřazené podle názvu.
|
||||
@@ -70,7 +117,7 @@ const loadRecipes = (storedList: StoredMealTemplate[] | undefined): MealTemplate
|
||||
* @param login přihlašovací jméno uživatele
|
||||
*/
|
||||
export const getMealTemplates = async (login: string): Promise<MealTemplate[]> =>
|
||||
loadRecipes(await storage.getData<StoredMealTemplate[]>(getKey(login)))
|
||||
withLinks(loadRecipes(await storage.getData<StoredMealTemplate[]>(getKey(login))))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'cs'));
|
||||
|
||||
/** Hodnoty hotového jídla a jedné porce receptu dopočtené ze surovin. */
|
||||
@@ -113,6 +160,9 @@ const sumDefined = (valueList: (number | undefined)[]): number | undefined => {
|
||||
* vařením se voda odpaří nebo nasákne (rýže, těstoviny), takže by vyšla špatně.
|
||||
* Porci si pak jde převážit a kalorie se dopočtou z gramáže jako u každého jídla.
|
||||
*
|
||||
* Počítá se vždy z gramáže; surovina v jiné jednotce (2 ks vajec) do výpočtu
|
||||
* vstupuje jen svým přepočtem na gramy, pokud ho má.
|
||||
*
|
||||
* Klient má stejnou funkci v `Utils.tsx` pro živý náhled — musí dávat stejné výsledky.
|
||||
*
|
||||
* @param ingredients suroviny receptu
|
||||
@@ -169,9 +219,17 @@ const normalizeNumber = (value: unknown, name: string): number | undefined => {
|
||||
return rounded;
|
||||
};
|
||||
|
||||
/** Vynechá nevyplněné hodnoty, aby se do úložiště nepsaly prázdné klíče. */
|
||||
const definedOnly = <T extends object>(value: T): T =>
|
||||
Object.fromEntries(Object.entries(value).filter(([, item]) => item != null)) as T;
|
||||
const isIngredientUnit = (value: unknown): value is IngredientUnit =>
|
||||
Object.values(IngredientUnit).includes(value as IngredientUnit);
|
||||
|
||||
/** Ověří volitelné kladné množství suroviny; necelé na setiny (0,25 l). */
|
||||
const normalizeAmount = (value: unknown, index: number): number | undefined => {
|
||||
if (value == null) return undefined;
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > MAX_AMOUNT) {
|
||||
throw new BadRequestError(`Surovina ${index + 1} nemá platné množství`);
|
||||
}
|
||||
return Math.round(value * 100) / 100;
|
||||
};
|
||||
|
||||
/** Ověří a znormalizuje jednu surovinu receptu. */
|
||||
const normalizeIngredient = (raw: unknown, index: number): RecipeIngredient => {
|
||||
@@ -180,12 +238,19 @@ const normalizeIngredient = (raw: unknown, index: number): RecipeIngredient => {
|
||||
if (!name) {
|
||||
throw new BadRequestError(`Surovina ${index + 1} nemá název`);
|
||||
}
|
||||
if (value.unit != null && !isIngredientUnit(value.unit)) {
|
||||
throw new BadRequestError(`Surovina ${index + 1} nemá platnou jednotku`);
|
||||
}
|
||||
const unit = value.unit ?? undefined;
|
||||
const weight = normalizeNumber(value.weight, 'weight');
|
||||
const caloriesPer100g = normalizeNumber(value.caloriesPer100g, 'caloriesPer100g');
|
||||
const calories = normalizeNumber(value.calories, 'calories');
|
||||
const price = normalizeNumber(value.price, 'price');
|
||||
return definedOnly({
|
||||
name,
|
||||
// Bez jednotky je množstvím gramáž, samostatné množství by ji jen zdvojovalo
|
||||
amount: unit ? normalizeAmount(value.amount, index) : undefined,
|
||||
unit,
|
||||
weight,
|
||||
caloriesPer100g,
|
||||
// Ručně zadaná energie platí jen tam, kde ji nejde dopočítat z gramáže
|
||||
@@ -194,8 +259,35 @@ const normalizeIngredient = (raw: unknown, index: number): RecipeIngredient => {
|
||||
});
|
||||
};
|
||||
|
||||
/** Ověří seznam propojených receptů — musí existovat a nesmí to být recept sám. */
|
||||
const normalizeLinkIds = (value: unknown, recipeIds: Set<string>, selfId: string): string[] => {
|
||||
if (value == null) return [];
|
||||
if (!Array.isArray(value) || value.length > MAX_LINKS) {
|
||||
throw new BadRequestError(`Recept může mít nejvýše ${MAX_LINKS} vazeb jednoho druhu`);
|
||||
}
|
||||
if (value.some(id => typeof id !== 'string' || id === selfId || !recipeIds.has(id))) {
|
||||
throw new BadRequestError("Propojený recept nebyl nalezen");
|
||||
}
|
||||
return [...new Set(value as string[])];
|
||||
};
|
||||
|
||||
/**
|
||||
* Upraví vazby jiného receptu na recept `recipeId`. Vazby, které se ho týkají,
|
||||
* určuje jen on: odeberou se a přílohou se jiný recept znovu stane jen tehdy,
|
||||
* když je v `sideDishOfIds`. Podobnost si recept `recipeId` drží sám.
|
||||
*/
|
||||
const relinkOther = (other: OwnRecipe, recipeId: string, sideDishOfIds: string[] = []): OwnRecipe => ({
|
||||
...other,
|
||||
sideDishIds: [
|
||||
...(other.sideDishIds ?? []).filter(id => id !== recipeId),
|
||||
...(sideDishOfIds.includes(other.id) ? [recipeId] : []),
|
||||
],
|
||||
similarIds: (other.similarIds ?? []).filter(id => id !== recipeId),
|
||||
});
|
||||
|
||||
/** Klíč jídla, ke kterému recept patří — stejně jako u skupin v seznamu jídel. */
|
||||
export const getRecipeKey = (recipe: MealTemplate): string => getNameSourceKey(recipe.name, recipe.source);
|
||||
export const getRecipeKey = (recipe: Pick<MealTemplate, 'name' | 'source'>): string =>
|
||||
getNameSourceKey(recipe.name, recipe.source);
|
||||
|
||||
/**
|
||||
* Založí nový recept, nebo přepíše existující podle předaného id.
|
||||
@@ -205,6 +297,8 @@ export const getRecipeKey = (recipe: MealTemplate): string => getNameSourceKey(r
|
||||
*
|
||||
* Hodnoty jedné porce (gramáž, energie, cena) se dopočtou ze surovin a uloží
|
||||
* do plochých polí, takže použití receptu založí běžné jídlo jako každé jiné.
|
||||
* Vazby na jiné recepty (přílohy, podobné) se zapíšou i z druhé strany, takže
|
||||
* vazbu jde přidat i odebrat v kterémkoli z obou receptů.
|
||||
* Zápis zároveň z úložiště odklidí zaniklé šablony hotových jídel.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
@@ -225,10 +319,18 @@ export const saveMealTemplate = async (login: string, input: unknown): Promise<M
|
||||
const ingredients = value.ingredients.map(normalizeIngredient);
|
||||
const servings = normalizeNumber(value.servings, 'servings');
|
||||
const cookedWeight = normalizeNumber(value.cookedWeight, 'cookedWeight') || undefined;
|
||||
const prepMinutes = normalizeNumber(value.prepMinutes, 'prepMinutes') || undefined;
|
||||
if (prepMinutes != null && prepMinutes > MAX_PREP_MINUTES) {
|
||||
throw new BadRequestError("Čas přípravy může být nejvýše týden");
|
||||
}
|
||||
const summary = deriveRecipe(ingredients, servings, cookedWeight);
|
||||
const id = typeof value.id === 'string' && value.id.length ? value.id : randomUUID();
|
||||
const recipeList = await getMealTemplates(login);
|
||||
const recipeIds = new Set(recipeList.map(item => item.id));
|
||||
const sideDishOfIds = normalizeLinkIds(value.sideDishOfIds, recipeIds, id);
|
||||
|
||||
const template: MealTemplate = definedOnly({
|
||||
id: typeof value.id === 'string' && value.id.length ? value.id : randomUUID(),
|
||||
const template: OwnRecipe = definedOnly({
|
||||
id,
|
||||
name,
|
||||
source: normalizeText(value.source, 'source'),
|
||||
category: normalizeText(value.category, 'category'),
|
||||
@@ -238,12 +340,15 @@ export const saveMealTemplate = async (login: string, input: unknown): Promise<M
|
||||
// Jedna porce je výchozí stav, ukládat ji zvlášť nemá smysl
|
||||
servings: servings && servings > 1 ? servings : undefined,
|
||||
cookedWeight,
|
||||
prepMinutes,
|
||||
sideDishIds: normalizeLinkIds(value.sideDishIds, recipeIds, id),
|
||||
similarIds: normalizeLinkIds(value.similarIds, recipeIds, id),
|
||||
weight: summary.weight,
|
||||
caloriesPer100g: summary.caloriesPer100g,
|
||||
calories: summary.calories,
|
||||
price: summary.price,
|
||||
});
|
||||
const otherRecipe = (await getMealTemplates(login))
|
||||
const otherRecipe = recipeList
|
||||
.find(item => item.id !== template.id && getRecipeKey(item) === getRecipeKey(template));
|
||||
if (otherRecipe) {
|
||||
throw new BadRequestError("Toto jídlo už recept má");
|
||||
@@ -255,14 +360,17 @@ export const saveMealTemplate = async (login: string, input: unknown): Promise<M
|
||||
}
|
||||
|
||||
await storage.updateData<StoredMealTemplate[]>(getKey(login), current => [
|
||||
...loadRecipes(current).filter(item => item.id !== template.id).map(toStored),
|
||||
...loadRecipes(current)
|
||||
.filter(item => item.id !== template.id)
|
||||
.map(item => toStored(relinkOther(item, template.id, sideDishOfIds))),
|
||||
toStored(template),
|
||||
]);
|
||||
return getMealTemplates(login);
|
||||
};
|
||||
|
||||
/**
|
||||
* Smaže recept. Jídla z něj založená zůstávají, jsou samostatná.
|
||||
* Smaže recept i vazby ostatních receptů na něj. Jídla z něj založená
|
||||
* zůstávají, jsou samostatná.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
* @param id identifikátor receptu
|
||||
@@ -275,7 +383,7 @@ export const deleteMealTemplate = async (login: string, id: unknown): Promise<Me
|
||||
await storage.updateData<StoredMealTemplate[]>(getKey(login), current => {
|
||||
const recipeList = loadRecipes(current);
|
||||
found = recipeList.some(item => item.id === id);
|
||||
return recipeList.filter(item => item.id !== id).map(toStored);
|
||||
return recipeList.filter(item => item.id !== id).map(item => toStored(relinkOther(item, id)));
|
||||
});
|
||||
if (!found) {
|
||||
throw new NotFoundError("Recept nebyl nalezen");
|
||||
@@ -333,7 +441,7 @@ export const applyMealTemplate = async (
|
||||
|
||||
/**
|
||||
* Přejmenuje recept jídla spolu s jídlem — při hromadném přejmenování v seznamu
|
||||
* jídel, aby recept zůstal u svého jídla.
|
||||
* jídel, aby recept zůstal u svého jídla. Vazby jdou přes id, přejmenování je nemění.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
* @param fromKey dosavadní klíč jídla (`getNameSourceKey`)
|
||||
@@ -343,10 +451,9 @@ export const applyMealTemplate = async (
|
||||
export const renameMealTemplate = async (
|
||||
login: string, fromKey: string, name: string, source?: string,
|
||||
): Promise<void> => {
|
||||
const recipeList = await getMealTemplates(login);
|
||||
const recipe = recipeList.find(item => getRecipeKey(item) === fromKey);
|
||||
if (!recipe) return;
|
||||
const renamed: MealTemplate = definedOnly({ ...recipe, name, source });
|
||||
if (!(await getMealTemplates(login)).some(item => getRecipeKey(item) === fromKey)) return;
|
||||
await storage.updateData<StoredMealTemplate[]>(getKey(login), current =>
|
||||
loadRecipes(current).map(item => toStored(item.id === recipe.id ? renamed : item)));
|
||||
loadRecipes(current).map(item => toStored(getRecipeKey(item) === fromKey
|
||||
? definedOnly({ ...item, name, source })
|
||||
: item)));
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
process.env.CALORIE_PROVIDER = 'none';
|
||||
|
||||
import { deleteMealTemplate, getMealTemplates, saveMealTemplate } from "../mealTemplates";
|
||||
import { addMeal } from "../meals";
|
||||
import { renameMealGroup } from "../mealGroups";
|
||||
import { resetMemoryStorage } from "../storage/memory";
|
||||
import getStorage from "../storage";
|
||||
import { IngredientUnit, MealTemplate, MealType } from "../../../types/gen/types.gen";
|
||||
|
||||
const LOGIN = 'tester';
|
||||
|
||||
beforeEach(() => resetMemoryStorage());
|
||||
|
||||
const createRecipe = async (name: string, extra: Record<string, unknown> = {}): Promise<MealTemplate> => {
|
||||
const recipeList = await saveMealTemplate(LOGIN, {
|
||||
name, source: 'doma', ingredients: [{ name: 'Sůl', weight: 5 }], ...extra,
|
||||
});
|
||||
return recipeList.find(item => item.name === name)!;
|
||||
};
|
||||
|
||||
const findRecipe = async (name: string): Promise<MealTemplate> =>
|
||||
(await getMealTemplates(LOGIN)).find(item => item.name === name)!;
|
||||
|
||||
describe('jednotky surovin', () => {
|
||||
|
||||
it('surovina v kusech nese množství a přepočet na gramy počítá výživu', async () => {
|
||||
const recipe = await createRecipe('Omeleta', {
|
||||
ingredients: [
|
||||
{ name: 'Vejce', amount: 3, unit: IngredientUnit.KS, weight: 180, caloriesPer100g: 140 },
|
||||
{ name: 'Mléko', amount: 0.125, unit: IngredientUnit.L },
|
||||
],
|
||||
});
|
||||
|
||||
expect(recipe.ingredients).toEqual([
|
||||
{ name: 'Vejce', amount: 3, unit: IngredientUnit.KS, weight: 180, caloriesPer100g: 140 },
|
||||
// Bez přepočtu na gramy do výživy nevstupuje, množství se zaokrouhlí na setiny
|
||||
{ name: 'Mléko', amount: 0.13, unit: IngredientUnit.L },
|
||||
]);
|
||||
expect(recipe).toMatchObject({ weight: 180, calories: 252 });
|
||||
});
|
||||
|
||||
it('bez jednotky je množstvím gramáž a samostatné množství se zahodí', async () => {
|
||||
const recipe = await createRecipe('Rýže', { ingredients: [{ name: 'Rýže', amount: 3, weight: 200 }] });
|
||||
|
||||
expect(recipe.ingredients).toEqual([{ name: 'Rýže', weight: 200 }]);
|
||||
});
|
||||
|
||||
it('jednotka bez množství zůstane (špetka soli)', async () => {
|
||||
const recipe = await createRecipe('Polévka', { ingredients: [{ name: 'Sůl', unit: IngredientUnit.SPETKA }] });
|
||||
|
||||
expect(recipe.ingredients).toEqual([{ name: 'Sůl', unit: IngredientUnit.SPETKA }]);
|
||||
});
|
||||
|
||||
it('odmítne neznámou jednotku a nekladné množství', async () => {
|
||||
await expect(createRecipe('X', { ingredients: [{ name: 'A', unit: 'VEDRO', amount: 1 }] }))
|
||||
.rejects.toThrow(/jednotku/);
|
||||
await expect(createRecipe('X', { ingredients: [{ name: 'A', unit: IngredientUnit.KS, amount: 0 }] }))
|
||||
.rejects.toThrow(/množství/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('čas přípravy', () => {
|
||||
|
||||
it('uloží se v minutách, nula znamená nevyplněný', async () => {
|
||||
expect((await createRecipe('Guláš', { prepMinutes: 150 })).prepMinutes).toBe(150);
|
||||
expect((await createRecipe('Salát', { prepMinutes: 0 })).prepMinutes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('odmítne čas delší než týden', async () => {
|
||||
await expect(createRecipe('Kimči', { prepMinutes: 8 * 24 * 60 })).rejects.toThrow(/týden/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vazby mezi recepty', () => {
|
||||
|
||||
it('příloha se u druhého receptu ukáže jako "přílohou k"', async () => {
|
||||
const ryze = await createRecipe('Rýže');
|
||||
const kure = await createRecipe('Kuře na kari', { sideDishIds: [ryze.id] });
|
||||
|
||||
expect(kure.sideDishIds).toEqual([ryze.id]);
|
||||
expect(kure.sideDishOfIds).toBeUndefined();
|
||||
expect((await findRecipe('Rýže')).sideDishOfIds).toEqual([kure.id]);
|
||||
});
|
||||
|
||||
it('přílohu jde zapsat i z přílohy a uloží se u hlavního jídla', async () => {
|
||||
const kure = await createRecipe('Kuře na kari');
|
||||
const ryze = await createRecipe('Rýže', { sideDishOfIds: [kure.id] });
|
||||
|
||||
expect((await findRecipe('Kuře na kari')).sideDishIds).toEqual([ryze.id]);
|
||||
|
||||
// Uložení přílohy bez vazby ji odebere i z hlavního jídla
|
||||
await createRecipe('Rýže', { id: ryze.id });
|
||||
expect((await findRecipe('Kuře na kari')).sideDishIds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('podobnost je oboustranná a odebrat jde z kterékoli strany', async () => {
|
||||
const gulas = await createRecipe('Guláš');
|
||||
const segedin = await createRecipe('Segedín', { similarIds: [gulas.id] });
|
||||
|
||||
expect((await findRecipe('Guláš')).similarIds).toEqual([segedin.id]);
|
||||
|
||||
await createRecipe('Guláš', { id: gulas.id, similarIds: [] });
|
||||
expect((await findRecipe('Guláš')).similarIds).toBeUndefined();
|
||||
expect((await findRecipe('Segedín')).similarIds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('úprava receptu, který vazbu zná jen z druhé strany, ji zachová', async () => {
|
||||
const gulas = await createRecipe('Guláš');
|
||||
const segedin = await createRecipe('Segedín', { similarIds: [gulas.id] });
|
||||
const knedlik = await createRecipe('Knedlík', { sideDishOfIds: [gulas.id] });
|
||||
|
||||
// Klient posílá zpět, co dostal — vazby uložené u druhého receptu
|
||||
const loaded = await findRecipe('Guláš');
|
||||
await saveMealTemplate(LOGIN, { ...loaded, note: 'Dlouho dusit' });
|
||||
|
||||
expect(await findRecipe('Guláš')).toMatchObject({ similarIds: [segedin.id], sideDishIds: [knedlik.id] });
|
||||
expect(await findRecipe('Segedín')).toMatchObject({ similarIds: [gulas.id] });
|
||||
});
|
||||
|
||||
it('odmítne vazbu na sebe sama a na neexistující recept', async () => {
|
||||
const gulas = await createRecipe('Guláš');
|
||||
|
||||
await expect(createRecipe('Guláš', { id: gulas.id, similarIds: [gulas.id] })).rejects.toThrow(/nebyl nalezen/);
|
||||
await expect(createRecipe('Segedín', { sideDishIds: ['neni'] })).rejects.toThrow(/nebyl nalezen/);
|
||||
});
|
||||
|
||||
it('smazání receptu odebere vazby na něj', async () => {
|
||||
const ryze = await createRecipe('Rýže');
|
||||
await createRecipe('Kuře na kari', { sideDishIds: [ryze.id], similarIds: [ryze.id] });
|
||||
|
||||
await deleteMealTemplate(LOGIN, ryze.id);
|
||||
|
||||
const stored = await getStorage().getData<Record<string, unknown>[]>(`mealTemplates:${LOGIN}`);
|
||||
expect(stored).toHaveLength(1);
|
||||
expect(stored![0]).not.toHaveProperty('sideDishIds');
|
||||
expect(stored![0]).not.toHaveProperty('similarIds');
|
||||
});
|
||||
|
||||
it('přejmenování jídla vazby zachová', async () => {
|
||||
await addMeal(LOGIN, { date: '2025-03-01', mealType: MealType.OBED, name: 'Rýže', source: 'doma' });
|
||||
const ryze = await createRecipe('Rýže');
|
||||
const kure = await createRecipe('Kuře na kari', { sideDishIds: [ryze.id] });
|
||||
|
||||
await renameMealGroup(LOGIN, 'Rýže', 'doma', 'Jasmínová rýže', 'doma');
|
||||
|
||||
expect(await findRecipe('Jasmínová rýže')).toMatchObject({ id: ryze.id, sideDishOfIds: [kure.id] });
|
||||
});
|
||||
|
||||
it('do úložiště se druhá strana vazby nezapisuje', async () => {
|
||||
const gulas = await createRecipe('Guláš');
|
||||
await createRecipe('Segedín', { similarIds: [gulas.id] });
|
||||
|
||||
const stored = await getStorage().getData<Record<string, unknown>[]>(`mealTemplates:${LOGIN}`);
|
||||
expect(stored!.find(item => item.name === 'Guláš')).not.toHaveProperty('similarIds');
|
||||
expect(stored!.find(item => item.name === 'Guláš')).not.toHaveProperty('sideDishOfIds');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user