Skip to content

Commit 881f199

Browse files
authored
Merge pull request #189 from codeableorg/product-route-module-test
Product-route-module-test
2 parents 567246c + 4747348 commit 881f199

File tree

4 files changed

+151
-61
lines changed

4 files changed

+151
-61
lines changed

src/routes/not-found/index.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import { Button, Container, Section } from "@/components/ui";
55
export default function NotFound() {
66
return (
77
<Container>
8-
<Section className='flex justify-center items-center'>
9-
<div className='text-center'>
10-
<p className='text-base font-semibold text-accent-foreground'>404</p>
11-
<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>
12-
<p className='text-lg font-medium text-muted-foreground mt-6 sm:text-xl leading-none'>
8+
<Section className="flex justify-center items-center">
9+
<div className="text-center" data-testid="not-found">
10+
<p className="text-base font-semibold text-accent-foreground">404</p>
11+
<h1 className="text-4xl leading-9 font-bold tracking-tight text-foreground mt-4 sm:text-6xl sm:leading-none">
12+
Página no encontrada
13+
</h1>
14+
<p className="text-lg font-medium text-muted-foreground mt-6 sm:text-xl leading-none">
1315
No pudimos encontrar la página que estás buscando.
1416
</p>
15-
<Button className='mt-10' asChild size="xl">
17+
<Button className="mt-10" asChild size="xl">
1618
<Link to="/">Regresar al inicio</Link>
1719
</Button>
1820
</div>

src/routes/product/product.loader.test.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, vi } from "vitest";
22

3-
import type { Product } from "@/models/product.model";
3+
import { createTestProduct } from "@/lib/utils.tests";
44
import * as productService from "@/services/product.service";
55

66
import { loader } from ".";
@@ -20,19 +20,7 @@ describe("Product loader", () => {
2020
});
2121

2222
it("returns a product when it exists", async () => {
23-
const mockProduct: Product = {
24-
id: 1,
25-
title: "Test Product",
26-
price: 99.99,
27-
description: "Test description",
28-
imgSrc: "/test-image.jpg",
29-
features: ["Feature 1", "Feature 2"],
30-
alt: "",
31-
categoryId: 1,
32-
isOnSale: false,
33-
createdAt: new Date().toISOString(),
34-
updatedAt: new Date().toISOString(),
35-
};
23+
const mockProduct = createTestProduct();
3624

3725
mockGetProductById.mockResolvedValue(mockProduct);
3826

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,162 @@
1-
import { describe, it } from "vitest";
1+
import { render, screen } from "@testing-library/react";
2+
import { useNavigation } from "react-router";
3+
import { describe, expect, it, vi } from "vitest";
4+
5+
import { createTestProduct } from "@/lib/utils.tests";
6+
import type { Product as ProductType } from "@/models/product.model";
7+
8+
import Product from ".";
9+
10+
import type { Route } from "./+types";
11+
12+
// Helper function to create a test navigation object
13+
const createTestNavigation = (overrides = {}) => ({
14+
state: "idle",
15+
location: undefined,
16+
formMethod: undefined,
17+
formAction: undefined,
18+
formEncType: undefined,
19+
formData: undefined,
20+
json: undefined,
21+
text: undefined,
22+
...overrides,
23+
});
24+
25+
// Mock de react-router
26+
vi.mock("react-router", () => ({
27+
Form: vi.fn(({ children }) => <form>{children}</form>),
28+
useNavigation: vi.fn(() => createTestNavigation()),
29+
Link: vi.fn(({ children, ...props }) => <a {...props}>{children}</a>),
30+
}));
31+
32+
const createTestProps = (
33+
productData: Partial<ProductType> = {}
34+
): Route.ComponentProps => ({
35+
loaderData: { product: createTestProduct(productData) },
36+
params: vi.fn() as any,
37+
matches: vi.fn() as any,
38+
});
239

340
describe("Product Component", () => {
441
describe("Rendering with valid product data", () => {
5-
// Crear un mock de producto de prueba con todos los campos necesarios
6-
// const mockProduct: Product = { id: 1, title: "Test Product", price: 99.99, ... }
7-
842
it("should render product title correctly", () => {
9-
// 1. Renderizar el componente Product con mockProduct usando render()
10-
// 2. Buscar el elemento h1 que contiene el título del producto
11-
// 3. Verificar que el texto coincida con mockProduct.title usando expect().toHaveTextContent()
43+
// Step 1: Setup - Create test props
44+
const props = createTestProps({ title: "Awesome Product" });
45+
// Step 2: Mock - Component mocks already set up above
46+
// Step 3: Call - Render component
47+
render(<Product {...props} />);
48+
// Step 4: Verify - Check title is rendered correctly
49+
const titleElement = screen.getByRole("heading", { level: 1 });
50+
expect(titleElement).toHaveTextContent("Awesome Product");
1251
});
1352

1453
it("should render product price with dollar sign", () => {
15-
// 1. Renderizar el componente Product con mockProduct
16-
// 2. Buscar el elemento que muestra el precio (probablemente un <p> con el precio)
17-
// 3. Verificar que el texto incluya el símbolo $ y el precio correcto
18-
// Tip: usar toHaveTextContent() con el formato "$99.99"
54+
// Step 1: Setup - Create test props
55+
const props = createTestProps({ price: 150.99 });
56+
// Step 2: Mock - Component mocks already set up above
57+
// Step 3: Call - Render component
58+
render(<Product {...props} />);
59+
// Step 4: Verify - Check price is rendered correctly
60+
expect(screen.queryByText("$150.99")).toBeInTheDocument();
1961
});
2062

2163
it("should render product description", () => {
22-
// 1. Renderizar el componente Product con mockProduct
23-
// 2. Buscar el párrafo que contiene la descripción del producto
24-
// 3. Verificar que el texto coincida con mockProduct.description
25-
// Nota: considerar el caso donde description puede ser null
64+
// Step 1: Setup - Create test props
65+
const props = createTestProps({
66+
description: "Amazing product",
67+
});
68+
// Step 2: Mock - Component mocks already set up above
69+
// Step 3: Call - Render component
70+
render(<Product {...props} />);
71+
// Step 4: Verify - Check description is rendered
72+
expect(screen.queryByText("Amazing product")).toBeInTheDocument();
2673
});
2774

2875
it("should render product image with correct src and alt attributes", () => {
29-
// 1. Renderizar el componente Product con mockProduct
30-
// 2. Buscar la imagen usando getByRole('img') o getByAltText()
31-
// 3. Verificar que el atributo src coincida con mockProduct.imgSrc usando toHaveAttribute()
32-
// 4. Verificar que el atributo alt coincida con mockProduct.title
76+
// Step 1: Setup - Create test props
77+
const props = createTestProps({
78+
imgSrc: "/test-image.jpg",
79+
alt: "Test Product",
80+
});
81+
// Step 2: Mock - Component mocks already set up above
82+
// Step 3: Call - Render component
83+
render(<Product {...props} />);
84+
// Step 4: Verify - Check image attributes
85+
const image = screen.getByRole("img");
86+
expect(image).toHaveAttribute("src", "/test-image.jpg");
87+
expect(image).toHaveAttribute("alt", "Test Product");
3388
});
3489

3590
it("should render all product features as list items", () => {
36-
// 1. Renderizar el componente Product con mockProduct que tenga un array de features
37-
// 2. Buscar todos los elementos <li> dentro de la lista de características
38-
// 3. Verificar que el número de elementos li coincida con mockProduct.features.length
39-
// 4. Verificar que cada feature aparezca en el DOM usando getAllByText() o similar
91+
// Step 1: Setup - Create test props
92+
const features = ["Feature 1", "Feature 2", "Feature 3"];
93+
const props = createTestProps({ features });
94+
// Step 2: Mock - Component mocks already set up above
95+
// Step 3: Call - Render component
96+
render(<Product {...props} />);
97+
// Step 4: Verify - Check features are rendered
98+
features.forEach((feature) => {
99+
expect(screen.queryByText(feature)).toBeInTheDocument();
100+
});
40101
});
41102

42103
it('should render "Agregar al Carrito" button', () => {
43-
// 1. Renderizar el componente Product con mockProduct
44-
// 2. Buscar el botón usando getByRole('button') o getByText('Agregar al Carrito')
45-
// 3. Verificar que el botón esté presente en el documento usando toBeInTheDocument()
46-
// 4. Verificar que el botón tenga el valor correcto (productId) usando toHaveAttribute()
104+
// Step 1: Setup - Create test props
105+
const props = createTestProps();
106+
// Step 2: Mock - Component mocks already set up above
107+
// Step 3: Call - Render component
108+
render(<Product {...props} />);
109+
// Step 4: Verify - Check button is present
110+
expect(
111+
screen.queryByRole("button", { name: "Agregar al Carrito" })
112+
).toBeInTheDocument();
47113
});
48114
});
49115

50116
describe("Form interactions", () => {
51-
it("should include hidden redirectTo input with correct value");
52-
it("should include productId as button value");
53-
it("should disable button when cart is loading");
117+
it("should include hidden redirectTo input with correct value", () => {
118+
// Step 1: Setup
119+
const productId = 123;
120+
const props = createTestProps({ id: productId });
121+
// Step 2: Mock - Component mocks already set up above
122+
// Step 3: Call
123+
render(<Product {...props} />);
124+
// Step 4: Verify
125+
const redirectInput = screen.queryByDisplayValue(
126+
`/products/${productId}`
127+
);
128+
expect(redirectInput).toBeInTheDocument();
129+
});
130+
131+
it("should disable button when cart is loading", () => {
132+
// Step 1: Setup
133+
const props = createTestProps();
134+
const expectedNavigation = createTestNavigation({ state: "submitting" });
135+
// Step 2: Mock - Override navigation state to simulate loading
136+
vi.mocked(useNavigation).mockReturnValue(expectedNavigation as any);
137+
// Step 3: Call
138+
render(<Product {...props} />);
139+
// Step 4: Verify
140+
const button = screen.getByRole("button");
141+
expect(button).toBeDisabled();
142+
expect(button).toHaveTextContent("Agregando...");
143+
});
54144
});
55145

56146
describe("Error handling", () => {
57-
it("should render NotFound component when product is not provided");
147+
it("should render NotFound component when product is not provided", () => {
148+
// Step 1: Setup - Create props without product
149+
const props = createTestProps();
150+
props.loaderData.product = undefined;
151+
152+
// Step 2: Mock - Mock NotFound component
153+
// vi.mock("../not-found", () => ({
154+
// default: () => <div data-testid="not-found">Not Found Page</div>,
155+
// }));
156+
// Step 3: Call
157+
render(<Product {...props} />);
158+
// Step 4: Verify
159+
expect(screen.getByTestId("not-found")).toBeInTheDocument();
160+
});
58161
});
59162
});

src/services/product.service.test.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("Product Service", () => {
2121
it("should return products for a valid category slug", async () => {
2222
// Step 1: Setup - Create test data with valid category and products
2323
const testCategory = createTestCategory();
24-
const mockedProducts = [
24+
const mockedProducts: Product[] = [
2525
createTestProduct({ id: 1, categoryId: testCategory.id }),
2626
createTestProduct({
2727
id: 2,
@@ -33,7 +33,7 @@ describe("Product Service", () => {
3333
// Step 2: Mock - Configure repository responses
3434
vi.mocked(getCategoryBySlug).mockResolvedValue(testCategory);
3535
vi.mocked(productRepository.getProductsByCategory).mockResolvedValue(
36-
mockedProducts as Product[]
36+
mockedProducts
3737
);
3838

3939
// Step 3: Call service function
@@ -52,19 +52,16 @@ describe("Product Service", () => {
5252
const invalidSlug = "invalid-slug";
5353

5454
// Step 2: Mock - Configure error response
55-
vi.mocked(getCategoryBySlug).mockRejectedValue(
56-
new Error(`Category with slug "${invalidSlug}" not found`)
57-
);
55+
const errorMessage = `Category with slug "${invalidSlug}" not found`;
56+
vi.mocked(getCategoryBySlug).mockRejectedValue(new Error(errorMessage));
5857

5958
// Step 3: Call service function
6059
const getProducts = getProductsByCategorySlug(
6160
invalidSlug as Category["slug"]
6261
);
6362

6463
// Step 4: Verify expected behavior
65-
await expect(getProducts).rejects.toThrow(
66-
`Category with slug "${invalidSlug}" not found`
67-
);
64+
await expect(getProducts).rejects.toThrow(errorMessage);
6865
expect(productRepository.getProductsByCategory).not.toHaveBeenCalled();
6966
});
7067
});
@@ -97,10 +94,10 @@ describe("Product Service", () => {
9794
vi.mocked(productRepository.getProductById).mockResolvedValue(null);
9895

9996
// Step 3: Call service function
100-
const getProduct = getProductById(nonExistentId);
97+
const productPromise = getProductById(nonExistentId);
10198

10299
// Step 4: Verify expected behavior
103-
await expect(getProduct).rejects.toThrow("Product not found");
100+
await expect(productPromise).rejects.toThrow("Product not found");
104101
});
105102
});
106103
});

0 commit comments

Comments
 (0)