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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ npm run test:e2e
- **Modificacion del archivo de data inicial**: Modificación del archivo 'initial_data.ts' para las diferentes variables de productos
- **Actualización del servicio de productos**: Modificación de las funciones para integrar variantes de productos
- **Filtros de Precio Inteligentes**: Implementación de la lógica de filtrado que considera todas las variantes de precio

- **Test para Product**: Actualización de los test para product service y product route
<!-- - **Testing de Variantes**: Actualización de tests unitarios y E2E para cubrir casos de uso con variantes -->

### 🤝 Tareas Colaborativas
Expand Down
44 changes: 37 additions & 7 deletions src/lib/utils.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { vi } from "vitest";

import type { Category } from "@/models/category.model";
import type { Order, OrderDetails, OrderItem } from "@/models/order.model";
import type { Product } from "@/models/product.model";
import type { Product, VariantAttributeValueWithNumber } from "@/models/product.model";
import type { User } from "@/models/user.model";

import type {
OrderItem as PrismaOrderItem,
Order as PrismaOrder,
Product as PrismaProduct,
VariantAttributeValue as PrismaVariantAttributeValue,

} from "@/../generated/prisma/client";
import type { Session } from "react-router";

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

export const createTestProduct = (overrides?: Partial<Product>): Product => ({
export const createTestDBProduct = (
overrides?: Partial<PrismaProduct> & { variantAttributeValues?: PrismaVariantAttributeValue[] }
): PrismaProduct & { variantAttributeValues: PrismaVariantAttributeValue[] } => ({
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(),
updatedAt: new Date(),
variantAttributeValues: overrides?.variantAttributeValues ?? [createTestDBVariantAttributeValue()],
...overrides,
});

export const createTestDBProduct = (
overrides?: Partial<PrismaProduct>
): PrismaProduct => ({
// --- FRONTEND PRODUCT ---
export const createTestProduct = (overrides?: Partial<Product>): Product => ({
id: 1,
title: "Test Product",
imgSrc: "/test-image.jpg",
alt: "Test alt text",
price: new Decimal(100),
description: "Test description",
categoryId: 1,
isOnSale: false,
features: ["Feature 1", "Feature 2"],
createdAt: new Date(),
updatedAt: new Date(),
variantAttributeValues: [createTestVariantAttributeValue()],
...overrides,
});


export const createTestCategory = (
overrides?: Partial<Category>
): Category => ({
Expand All @@ -99,6 +103,32 @@ export const createTestCategory = (
...overrides,
});

export const createTestDBVariantAttributeValue = (
overrides?: Partial<PrismaVariantAttributeValue>
): PrismaVariantAttributeValue => ({
id: 1,
attributeId: 1,
productId: 1,
value: "Default",
price: new Decimal(100),
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
export const createTestVariantAttributeValue = (
overrides: Partial<VariantAttributeValueWithNumber> = {}
): VariantAttributeValueWithNumber => ({
id: 1,
attributeId: 1,
productId: 1,
value: "Default",
price: 100, // ya es number
createdAt: new Date(),
updatedAt: new Date(),
variantAttribute: [{ id: 1, name: "Talla", createdAt: new Date(), updatedAt: new Date() }],
...overrides,
});

export const createTestOrderDetails = (
overrides: Partial<OrderDetails> = {}
): OrderDetails => ({
Expand Down
13 changes: 8 additions & 5 deletions src/models/product.model.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { VariantAttributeValue } from "./variant-attribute.model";
import type { Product as PrismaProduct } from "@/../generated/prisma/client";
import type { VariantAttributeValue as PrismaVariantAttributeValue, } from "./variant-attribute.model";
import type { Product as PrismaProduct, VariantAttribute } from "@/../generated/prisma/client";

export type Product = PrismaProduct & {
price?: number | null;
minPrice?: number | null;
maxPrice?: number | null;
variantAttributeValues?: VariantAttributeValue[];
variantAttributeValues?: VariantAttributeValueWithNumber[];
};

export type ProductVariantValue = PrismaProduct & {
variantAttributeValues: VariantAttributeValue[];


export type VariantAttributeValueWithNumber = Omit<PrismaVariantAttributeValue, "price"> & {
price: number
variantAttribute: VariantAttribute[]
}
6 changes: 5 additions & 1 deletion src/routes/product/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import NotFound from "../not-found";

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

const shirtId = 1;
const stickerId = 3;

export async function loader({ params }: Route.LoaderArgs) {
try {
const product = await getProductById(parseInt(params.id));
Expand All @@ -34,7 +37,7 @@ export default function Product({ loaderData }: Route.ComponentProps) {
const hasVariants = product?.variantAttributeValues && product.variantAttributeValues.length > 0;

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

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

return (
<>
<pre>{JSON.stringify(variantGroups, null, 2)}</pre>
<section className="py-12">
<Container className="flex flex-col gap-8 md:flex-row md:items-start">
<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">
Expand Down
164 changes: 62 additions & 102 deletions src/routes/product/product.test.tsx
Original file line number Diff line number Diff line change
@@ -1,163 +1,123 @@
import { render, screen } from "@testing-library/react";
import { useNavigation } from "react-router";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import { createTestProduct } from "@/lib/utils.tests";
import type { Product as ProductType } from "@/models/product.model";
import { createTestProduct, createTestVariantAttributeValue } from "@/lib/utils.tests";
import type { Product as ProductModel, VariantAttributeValueWithNumber } 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" as const,
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>),
}));
vi.mock("react-router", () => {
const actual = vi.importActual("react-router"); // mantener los demás exports reales
return {
...actual,
Form: vi.fn(({ children }) => <form>{children}</form>),
useNavigation: vi.fn(() => ({ state: "idle" } )),
useSearchParams: vi.fn(() => [new URLSearchParams(), vi.fn()]),
Link: vi.fn(({ children, ...props }) => <a {...props}>{children}</a>),
};
});

const createTestProps = (
productData: Partial<ProductType> = {}
productData: Partial<ProductModel> = {}
): Route.ComponentProps => ({
loaderData: { product: createTestProduct(productData) },
params: { id: "123" },
// Hack to satisfy type requirements
matches: [] as unknown as Route.ComponentProps["matches"],
});

describe("Product Component", () => {
describe("Rendering with valid product data", () => {
it("should render product title correctly", () => {
// 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");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Awesome Product");
});

it("should render product price with correct currency", () => {
// 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
const props = createTestProps({
categoryId: 1, // Para que el componente muestre variantes
variantAttributeValues: [
createTestVariantAttributeValue({ id: 1, value: "S", price: 100 }),
createTestVariantAttributeValue({ id: 2, value: "M", price: 120 }),
],
});
render(<Product {...props} />);
// Step 4: Verify - Check price is rendered correctly
expect(screen.queryByText("S/150.99")).toBeInTheDocument();
expect(screen.getByText("S/100.00")).toBeInTheDocument();
});

it("should render product description", () => {
// 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
const props = createTestProps({ description: "Amazing product" });
render(<Product {...props} />);
// Step 4: Verify - Check description is rendered
expect(screen.queryByText("Amazing product")).toBeInTheDocument();
expect(screen.getByText("Amazing product")).toBeInTheDocument();
});

it("should render product image with correct src and alt attributes", () => {
// Step 1: Setup - Create test props
it("should render product image with correct src and alt", () => {
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", () => {
// 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();
expect(screen.getByText(feature)).toBeInTheDocument();
});
});

it('should render "Agregar al Carrito" button', () => {
// 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();
expect(screen.getByRole("button", { name: "Agregar al Carrito" })).toBeInTheDocument();
});
});

describe("Form interactions", () => {
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 render variants and update price when variant is selected", () => {
const props = createTestProps({
categoryId: 1,
variantAttributeValues: [
{
id: 1,
attributeId: 1,
productId: 1,
value: "S",
price: 100,
createdAt: new Date(),
updatedAt: new Date(),
variantAttribute: { id: 1, name: "Talla" },
},
{
id: 2,
attributeId: 1,
productId: 1,
value: "M",
price: 120,
createdAt: new Date(),
updatedAt: new Date(),
variantAttribute: { id: 1, name: "Talla" },
},
] as VariantAttributeValueWithNumber[],
});

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);
// 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", () => {
// Step 1: Setup - Create props without product
const props = createTestProps();
props.loaderData.product = undefined;
const smallBtn = screen.getByRole("button", { name: "S" });
const mediumBtn = screen.getByRole("button", { name: "M" });
expect(smallBtn).toBeInTheDocument();
expect(mediumBtn).toBeInTheDocument();

// 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();
expect(screen.getByText("S/100.00")).toBeInTheDocument();

fireEvent.click(mediumBtn);
expect(screen.getByText("S/120.00")).toBeInTheDocument();
});
});
});
Loading