Skip to content

Commit 1bd306c

Browse files
Merge pull request #281 from codeableorg/feature/product-unit-test
Feature/product unit test
2 parents 3d0babc + bccd340 commit 1bd306c

File tree

7 files changed

+156
-168
lines changed

7 files changed

+156
-168
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ npm run test:e2e
318318
- **Modificacion del archivo de data inicial**: Modificación del archivo 'initial_data.ts' para las diferentes variables de productos
319319
- **Actualización del servicio de productos**: Modificación de las funciones para integrar variantes de productos
320320
- **Filtros de Precio Inteligentes**: Implementación de la lógica de filtrado que considera todas las variantes de precio
321-
321+
- **Test para Product**: Actualización de los test para product service y product route
322322
<!-- - **Testing de Variantes**: Actualización de tests unitarios y E2E para cubrir casos de uso con variantes -->
323323

324324
### 🤝 Tareas Colaborativas

src/lib/utils.tests.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { vi } from "vitest";
22

33
import type { Category } from "@/models/category.model";
44
import type { Order, OrderDetails, OrderItem } from "@/models/order.model";
5-
import type { Product } from "@/models/product.model";
5+
import type { Product, VariantAttributeValueWithNumber } from "@/models/product.model";
66
import type { User } from "@/models/user.model";
77

88
import type {
99
OrderItem as PrismaOrderItem,
1010
Order as PrismaOrder,
1111
Product as PrismaProduct,
12+
VariantAttributeValue as PrismaVariantAttributeValue,
13+
1214
} from "@/../generated/prisma/client";
1315
import type { Session } from "react-router";
1416

@@ -53,38 +55,40 @@ export const createMockSession = (userId: number | null): Session => ({
5355
unset: vi.fn(),
5456
});
5557

