diff --git a/frontend/src/components/__tests__/Recipe.test.tsx b/frontend/src/components/__tests__/Recipe.test.tsx
new file mode 100644
index 0000000..797653f
--- /dev/null
+++ b/frontend/src/components/__tests__/Recipe.test.tsx
@@ -0,0 +1,143 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { BrowserRouter } from "react-router-dom";
+import Recipe from "../Recipe";
+
+// Mock fetch
+const mockFetch = vi.fn();
+global.fetch = mockFetch;
+
+// Mock localStorage
+const mockLocalStorage = {
+ getItem: vi.fn(),
+ setItem: vi.fn(),
+ clear: vi.fn(),
+};
+Object.defineProperty(window, "localStorage", { value: mockLocalStorage });
+
+describe("Recipe", () => {
+ const mockRecipe = {
+ id: 1,
+ title: "Test Recipe",
+ image: "test-image.jpg",
+ readyInMinutes: 30,
+ servings: 4,
+ vegan: true,
+ vegetarian: true,
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockLocalStorage.getItem.mockReturnValue(
+ JSON.stringify({
+ user: { id: "test-user-id" },
+ })
+ );
+ });
+
+ const renderRecipe = (props = {}) => {
+ return render(
+
+
+
+ );
+ };
+
+ it("renders recipe card with all elements", () => {
+ renderRecipe();
+
+ expect(screen.getByText("Test Recipe")).toBeInTheDocument();
+ expect(screen.getByText("30 mins")).toBeInTheDocument();
+ expect(screen.getByText("Servings: 4")).toBeInTheDocument();
+ expect(screen.getByText("Vegan")).toBeInTheDocument();
+ expect(screen.getByText("Vegetarian")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "View Recipe" })
+ ).toBeInTheDocument();
+ });
+
+ it("renders recipe image correctly", () => {
+ renderRecipe();
+
+ const image = screen.getByAltText("Test Recipe");
+ expect(image).toHaveAttribute("src", "test-image.jpg");
+ });
+
+ it("shows fallback when no image is provided", () => {
+ renderRecipe({
+ recipe: { ...mockRecipe, image: "" },
+ });
+
+ expect(screen.getByText("No image available")).toBeInTheDocument();
+ });
+
+ it("toggles favorite state when clicking favorite button", async () => {
+ mockFetch.mockResolvedValueOnce({ ok: true });
+ renderRecipe();
+
+ const favoriteButton = screen.getByRole("button", {
+ name: "Add to favorites",
+ });
+ fireEvent.click(favoriteButton);
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.stringContaining("/user/favorite"),
+ expect.objectContaining({
+ method: "POST",
+ headers: expect.objectContaining({
+ "Content-Type": "application/json",
+ ID: "test-user-id",
+ }),
+ body: JSON.stringify(mockRecipe),
+ })
+ );
+ });
+
+ it("shows alert when trying to favorite without being logged in", async () => {
+ mockLocalStorage.getItem.mockReturnValue(null);
+ const mockAlert = vi.spyOn(window, "alert").mockImplementation(() => {});
+
+ renderRecipe();
+
+ const favoriteButton = screen.getByRole("button", {
+ name: "Add to favorites",
+ });
+ fireEvent.click(favoriteButton);
+
+ expect(mockAlert).toHaveBeenCalledWith(
+ "Please sign in to use this feature."
+ );
+ expect(mockFetch).not.toHaveBeenCalled();
+
+ mockAlert.mockRestore();
+ });
+
+ it("handles favorite API error gracefully", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ text: () => Promise.resolve("API Error"),
+ });
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
+
+ renderRecipe();
+
+ const favoriteButton = screen.getByRole("button", {
+ name: "Add to favorites",
+ });
+ await fireEvent.click(favoriteButton);
+
+ expect(consoleError).toHaveBeenCalled();
+
+ consoleError.mockRestore();
+ });
+
+ it("renders with initial favorite state", () => {
+ renderRecipe({ initialFavorited: true });
+
+ expect(
+ screen.getByRole("button", { name: "Remove from favorites" })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/pages/GeminiPage.tsx b/frontend/src/pages/GeminiPage.tsx
index 17dc4f4..e5777f4 100644
--- a/frontend/src/pages/GeminiPage.tsx
+++ b/frontend/src/pages/GeminiPage.tsx
@@ -138,7 +138,10 @@ export default function GeminiPage() {
className="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
-
+