Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions frontend/src/components/__tests__/Recipe.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<BrowserRouter>
<Recipe recipe={mockRecipe} {...props} />
</BrowserRouter>
);
};

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();
});
});
5 changes: 4 additions & 1 deletion frontend/src/pages/GeminiPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ export default function GeminiPage() {
className="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex gap-4">
<Link to={`/recipes/${recipe.id}`}>
<Link
to={`/recipes/${recipe.id}`}
state={{ recipe: recipe }}
>
<img
src={recipe.image}
alt={recipe.title}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/SignupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ function SignupPage() {
<div className="flex space-x-4">
<button
type="button"
disabled
className="flex w-1/2 items-center justify-center space-x-2
rounded border border-gray-300 py-2 text-sm
bg-gray-200 cursor-not-allowed"
Expand All @@ -120,6 +121,7 @@ function SignupPage() {
</button>
<button
type="button"
disabled
className="flex w-1/2 items-center justify-center space-x-2
rounded border border-gray-300 py-2 text-sm
bg-gray-200 cursor-not-allowed"
Expand Down
184 changes: 184 additions & 0 deletions frontend/src/pages/__tests__/FavoritesPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import FavoritesPage from "../favoritePage";

// 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("FavoritesPage", () => {
const mockRecipes = [
{
id: 1,
title: "Recipe 1",
image: "image1.jpg",
readyInMinutes: 30,
servings: 4,
vegan: true,
vegetarian: true,
glutenFree: false,
dairyFree: true,
},
{
id: 2,
title: "Recipe 2",
image: "image2.jpg",
readyInMinutes: 45,
servings: 6,
vegan: false,
vegetarian: true,
glutenFree: true,
dairyFree: false,
},
];

beforeEach(() => {
vi.clearAllMocks();
mockLocalStorage.getItem.mockReturnValue(
JSON.stringify({
user: { id: "test-user-id" },
})
);
});

const renderFavoritesPage = () => {
return render(
<BrowserRouter>
<FavoritesPage />
</BrowserRouter>
);
};

it("renders loading state initially", () => {
renderFavoritesPage();
expect(screen.getByText("Loading favorites…")).toBeInTheDocument();
});

it("shows error when user is not logged in", async () => {
mockLocalStorage.getItem.mockReturnValue(null);
renderFavoritesPage();

await waitFor(() => {
expect(
screen.getByText(/Please sign in to view favorites/i)
).toBeInTheDocument();
});
});

it("displays favorite recipes when loaded successfully", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ recipes: mockRecipes }),
});

renderFavoritesPage();

await waitFor(() => {
expect(screen.getByText("Recipe 1")).toBeInTheDocument();
expect(screen.getByText("Recipe 2")).toBeInTheDocument();
});
});

it("shows empty state when no favorites exist", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ recipes: [] }),
});

renderFavoritesPage();

await waitFor(() => {
expect(
screen.getByText(/You haven't favorited any recipes yet/i)
).toBeInTheDocument();
});
});

it("handles filter toggle", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ recipes: mockRecipes }),
});

renderFavoritesPage();

await waitFor(() => {
expect(screen.getByText("Recipe 1")).toBeInTheDocument();
});

const filterButton = screen.getByText("Show Filters");
fireEvent.click(filterButton);

expect(screen.getByText("Hide Filters")).toBeInTheDocument();
expect(screen.getByText("Dietary Preferences")).toBeInTheDocument();
});

it("filters recipes based on dietary preferences", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ recipes: mockRecipes }),
});

renderFavoritesPage();

await waitFor(() => {
expect(screen.getByText("Recipe 1")).toBeInTheDocument();
});

// Open filters
fireEvent.click(screen.getByText("Show Filters"));

// Enable vegan filter
const veganCheckbox = screen.getByLabelText("vegan");
fireEvent.click(veganCheckbox);

// Should only show Recipe 1 (vegan)
expect(screen.getByText("Recipe 1")).toBeInTheDocument();
expect(screen.queryByText("Recipe 2")).not.toBeInTheDocument();
});

it("resets filters when clear filters is clicked", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ recipes: mockRecipes }),
});

renderFavoritesPage();

await waitFor(() => {
expect(screen.getByText("Recipe 1")).toBeInTheDocument();
});

// Open filters
fireEvent.click(screen.getByText("Show Filters"));

// Enable vegan filter
const veganCheckbox = screen.getByLabelText("vegan");
fireEvent.click(veganCheckbox);

// Clear filters
fireEvent.click(screen.getByText("Clear Filters"));

// Should show all recipes again
expect(screen.getByText("Recipe 1")).toBeInTheDocument();
expect(screen.getByText("Recipe 2")).toBeInTheDocument();
});

it("handles API error gracefully", async () => {
mockFetch.mockRejectedValueOnce(new Error("API Error"));
renderFavoritesPage();

await waitFor(() => {
expect(screen.getByText(/Error: API Error/i)).toBeInTheDocument();
});
});
});
Loading
Loading