56-
export const createTestProduct = (overrides?: Partial<Product>): Product => ({
58+
export const createTestDBProduct = (
59+
overrides?: Partial<PrismaProduct> & { variantAttributeValues?: PrismaVariantAttributeValue[] }
60+
): PrismaProduct & { variantAttributeValues: PrismaVariantAttributeValue[] } => ({
5761
id: 1,
5862
title: "Test Product",
5963
imgSrc: "/test-image.jpg",
6064
alt: "Test alt text",
61-
price: 100,
6265
description: "Test description",
6366
categoryId: 1,
6467
isOnSale: false,
6568
features: ["Feature 1", "Feature 2"],
6669
createdAt: new Date(),
6770
updatedAt: new Date(),
71+
variantAttributeValues: overrides?.variantAttributeValues ?? [createTestDBVariantAttributeValue()],
6872
...overrides,
6973
});
7074

71-
export const createTestDBProduct = (
72-
overrides?: Partial<PrismaProduct>
73-
): PrismaProduct => ({
75+
// --- FRONTEND PRODUCT ---
76+
export const createTestProduct = (overrides?: Partial<Product>): Product => ({
7477
id: 1,
7578
title: "Test Product",
7679
imgSrc: "/test-image.jpg",
7780
alt: "Test alt text",
78-
price: new Decimal(100),
7981
description: "Test description",
8082
categoryId: 1,
8183
isOnSale: false,
8284
features: ["Feature 1", "Feature 2"],
8385
createdAt: new Date(),
8486
updatedAt: new Date(),
87+
variantAttributeValues: [createTestVariantAttributeValue()],
8588
...overrides,
8689
});
8790

91+
8892
export const createTestCategory = (
8993
overrides?: Partial<Category>
9094
): Category => ({
@@ -99,6 +103,32 @@ export const createTestCategory = (
99103
...overrides,
100104
});
101105

106+
export const createTestDBVariantAttributeValue = (
107+
overrides?: Partial<PrismaVariantAttributeValue>
108+
): PrismaVariantAttributeValue => ({
109+
id: 1,
110+
attributeId: 1,
111+
productId: 1,
112+
value: "Default",
113+
price: new Decimal(100),
114+
createdAt: new Date(),
115+
updatedAt: new Date(),
116+
...overrides,
117+
});
118+
export const createTestVariantAttributeValue = (
119+
overrides: Partial<VariantAttributeValueWithNumber> = {}
120+
): VariantAttributeValueWithNumber => ({
121+
id: 1,
122+
attributeId: 1,
123+
productId: 1,
124+
value: "Default",
125+
price: 100, // ya es number
126+
createdAt: new Date(),
127+
updatedAt: new Date(),
128+
variantAttribute: [{ id: 1, name: "Talla", createdAt: new Date(), updatedAt: new Date() }],
129+
...overrides,
130+
});
131+
102132
export const createTestOrderDetails = (
103133
overrides: Partial<OrderDetails> = {}
104134
): OrderDetails => ({

src/models/product.model.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import type { VariantAttributeValue } from "./variant-attribute.model";
2-
import type { Product as PrismaProduct } from "@/../generated/prisma/client";
1+
import type { VariantAttributeValue as PrismaVariantAttributeValue, } from "./variant-attribute.model";
2+
import type { Product as PrismaProduct, VariantAttribute } from "@/../generated/prisma/client";
33

44
export type Product = PrismaProduct & {
55
price?: number | null;
66
minPrice?: number | null;
77
maxPrice?: number | null;
8-
variantAttributeValues?: VariantAttributeValue[];
8+
variantAttributeValues?: VariantAttributeValueWithNumber[];
99
};
1010

11-
export type ProductVariantValue = PrismaProduct & {
12-
variantAttributeValues: VariantAttributeValue[];
11+
12+
13+
export type VariantAttributeValueWithNumber = Omit<PrismaVariantAttributeValue, "price"> & {
14+
price: number
15+
variantAttribute: VariantAttribute[]
1316
}

src/routes/product/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import NotFound from "../not-found";
99

1010
import type { Route } from "./+types";
1111

12+
const shirtId = 1;
13+
const stickerId = 3;
14+
1215
export async function loader({ params }: Route.LoaderArgs) {
1316
try {
1417
const product = await getProductById(parseInt(params.id));
@@ -34,7 +37,7 @@ export default function Product({ loaderData }: Route.ComponentProps) {
3437
const hasVariants = product?.variantAttributeValues && product.variantAttributeValues.length > 0;
3538

3639
// Verificar si debe mostrar selectores (solo polos y stickers)
37-
const shouldShowVariants = hasVariants && (product?.categoryId === 1 || product?.categoryId === 3);
40+
const shouldShowVariants = hasVariants && (product?.categoryId === shirtId || product?.categoryId === stickerId);
3841

3942
// Agrupar variantes por atributo
4043
const variantGroups = shouldShowVariants
@@ -75,6 +78,7 @@ export default function Product({ loaderData }: Route.ComponentProps) {
7578

7679
return (
7780
<>
81+
<pre>{JSON.stringify(variantGroups, null, 2)}</pre>
7882
<section className="py-12">
7983
<Container className="flex flex-col gap-8 md:flex-row md:items-start">
8084
<div className="bg-muted rounded-xl min-w-[min(100%,28rem)] self-center flex-grow max-w-xl md:min-w-fit md:self-start">
Lines changed: 62 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,123 @@
1-
import { render, screen } from "@testing-library/react";
2-
import { useNavigation } from "react-router";
1+
import { render, screen, fireEvent } from "@testing-library/react";
32
import { describe, expect, it, vi } from "vitest";
43

5-
import { createTestProduct } from "@/lib/utils.tests";
6-
import type { Product as ProductType } from "@/models/product.model";
4+
import { createTestProduct, createTestVariantAttributeValue } from "@/lib/utils.tests";
5+
import type { Product as ProductModel, VariantAttributeValueWithNumber } from "@/models/product.model";
76

87
import Product from ".";
98

109
import type { Route } from "./+types";
1110

12-
// Helper function to create a test navigation object
13-
const createTestNavigation = (overrides = {}) => ({
14-
state: "idle" as const,
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-
2511
// 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-
}));
12+
vi.mock("react-router", () => {
13+
const actual = vi.importActual("react-router"); // mantener los demás exports reales
14+
return {
15+
...actual,
16+
Form: vi.fn(({ children }) => <form>{children}</form>),
17+
useNavigation: vi.fn(() => ({ state: "idle" } )),
18+
useSearchParams: vi.fn(() => [new URLSearchParams(), vi.fn()]),
19+
Link: vi.fn(({ children, ...props }) => <a {...props}>{children}</a>),
20+
};
21+
});
3122

3223
const createTestProps = (
33-
productData: Partial<ProductType> = {}
24+
productData: Partial<ProductModel> = {}
3425
): Route.ComponentProps => ({
3526
loaderData: { product: createTestProduct(productData) },
3627
params: { id: "123" },
37-
// Hack to satisfy type requirements
3828
matches: [] as unknown as Route.ComponentProps["matches"],
3929
});
4030

4131
describe("Product Component", () => {
4232
describe("Rendering with valid product data", () => {
4333
it("should render product title correctly", () => {
44-
// Step 1: Setup - Create test props
4534
const props = createTestProps({ title: "Awesome Product" });
46-
// Step 2: Mock - Component mocks already set up above
47-
// Step 3: Call - Render component
4835
render(<Product {...props} />);
49-
// Step 4: Verify - Check title is rendered correctly
50-
const titleElement = screen.getByRole("heading", { level: 1 });
51-
expect(titleElement).toHaveTextContent("Awesome Product");
36+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Awesome Product");
5237
});
5338

5439
it("should render product price with correct currency", () => {
55-
// Step 1: Setup - Create test props
56-
const props = createTestProps({ price: 150.99 });
57-
// Step 2: Mock - Component mocks already set up above
58-
// Step 3: Call - Render component
40+
const props = createTestProps({
41+
categoryId: 1, // Para que el componente muestre variantes
42+
variantAttributeValues: [
43+
createTestVariantAttributeValue({ id: 1, value: "S", price: 100 }),
44+
createTestVariantAttributeValue({ id: 2, value: "M", price: 120 }),
45+
],
46+
});
5947
render(<Product {...props} />);
60-
// Step 4: Verify - Check price is rendered correctly
61-
expect(screen.queryByText("S/150.99")).toBeInTheDocument();
48+
expect(screen.getByText("S/100.00")).toBeInTheDocument();
6249
});
6350

6451
it("should render product description", () => {
65-
// Step 1: Setup - Create test props
66-
const props = createTestProps({
67-
description: "Amazing product",
68-
});
69-
// Step 2: Mock - Component mocks already set up above
70-
// Step 3: Call - Render component
52+
const props = createTestProps({ description: "Amazing product" });
7153
render(<Product {...props} />);
72-
// Step 4: Verify - Check description is rendered
73-
expect(screen.queryByText("Amazing product")).toBeInTheDocument();
54+
expect(screen.getByText("Amazing product")).toBeInTheDocument();
7455
});
7556

76-
it("should render product image with correct src and alt attributes", () => {
77-
// Step 1: Setup - Create test props
57+
it("should render product image with correct src and alt", () => {
7858
const props = createTestProps({
7959
imgSrc: "/test-image.jpg",
8060
alt: "Test Product",
8161
});
82-
// Step 2: Mock - Component mocks already set up above
83-
// Step 3: Call - Render component
8462
render(<Product {...props} />);
85-
// Step 4: Verify - Check image attributes
8663
const image = screen.getByRole("img");
8764
expect(image).toHaveAttribute("src", "/test-image.jpg");
8865
expect(image).toHaveAttribute("alt", "Test Product");
8966
});
9067

9168
it("should render all product features as list items", () => {
92-
// Step 1: Setup - Create test props
9369
const features = ["Feature 1", "Feature 2", "Feature 3"];
9470
const props = createTestProps({ features });
95-
// Step 2: Mock - Component mocks already set up above
96-
// Step 3: Call - Render component
9771
render(<Product {...props} />);
98-
// Step 4: Verify - Check features are rendered
9972
features.forEach((feature) => {
100-
expect(screen.queryByText(feature)).toBeInTheDocument();
73+
expect(screen.getByText(feature)).toBeInTheDocument();
10174
});
10275
});
10376

10477
it('should render "Agregar al Carrito" button', () => {
105-
// Step 1: Setup - Create test props
10678
const props = createTestProps();
107-
// Step 2: Mock - Component mocks already set up above
108-
// Step 3: Call - Render component
10979
render(<Product {...props} />);
110-
// Step 4: Verify - Check button is present
111-
expect(
112-
screen.queryByRole("button", { name: "Agregar al Carrito" })
113-
).toBeInTheDocument();
80+
expect(screen.getByRole("button", { name: "Agregar al Carrito" })).toBeInTheDocument();
11481
});
115-
});
11682

117-
describe("Form interactions", () => {
118-
it("should include hidden redirectTo input with correct value", () => {
119-
// Step 1: Setup
120-
const productId = 123;
121-
const props = createTestProps({ id: productId });
122-
// Step 2: Mock - Component mocks already set up above
123-
// Step 3: Call
124-
render(<Product {...props} />);
125-
// Step 4: Verify
126-
const redirectInput = screen.queryByDisplayValue(
127-
`/products/${productId}`
128-
);
129-
expect(redirectInput).toBeInTheDocument();
130-
});
83+
it("should render variants and update price when variant is selected", () => {
84+
const props = createTestProps({
85+
categoryId: 1,
86+
variantAttributeValues: [
87+
{
88+
id: 1,
89+
attributeId: 1,
90+
productId: 1,
91+
value: "S",
92+
price: 100,
93+
createdAt: new Date(),
94+
updatedAt: new Date(),
95+
variantAttribute: { id: 1, name: "Talla" },
96+
},
97+
{
98+
id: 2,
99+
attributeId: 1,
100+
productId: 1,
101+
value: "M",
102+
price: 120,
103+
createdAt: new Date(),
104+
updatedAt: new Date(),
105+
variantAttribute: { id: 1, name: "Talla" },
106+
},
107+
] as VariantAttributeValueWithNumber[],
108+
});
131109

132-
it("should disable button when cart is loading", () => {
133-
// Step 1: Setup
134-
const props = createTestProps();
135-
const expectedNavigation = createTestNavigation({ state: "submitting" });
136-
// Step 2: Mock - Override navigation state to simulate loading
137-
vi.mocked(useNavigation).mockReturnValue(expectedNavigation);
138-
// Step 3: Call
139110
render(<Product {...props} />);
140-
// Step 4: Verify
141-
const button = screen.getByRole("button");
142-
expect(button).toBeDisabled();
143-
expect(button).toHaveTextContent("Agregando...");
144-
});
145-
});
146111

147-
describe("Error handling", () => {
148-
it("should render NotFound component when product is not provided", () => {
149-
// Step 1: Setup - Create props without product
150-
const props = createTestProps();
151-
props.loaderData.product = undefined;
112+
const smallBtn = screen.getByRole("button", { name: "S" });
113+
const mediumBtn = screen.getByRole("button", { name: "M" });
114+
expect(smallBtn).toBeInTheDocument();
115+
expect(mediumBtn).toBeInTheDocument();
152116

153-
// Step 2: Mock - Mock NotFound component
154-
// vi.mock("../not-found", () => ({
155-
// default: () => <div data-testid="not-found">Not Found Page</div>,
156-
// }));
157-
// Step 3: Call
158-
render(<Product {...props} />);
159-
// Step 4: Verify
160-
expect(screen.getByTestId("not-found")).toBeInTheDocument();
117+
expect(screen.getByText("S/100.00")).toBeInTheDocument();
118+
119+
fireEvent.click(mediumBtn);
120+
expect(screen.getByText("S/120.00")).toBeInTheDocument();
161121
});
162122
});
163123
});

0 commit comments

Comments
 (0)