import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
import { MealieClient } from "../client.js";
// Mock the client
vi.mock("../client.js", () => {
return {
MealieClient: vi.fn().mockImplementation(() => ({
createRecipe: vi.fn(),
getRecipe: vi.fn(),
updateRecipe: vi.fn(),
findOrCreateFood: vi.fn(),
findOrCreateUnit: vi.fn(),
})),
};
});
interface MockClient {
createRecipe: Mock<(name: string) => Promise<unknown>>;
getRecipe: Mock<(slug: string) => Promise<unknown>>;
updateRecipe: Mock<(slug: string, data: unknown) => Promise<unknown>>;
findOrCreateFood: Mock<(name: string) => Promise<{ id: string; name: string }>>;
findOrCreateUnit: Mock<(name: string) => Promise<{ id: string; name: string }>>;
}
describe("Recipe Creation", () => {
let mockClient: MockClient;
beforeEach(() => {
vi.clearAllMocks();
mockClient = {
createRecipe: vi.fn(),
getRecipe: vi.fn(),
updateRecipe: vi.fn(),
findOrCreateFood: vi.fn(),
findOrCreateUnit: vi.fn(),
} as unknown as MockClient;
});
describe("Plain text ingredients", () => {
it("should convert plain text ingredient to note with disableAmount", async () => {
const ingredient = "2 cups flour, sifted";
// Plain text ingredients should become:
// { note: "2 cups flour, sifted", disableAmount: true }
const expected = {
note: ingredient,
disableAmount: true,
};
// Simulate the conversion logic from recipe.ts
const result = typeof ingredient === "string"
? { note: ingredient, disableAmount: true }
: null;
expect(result).toEqual(expected);
});
it("should handle multiple plain text ingredients", async () => {
const ingredients = [
"2 cups flour",
"1 tsp salt",
"3 eggs, beaten",
];
const results = ingredients.map((ing) => ({
note: ing,
disableAmount: true,
}));
expect(results).toHaveLength(3);
expect(results[0].note).toBe("2 cups flour");
expect(results[1].note).toBe("1 tsp salt");
expect(results[2].note).toBe("3 eggs, beaten");
results.forEach((r) => expect(r.disableAmount).toBe(true));
});
});
describe("Structured ingredients", () => {
it("should resolve structured ingredient with food and unit", async () => {
const ingredient = {
quantity: 2,
unit: "cups",
food: "flour",
note: "sifted",
};
mockClient.findOrCreateFood.mockResolvedValue({
id: "food-123",
name: "flour",
});
mockClient.findOrCreateUnit.mockResolvedValue({
id: "unit-456",
name: "cups",
});
// Simulate the resolution logic
const recipeIngredient: Record<string, unknown> = {
quantity: ingredient.quantity ?? 1,
note: ingredient.note ?? "",
disableAmount: false,
};
if (ingredient.food) {
const food = await mockClient.findOrCreateFood(ingredient.food);
recipeIngredient.food = { id: food.id, name: food.name };
recipeIngredient.isFood = true;
}
if (ingredient.unit) {
const unit = await mockClient.findOrCreateUnit(ingredient.unit);
recipeIngredient.unit = { id: unit.id, name: unit.name };
}
expect(recipeIngredient).toEqual({
quantity: 2,
note: "sifted",
disableAmount: false,
food: { id: "food-123", name: "flour" },
isFood: true,
unit: { id: "unit-456", name: "cups" },
});
});
it("should handle structured ingredient without unit", async () => {
const ingredient = {
quantity: 3,
food: "eggs",
note: "beaten",
};
mockClient.findOrCreateFood.mockResolvedValue({
id: "food-789",
name: "eggs",
});
const recipeIngredient: Record<string, unknown> = {
quantity: ingredient.quantity ?? 1,
note: ingredient.note ?? "",
disableAmount: false,
};
if (ingredient.food) {
const food = await mockClient.findOrCreateFood(ingredient.food);
recipeIngredient.food = { id: food.id, name: food.name };
recipeIngredient.isFood = true;
}
expect(recipeIngredient).toEqual({
quantity: 3,
note: "beaten",
disableAmount: false,
food: { id: "food-789", name: "eggs" },
isFood: true,
});
expect(recipeIngredient.unit).toBeUndefined();
});
it("should handle structured ingredient with only quantity and note", async () => {
const ingredient = {
quantity: 1,
note: "pinch of salt",
};
const recipeIngredient: Record<string, unknown> = {
quantity: ingredient.quantity ?? 1,
note: ingredient.note ?? "",
disableAmount: false,
};
expect(recipeIngredient).toEqual({
quantity: 1,
note: "pinch of salt",
disableAmount: false,
});
});
it("should default quantity to 1 when not provided", async () => {
const ingredient = {
food: "butter",
};
mockClient.findOrCreateFood.mockResolvedValue({
id: "food-butter",
name: "butter",
});
const recipeIngredient: Record<string, unknown> = {
quantity: (ingredient as { quantity?: number }).quantity ?? 1,
note: (ingredient as { note?: string }).note ?? "",
disableAmount: false,
};
if (ingredient.food) {
const food = await mockClient.findOrCreateFood(ingredient.food);
recipeIngredient.food = { id: food.id, name: food.name };
recipeIngredient.isFood = true;
}
expect(recipeIngredient.quantity).toBe(1);
});
});
describe("Mixed ingredients", () => {
it("should handle array with both plain text and structured ingredients", async () => {
const ingredients: (string | { quantity?: number; unit?: string; food?: string; note?: string })[] = [
"2 cups flour",
{ quantity: 1, unit: "tsp", food: "salt" },
"3 eggs",
{ quantity: 0.5, unit: "cup", food: "sugar", note: "granulated" },
];
mockClient.findOrCreateFood
.mockResolvedValueOnce({ id: "food-salt", name: "salt" })
.mockResolvedValueOnce({ id: "food-sugar", name: "sugar" });
mockClient.findOrCreateUnit
.mockResolvedValueOnce({ id: "unit-tsp", name: "tsp" })
.mockResolvedValueOnce({ id: "unit-cup", name: "cup" });
const results: Record<string, unknown>[] = [];
for (const ingredient of ingredients) {
if (typeof ingredient === "string") {
results.push({
note: ingredient,
disableAmount: true,
});
} else {
const recipeIngredient: Record<string, unknown> = {
quantity: ingredient.quantity ?? 1,
note: ingredient.note ?? "",
disableAmount: false,
};
if (ingredient.food) {
const food = await mockClient.findOrCreateFood(ingredient.food);
recipeIngredient.food = { id: food.id, name: food.name };
recipeIngredient.isFood = true;
}
if (ingredient.unit) {
const unit = await mockClient.findOrCreateUnit(ingredient.unit);
recipeIngredient.unit = { id: unit.id, name: unit.name };
}
results.push(recipeIngredient);
}
}
expect(results).toHaveLength(4);
// First: plain text
expect(results[0]).toEqual({
note: "2 cups flour",
disableAmount: true,
});
// Second: structured with food and unit
expect(results[1]).toEqual({
quantity: 1,
note: "",
disableAmount: false,
food: { id: "food-salt", name: "salt" },
isFood: true,
unit: { id: "unit-tsp", name: "tsp" },
});
// Third: plain text
expect(results[2]).toEqual({
note: "3 eggs",
disableAmount: true,
});
// Fourth: structured with food, unit, and note
expect(results[3]).toEqual({
quantity: 0.5,
note: "granulated",
disableAmount: false,
food: { id: "food-sugar", name: "sugar" },
isFood: true,
unit: { id: "unit-cup", name: "cup" },
});
});
});
describe("Instructions", () => {
it("should convert instruction strings to instruction objects", () => {
const instructions = [
"Preheat oven to 350°F",
"Mix dry ingredients",
"Add wet ingredients and stir",
];
const recipeInstructions = instructions.map((text) => ({
text,
ingredientReferences: [],
}));
expect(recipeInstructions).toHaveLength(3);
expect(recipeInstructions[0]).toEqual({
text: "Preheat oven to 350°F",
ingredientReferences: [],
});
expect(recipeInstructions[1]).toEqual({
text: "Mix dry ingredients",
ingredientReferences: [],
});
expect(recipeInstructions[2]).toEqual({
text: "Add wet ingredients and stir",
ingredientReferences: [],
});
});
});
});