diff --git a/src/lib/utils.tests.ts b/src/lib/utils.tests.ts index c420c04..827733f 100644 --- a/src/lib/utils.tests.ts +++ b/src/lib/utils.tests.ts @@ -1,6 +1,10 @@ +import { vi } from "vitest"; + +import type { Category } from "@/models/category.model"; +import type { Product } from "@/models/product.model"; import type { User } from "@/models/user.model"; + import type { Session } from "react-router"; -import { vi } from "vitest"; type TestRequestConfig = { url?: string; @@ -40,3 +44,32 @@ export const createMockSession = (userId: number | null): Session => ({ flash: vi.fn(), unset: vi.fn(), }); + +export const createTestProduct = (overrides?: Partial): Product => ({ + id: 1, + title: "Test Product", + imgSrc: "/test-image.jpg", + alt: "Test alt text", + price: 100, + description: "Test description", + categoryId: 1, + isOnSale: false, + features: ["Feature 1", "Feature 2"], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, +}); + +export const createTestCategory = ( + overrides?: Partial +): Category => ({ + id: 1, + title: "Polos", + slug: "polos", + imgSrc: "/images/polos.jpg", + alt: "Colección de polos para programadores", + description: "Explora nuestra colección de polos para programadores", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, +}); diff --git a/src/routes/not-found/index.tsx b/src/routes/not-found/index.tsx index 5148f87..e596126 100644 --- a/src/routes/not-found/index.tsx +++ b/src/routes/not-found/index.tsx @@ -5,14 +5,16 @@ import { Button, Container, Section } from "@/components/ui"; export default function NotFound() { return ( -
-
-

404

-

Página no encontrada

-

+

+
+

404

+

+ Página no encontrada +

+

No pudimos encontrar la página que estás buscando.

-
diff --git a/src/routes/product/product.loader.test.ts b/src/routes/product/product.loader.test.ts index 3cfbebf..794b99b 100644 --- a/src/routes/product/product.loader.test.ts +++ b/src/routes/product/product.loader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import type { Product } from "@/models/product.model"; +import { createTestProduct } from "@/lib/utils.tests"; import * as productService from "@/services/product.service"; import { loader } from "."; @@ -20,19 +20,7 @@ describe("Product loader", () => { }); it("returns a product when it exists", async () => { - const mockProduct: Product = { - id: 1, - title: "Test Product", - price: 99.99, - description: "Test description", - imgSrc: "/test-image.jpg", - features: ["Feature 1", "Feature 2"], - alt: "", - categoryId: 1, - isOnSale: false, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; + const mockProduct = createTestProduct(); mockGetProductById.mockResolvedValue(mockProduct); diff --git a/src/routes/product/product.test.tsx b/src/routes/product/product.test.tsx index a0188ee..3d0bbee 100644 --- a/src/routes/product/product.test.tsx +++ b/src/routes/product/product.test.tsx @@ -1,59 +1,162 @@ -import { describe, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { useNavigation } from "react-router"; +import { describe, expect, it, vi } from "vitest"; + +import { createTestProduct } from "@/lib/utils.tests"; +import type { Product as ProductType } from "@/models/product.model"; + +import Product from "."; + +import type { Route } from "./+types"; + +// Helper function to create a test navigation object +const createTestNavigation = (overrides = {}) => ({ + state: "idle", + location: undefined, + formMethod: undefined, + formAction: undefined, + formEncType: undefined, + formData: undefined, + json: undefined, + text: undefined, + ...overrides, +}); + +// Mock de react-router +vi.mock("react-router", () => ({ + Form: vi.fn(({ children }) =>
{children}
), + useNavigation: vi.fn(() => createTestNavigation()), + Link: vi.fn(({ children, ...props }) => {children}), +})); + +const createTestProps = ( + productData: Partial = {} +): Route.ComponentProps => ({ + loaderData: { product: createTestProduct(productData) }, + params: vi.fn() as any, + matches: vi.fn() as any, +}); describe("Product Component", () => { describe("Rendering with valid product data", () => { - // Crear un mock de producto de prueba con todos los campos necesarios - // const mockProduct: Product = { id: 1, title: "Test Product", price: 99.99, ... } - it("should render product title correctly", () => { - // 1. Renderizar el componente Product con mockProduct usando render() - // 2. Buscar el elemento h1 que contiene el título del producto - // 3. Verificar que el texto coincida con mockProduct.title usando expect().toHaveTextContent() + // Step 1: Setup - Create test props + const props = createTestProps({ title: "Awesome Product" }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check title is rendered correctly + const titleElement = screen.getByRole("heading", { level: 1 }); + expect(titleElement).toHaveTextContent("Awesome Product"); }); it("should render product price with dollar sign", () => { - // 1. Renderizar el componente Product con mockProduct - // 2. Buscar el elemento que muestra el precio (probablemente un

con el precio) - // 3. Verificar que el texto incluya el símbolo $ y el precio correcto - // Tip: usar toHaveTextContent() con el formato "$99.99" + // Step 1: Setup - Create test props + const props = createTestProps({ price: 150.99 }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check price is rendered correctly + expect(screen.queryByText("$150.99")).toBeInTheDocument(); }); it("should render product description", () => { - // 1. Renderizar el componente Product con mockProduct - // 2. Buscar el párrafo que contiene la descripción del producto - // 3. Verificar que el texto coincida con mockProduct.description - // Nota: considerar el caso donde description puede ser null + // Step 1: Setup - Create test props + const props = createTestProps({ + description: "Amazing product", + }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check description is rendered + expect(screen.queryByText("Amazing product")).toBeInTheDocument(); }); it("should render product image with correct src and alt attributes", () => { - // 1. Renderizar el componente Product con mockProduct - // 2. Buscar la imagen usando getByRole('img') o getByAltText() - // 3. Verificar que el atributo src coincida con mockProduct.imgSrc usando toHaveAttribute() - // 4. Verificar que el atributo alt coincida con mockProduct.title + // Step 1: Setup - Create test props + const props = createTestProps({ + imgSrc: "/test-image.jpg", + alt: "Test Product", + }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check image attributes + const image = screen.getByRole("img"); + expect(image).toHaveAttribute("src", "/test-image.jpg"); + expect(image).toHaveAttribute("alt", "Test Product"); }); it("should render all product features as list items", () => { - // 1. Renderizar el componente Product con mockProduct que tenga un array de features - // 2. Buscar todos los elementos

  • dentro de la lista de características - // 3. Verificar que el número de elementos li coincida con mockProduct.features.length - // 4. Verificar que cada feature aparezca en el DOM usando getAllByText() o similar + // Step 1: Setup - Create test props + const features = ["Feature 1", "Feature 2", "Feature 3"]; + const props = createTestProps({ features }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check features are rendered + features.forEach((feature) => { + expect(screen.queryByText(feature)).toBeInTheDocument(); + }); }); it('should render "Agregar al Carrito" button', () => { - // 1. Renderizar el componente Product con mockProduct - // 2. Buscar el botón usando getByRole('button') o getByText('Agregar al Carrito') - // 3. Verificar que el botón esté presente en el documento usando toBeInTheDocument() - // 4. Verificar que el botón tenga el valor correcto (productId) usando toHaveAttribute() + // Step 1: Setup - Create test props + const props = createTestProps(); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call - Render component + render(); + // Step 4: Verify - Check button is present + expect( + screen.queryByRole("button", { name: "Agregar al Carrito" }) + ).toBeInTheDocument(); }); }); describe("Form interactions", () => { - it("should include hidden redirectTo input with correct value"); - it("should include productId as button value"); - it("should disable button when cart is loading"); + it("should include hidden redirectTo input with correct value", () => { + // Step 1: Setup + const productId = 123; + const props = createTestProps({ id: productId }); + // Step 2: Mock - Component mocks already set up above + // Step 3: Call + render(); + // Step 4: Verify + const redirectInput = screen.queryByDisplayValue( + `/products/${productId}` + ); + expect(redirectInput).toBeInTheDocument(); + }); + + it("should disable button when cart is loading", () => { + // Step 1: Setup + const props = createTestProps(); + const expectedNavigation = createTestNavigation({ state: "submitting" }); + // Step 2: Mock - Override navigation state to simulate loading + vi.mocked(useNavigation).mockReturnValue(expectedNavigation as any); + // Step 3: Call + render(); + // Step 4: Verify + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + expect(button).toHaveTextContent("Agregando..."); + }); }); describe("Error handling", () => { - it("should render NotFound component when product is not provided"); + it("should render NotFound component when product is not provided", () => { + // Step 1: Setup - Create props without product + const props = createTestProps(); + props.loaderData.product = undefined; + + // Step 2: Mock - Mock NotFound component + // vi.mock("../not-found", () => ({ + // default: () =>
    Not Found Page
    , + // })); + // Step 3: Call + render(); + // Step 4: Verify + expect(screen.getByTestId("not-found")).toBeInTheDocument(); + }); }); }); diff --git a/src/services/product.service.test.ts b/src/services/product.service.test.ts index 57cd8ba..ea57513 100644 --- a/src/services/product.service.test.ts +++ b/src/services/product.service.test.ts @@ -1,11 +1,12 @@ -import { describe, it, vi, beforeEach, expect } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestCategory, createTestProduct } from "@/lib/utils.tests"; import type { Category } from "@/models/category.model"; import type { Product } from "@/models/product.model"; import * as productRepository from "@/repositories/product.repository"; import { getCategoryBySlug } from "./category.service"; -import { getProductsByCategorySlug } from "./product.service"; +import { getProductById, getProductsByCategorySlug } from "./product.service"; // Mock dependencies vi.mock("@/repositories/product.repository"); @@ -18,69 +19,85 @@ describe("Product Service", () => { describe("getProductsByCategorySlug", () => { it("should return products for a valid category slug", async () => { - // 1. Mock getCategoryBySlug to return a valid category with id - const mockCategory = { - id: 1, - slug: "polos", - } as Category; - - vi.mocked(getCategoryBySlug).mockResolvedValue(mockCategory); - - // 2. Mock productRepository.getProductsByCategory to return array of products - const mockedProducts = [ - { id: 1, title: "Product 1", price: 10, categoryId: 1 } as Product, - { id: 2, title: "Product 2", price: 20, categoryId: 1 } as Product, + // Step 1: Setup - Create test data with valid category and products + const testCategory = createTestCategory(); + const mockedProducts: Product[] = [ + createTestProduct({ id: 1, categoryId: testCategory.id }), + createTestProduct({ + id: 2, + title: "Test Product 2", + categoryId: testCategory.id, + }), ]; + // Step 2: Mock - Configure repository responses + vi.mocked(getCategoryBySlug).mockResolvedValue(testCategory); vi.mocked(productRepository.getProductsByCategory).mockResolvedValue( mockedProducts ); - // 3. Call getProductsByCategorySlug with valid slug (e.g., "polos") - const products = await getProductsByCategorySlug(mockCategory.slug); - // 4. Assert that getCategoryBySlug was called with correct slug - expect(getCategoryBySlug).toHaveBeenCalledWith(mockCategory.slug); - // 5. Assert that getProductsByCategory was called with category.id + // Step 3: Call service function + const products = await getProductsByCategorySlug(testCategory.slug); + + // Step 4: Verify expected behavior + expect(getCategoryBySlug).toHaveBeenCalledWith(testCategory.slug); expect(productRepository.getProductsByCategory).toHaveBeenCalledWith( - mockCategory.id + testCategory.id ); - // 6. Assert that returned products match the mocked products array expect(products).toEqual(mockedProducts); }); it("should throw error when category slug does not exist", async () => { - // 1. Mock getCategoryBySlug to throw "Category not found" error - // 2. Call getProductsByCategorySlug with invalid slug - // 3. Assert that the function throws the expected error - // 4. Assert that getProductsByCategory was NOT called - }); + // Step 1: Setup - Create test data for non-existent category + const invalidSlug = "invalid-slug"; - it("should handle repository errors gracefully", async () => { - // 1. Mock getCategoryBySlug to return valid category - // 2. Mock productRepository.getProductsByCategory to throw database error - // 3. Call getProductsByCategorySlug with valid slug - // 4. Assert that the function throws/propagates the repository error + // Step 2: Mock - Configure error response + const errorMessage = `Category with slug "${invalidSlug}" not found`; + vi.mocked(getCategoryBySlug).mockRejectedValue(new Error(errorMessage)); + + // Step 3: Call service function + const getProducts = getProductsByCategorySlug( + invalidSlug as Category["slug"] + ); + + // Step 4: Verify expected behavior + await expect(getProducts).rejects.toThrow(errorMessage); + expect(productRepository.getProductsByCategory).not.toHaveBeenCalled(); }); }); describe("getProductById", () => { - it("should return product for valid existing ID", async () => { - // 1. Mock productRepository.getProductById to return a product object - // 2. Call getProductById with valid ID (e.g., 1) - // 3. Assert that getProductById was called with correct ID - // 4. Assert that returned product matches the mocked product - }); + it("should return product for valid ID", async () => { + // Step 1: Setup - Create test data for existing product + const testProduct = createTestProduct(); - it("should throw error when product ID does not exist", async () => { - // 1. Mock productRepository.getProductById to return null - // 2. Call getProductById with non-existent ID - // 3. Assert that function throws "Product not found" error + // Step 2: Mock - Configure repository response + vi.mocked(productRepository.getProductById).mockResolvedValue( + testProduct + ); + + // Step 3: Call service function + const result = await getProductById(testProduct.id); + + // Step 4: Verify expected behavior + expect(productRepository.getProductById).toHaveBeenCalledWith( + testProduct.id + ); + expect(result).toEqual(testProduct); }); - it("should handle repository errors gracefully", async () => { - // 1. Mock productRepository.getProductById to throw database error - // 2. Call getProductById with any ID - // 3. Assert that the function throws/propagates the repository error + it("should throw error when product does not exist", async () => { + // Step 1: Setup - Configure ID for non-existent product + const nonExistentId = 999; + + // Step 2: Mock - Configure null response from repository + vi.mocked(productRepository.getProductById).mockResolvedValue(null); + + // Step 3: Call service function + const productPromise = getProductById(nonExistentId); + + // Step 4: Verify expected behavior + await expect(productPromise).rejects.toThrow("Product not found"); }); }); });