Cleanup after initial plan

This commit is contained in:
Aerilyn Weber 2026-05-19 10:13:40 +09:00
parent d2a7e652b3
commit 245520fb50
53 changed files with 6733 additions and 621 deletions

View file

@ -106,5 +106,129 @@ describe(ShoppingGapService.name, () => {
const result = await service.calculateGap('hh1', 'plan2');
expect(result.missingItems.length).toBe(0);
});
it('aggregates duplicate ingredients and sorts by product name', async () => {
mockMealRepo.findById.mockResolvedValue({
_id: 'plan-multi',
days: [
{
meals: [
{ recipeId: 'recipeA', servings: 1 },
{ recipeId: 'recipeB', servings: 1 },
]
}
]
});
mockRecipesRepo.findById.mockImplementation(async (id) => {
if (id === 'recipeA') {
return {
_id: 'recipeA', servings: 1,
ingredients: [{ productId: 'prod1', quantity: 10, isOptional: false }]
};
}
return {
_id: 'recipeB', servings: 1,
ingredients: [
{ productId: 'prod1', quantity: 20, isOptional: false },
{ productId: 'prod2', quantity: 5, isOptional: false },
]
};
});
mockProductsRepo.findByIds.mockResolvedValue([
{ _id: 'prod1', name: 'Banana' },
{ _id: 'prod2', name: 'Apple' },
]);
mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);
const result = await service.calculateGap('hh1', 'plan-multi');
expect(result.missingItems).toHaveLength(2);
expect(result.missingItems[0].productName).toBe('Apple');
expect(result.missingItems[1].productName).toBe('Banana');
expect(result.missingItems[1].requiredQuantity).toBe(30);
});
it('covers fallback paths for missing list, recipe properties and pantry quantities', async () => {
mockMealRepo.findById.mockResolvedValue({
_id: 'plan-empty',
});
mockProductsRepo.findByIds.mockResolvedValue([]);
mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);
let res = await service.calculateGap('hh1', 'plan-empty');
expect(res.missingItems).toHaveLength(0);
mockMealRepo.findById.mockResolvedValue({
_id: 'plan-missing',
days: [
{
meals: [{ recipeId: 'recipeC', servings: 1 }]
}
]
});
mockRecipesRepo.findById.mockResolvedValue({
_id: 'recipeC',
servings: 1,
ingredients: [
{ productId: 'prod3', quantity: 10, isOptional: false }
]
});
mockProductsRepo.findByIds.mockResolvedValue([
{ _id: 'prod3' }
]);
mockPantryRepo.findActiveByHousehold.mockResolvedValue([
{ productId: 'prod3' }
]);
res = await service.calculateGap('hh1', 'plan-missing');
expect(res.missingItems).toHaveLength(1);
const itm = res.missingItems[0]!;
expect(itm.unit).toBe('g');
expect(itm.productName).toBe('Unknown Ingredient');
expect(itm.category).toBe('other');
expect(itm.pantryQuantity).toBe(0);
});
it('skips optional ingredients, handles missing recipes and defaults servings to 1', async () => {
mockMealRepo.findById.mockResolvedValue({
_id: 'plan-edge',
days: [
{
meals: [
{ recipeId: 'recipeExist', servings: 2 },
{ recipeId: 'recipeNotExist', servings: 1 },
]
}
]
});
mockRecipesRepo.findById.mockImplementation(async (id) => {
if (id === 'recipeExist') {
return {
_id: 'recipeExist',
servings: 0,
ingredients: [
{ productId: 'prodIng', quantity: 5, isOptional: false },
{ productId: 'prodOptional', quantity: 10, isOptional: true },
]
};
}
return null;
});
mockProductsRepo.findByIds.mockResolvedValue([{ _id: 'prodIng', name: 'Ingredient' }]);
mockPantryRepo.findActiveByHousehold.mockResolvedValue([]);
const result = await service.calculateGap('hh1', 'plan-edge');
expect(result.missingItems).toHaveLength(1);
expect(result.missingItems[0].productId).toBe('prodIng');
expect(result.missingItems[0].requiredQuantity).toBe(10);
});
});
});