Skip to content
Merged
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
35 changes: 34 additions & 1 deletion src/lib/utils.tests.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -40,3 +44,32 @@ export const createMockSession = (userId: number | null): Session => ({
flash: vi.fn(),
unset: vi.fn(),
});

export const createTestProduct = (overrides?: Partial<Product>): 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>
): 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,
});
14 changes: 8 additions & 6 deletions src/routes/not-found/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { Button, Container, Section } from "@/components/ui";
export default function NotFound() {
return (
<Container>
<Section className='flex justify-center items-center'>
<div className='text-center'>
<p className='text-base font-semibold text-accent-foreground'>404</p>
<h1 className='text-4xl leading-9 font-bold tracking-tight text-foreground mt-4 sm:text-6xl sm:leading-none'>Página no encontrada</h1>
<p className='text-lg font-medium text-muted-foreground mt-6 sm:text-xl leading-none'>
<Section className="flex justify-center items-center">
<div className="text-center" data-testid="not-found">
<p className="text-base font-semibold text-accent-foreground">404</p>
<h1 className="text-4xl leading-9 font-bold tracking-tight text-foreground mt-4 sm:text-6xl sm:leading-none">
Página no encontrada
</h1>
<p className="text-lg font-medium text-muted-foreground mt-6 sm:text-xl leading-none">
No pudimos encontrar la página que estás buscando.
</p>
<Button className='mt-10' asChild size="xl">
<Button className="mt-10" asChild size="xl">
<Link to="/">Regresar al inicio</Link>
</Button>
</div>
Expand Down
16 changes: 2 additions & 14 deletions src/routes/product/product.loader.test.ts
Original file line number Diff line number Diff line change
@@ -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 ".";
Expand All @@ -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);

Expand Down
165 changes: 134 additions & 31 deletions src/routes/product/product.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => <form>{children}</form>),
useNavigation: vi.fn(() => createTestNavigation()),
Link: vi.fn(({ children, ...props }) => <a {...props}>{children}</a>),
}));

const createTestProps = (
productData: Partial<ProductType> = {}
): 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(<Product {...props} />);
// 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 <p> 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(<Product {...props} />);
// 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(<Product {...props} />);
// 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(<Product {...props} />);
// 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 <li> 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(<Product {...props} />);
// 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(<Product {...props} />);
// 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(<Product {...props} />);
// 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(<Product {...props} />);
// 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: () => <div data-testid="not-found">Not Found Page</div>,
// }));
// Step 3: Call
render(<Product {...props} />);
// Step 4: Verify
expect(screen.getByTestId("not-found")).toBeInTheDocument();
});
});
});
Loading