From 8e73418196f20a1069d2e81fef0f436d817ef562 Mon Sep 17 00:00:00 2001 From: Janet Huacahuasi Date: Mon, 1 Sep 2025 01:51:24 -0500 Subject: [PATCH 1/2] feat: update product service test --- src/lib/utils.tests.ts | 31 ++++++++--- src/services/product.service.test.ts | 82 +++++++++++++--------------- 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/src/lib/utils.tests.ts b/src/lib/utils.tests.ts index 1526f23..97f306d 100644 --- a/src/lib/utils.tests.ts +++ b/src/lib/utils.tests.ts @@ -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 { ProductVariantValue } 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"; @@ -53,38 +55,40 @@ export const createMockSession = (userId: number | null): Session => ({ unset: vi.fn(), }); -export const createTestProduct = (overrides?: Partial): Product => ({ +export const createTestDBProduct = ( + overrides?: Partial & { 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 => ({ +// --- FRONTEND PRODUCT --- +export const createTestProduct = (overrides?: Partial): ProductVariantValue => ({ 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: [createTestDBVariantAttributeValue()], ...overrides, }); + export const createTestCategory = ( overrides?: Partial ): Category => ({ @@ -99,6 +103,19 @@ export const createTestCategory = ( ...overrides, }); +export const createTestDBVariantAttributeValue = ( + overrides?: Partial +): PrismaVariantAttributeValue => ({ + id: 1, + attributeId: 1, + productId: 1, + value: "Default", + price: new Decimal(100), + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, +}); + export const createTestOrderDetails = ( overrides: Partial = {} ): OrderDetails => ({ diff --git a/src/services/product.service.test.ts b/src/services/product.service.test.ts index 7bac27a..bb2270f 100644 --- a/src/services/product.service.test.ts +++ b/src/services/product.service.test.ts @@ -1,13 +1,19 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { vi, describe, it, expect, beforeEach } from "vitest"; + import { prisma as mockPrisma } from "@/db/prisma"; -import { createTestCategory, createTestDBProduct } from "@/lib/utils.tests"; -import type { Category } from "@/models/category.model"; +import { + createTestCategory, + createTestDBProduct, + createTestDBVariantAttributeValue, +} from "@/lib/utils.tests"; import { getCategoryBySlug } from "./category.service"; -import { getProductById, getProductsByCategorySlug } from "./product.service"; +import { getProductsByCategorySlug, getProductById } from "./product.service"; + +import type { Category } from "generated/prisma/client"; -import type { Product as PrismaProduct } from "@/../generated/prisma/client"; +import { Decimal } from "@/../generated/prisma/internal/prismaNamespace"; vi.mock("@/db/prisma", () => ({ prisma: { @@ -18,7 +24,6 @@ vi.mock("@/db/prisma", () => ({ }, })); -// Mock category service vi.mock("./category.service"); describe("Product Service", () => { @@ -27,93 +32,82 @@ describe("Product Service", () => { }); describe("getProductsByCategorySlug", () => { - it("should return products for a valid category slug", async () => { - // Step 1: Setup - Create test data with valid category and products + it("should return products for a valid category slug with prices as numbers", async () => { const testCategory = createTestCategory(); - const mockedProducts: PrismaProduct[] = [ + + const mockedProducts = [ createTestDBProduct({ id: 1, categoryId: testCategory.id }), - createTestDBProduct({ - id: 2, - title: "Test Product 2", - categoryId: testCategory.id, - }), + createTestDBProduct({ id: 2, categoryId: testCategory.id }), ]; - // Step 2: Mock - Configure responses vi.mocked(getCategoryBySlug).mockResolvedValue(testCategory); - vi.mocked(mockPrisma.product.findMany).mockResolvedValue(mockedProducts); - // Step 3: Call service function const products = await getProductsByCategorySlug(testCategory.slug); - // Step 4: Verify expected behavior expect(getCategoryBySlug).toHaveBeenCalledWith(testCategory.slug); expect(mockPrisma.product.findMany).toHaveBeenCalledWith({ where: { categoryId: testCategory.id }, + include: { variantAttributeValues: true }, }); + + // Comprobamos que los precios se transforman a number expect(products).toEqual( mockedProducts.map((product) => ({ ...product, - price: product.price.toNumber(), + price: product.variantAttributeValues[0].price.toNumber(), })) ); }); - it("should throw error when category slug does not exist", async () => { - // Step 1: Setup - Create test data for non-existent category const invalidSlug = "invalid-slug"; - - // 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"] - ); + vi.mocked(getCategoryBySlug).mockRejectedValue(new Error(errorMessage)); - // Step 4: Verify expected behavior - await expect(getProducts).rejects.toThrow(errorMessage); + await expect(getProductsByCategorySlug(invalidSlug as Category["slug"])).rejects.toThrow(errorMessage); expect(mockPrisma.product.findMany).not.toHaveBeenCalled(); }); }); describe("getProductById", () => { - it("should return product for valid ID", async () => { - // Step 1: Setup - Create test data for existing product - const testProduct = createTestDBProduct(); + it("should return product for valid ID with prices as numbers", async () => { + const testProduct = createTestDBProduct({ + id: 1, + variantAttributeValues: [ + createTestDBVariantAttributeValue({ price: new Decimal(120) }), + createTestDBVariantAttributeValue({ id: 2, price: new Decimal(150) }), + ], + }); - // Step 2: Mock - Configure Prisma response vi.mocked(mockPrisma.product.findUnique).mockResolvedValue(testProduct); - // Step 3: Call service function const result = await getProductById(testProduct.id); - // Step 4: Verify expected behavior expect(mockPrisma.product.findUnique).toHaveBeenCalledWith({ where: { id: testProduct.id }, + include: { variantAttributeValues: { include: { variantAttribute: true } } }, }); + + // Se espera que el producto devuelto tenga minPrice y maxPrice correctamente calculados expect(result).toEqual({ ...testProduct, - price: testProduct.price.toNumber(), + variantAttributeValues: testProduct.variantAttributeValues.map((v) => ({ + ...v, + price: v.price.toNumber(), + })), }); }); 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 Prisma vi.mocked(mockPrisma.product.findUnique).mockResolvedValue(null); - // Step 3: Call service function - const productPromise = getProductById(nonExistentId); - - // Step 4: Verify expected behavior - await expect(productPromise).rejects.toThrow("Product not found"); + await expect(getProductById(nonExistentId)).rejects.toThrow("Product not found"); expect(mockPrisma.product.findUnique).toHaveBeenCalledWith({ where: { id: nonExistentId }, + include: { variantAttributeValues: { include: { variantAttribute: true } } }, }); }); }); From bccd340ae994cddf69fb947dbc4dfb4f3c746f65 Mon Sep 17 00:00:00 2001 From: Janet Huacahuasi Date: Mon, 1 Sep 2025 03:23:56 -0500 Subject: [PATCH 2/2] feat: add product page service --- README.md | 2 +- src/lib/utils.tests.ts | 19 +++- src/models/product.model.ts | 13 ++- src/routes/product/index.tsx | 6 +- src/routes/product/product.test.tsx | 164 ++++++++++----------------- src/services/product.service.test.ts | 7 +- src/services/product.service.ts | 10 +- 7 files changed, 99 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index b85b871..f593ea1 100644 --- a/README.md +++ b/README.md @@ -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 ### 🤝 Tareas Colaborativas diff --git a/src/lib/utils.tests.ts b/src/lib/utils.tests.ts index 97f306d..895766b 100644 --- a/src/lib/utils.tests.ts +++ b/src/lib/utils.tests.ts @@ -2,7 +2,7 @@ import { vi } from "vitest"; import type { Category } from "@/models/category.model"; import type { Order, OrderDetails, OrderItem } from "@/models/order.model"; -import type { ProductVariantValue } from "@/models/product.model"; +import type { Product, VariantAttributeValueWithNumber } from "@/models/product.model"; import type { User } from "@/models/user.model"; import type { @@ -73,7 +73,7 @@ export const createTestDBProduct = ( }); // --- FRONTEND PRODUCT --- -export const createTestProduct = (overrides?: Partial): ProductVariantValue => ({ +export const createTestProduct = (overrides?: Partial): Product => ({ id: 1, title: "Test Product", imgSrc: "/test-image.jpg", @@ -84,7 +84,7 @@ export const createTestProduct = (overrides?: Partial): Pro features: ["Feature 1", "Feature 2"], createdAt: new Date(), updatedAt: new Date(), - variantAttributeValues: [createTestDBVariantAttributeValue()], + variantAttributeValues: [createTestVariantAttributeValue()], ...overrides, }); @@ -115,6 +115,19 @@ export const createTestDBVariantAttributeValue = ( updatedAt: new Date(), ...overrides, }); +export const createTestVariantAttributeValue = ( + overrides: Partial = {} +): 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 = {} diff --git a/src/models/product.model.ts b/src/models/product.model.ts index 47259f5..ab58c43 100644 --- a/src/models/product.model.ts +++ b/src/models/product.model.ts @@ -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 & { + price: number + variantAttribute: VariantAttribute[] } \ No newline at end of file diff --git a/src/routes/product/index.tsx b/src/routes/product/index.tsx index 906ac90..ae2c370 100644 --- a/src/routes/product/index.tsx +++ b/src/routes/product/index.tsx @@ -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)); @@ -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 @@ -75,6 +78,7 @@ export default function Product({ loaderData }: Route.ComponentProps) { return ( <> +
{JSON.stringify(variantGroups, null, 2)}
diff --git a/src/routes/product/product.test.tsx b/src/routes/product/product.test.tsx index f70059d..3c2c5c2 100644 --- a/src/routes/product/product.test.tsx +++ b/src/routes/product/product.test.tsx @@ -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 }) =>
{children}
), - useNavigation: vi.fn(() => createTestNavigation()), - Link: vi.fn(({ children, ...props }) => {children}), -})); +vi.mock("react-router", () => { + const actual = vi.importActual("react-router"); // mantener los demás exports reales + return { + ...actual, + Form: vi.fn(({ children }) =>
{children}
), + useNavigation: vi.fn(() => ({ state: "idle" } )), + useSearchParams: vi.fn(() => [new URLSearchParams(), vi.fn()]), + Link: vi.fn(({ children, ...props }) => {children}), + }; +}); const createTestProps = ( - productData: Partial = {} + productData: Partial = {} ): 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(); - // 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(); - // 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(); - // 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(); - // 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(); - // 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(); - // 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(); - // 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(); - // 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: () =>
Not Found Page
, - // })); - // Step 3: Call - render(); - // 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(); }); }); }); diff --git a/src/services/product.service.test.ts b/src/services/product.service.test.ts index bb2270f..7da50a3 100644 --- a/src/services/product.service.test.ts +++ b/src/services/product.service.test.ts @@ -9,7 +9,7 @@ import { } from "@/lib/utils.tests"; import { getCategoryBySlug } from "./category.service"; -import { getProductsByCategorySlug, getProductById } from "./product.service"; +import { getProductsByCategorySlug, getProductById, formattedProduct } from "./product.service"; import type { Category } from "generated/prisma/client"; @@ -53,10 +53,7 @@ describe("Product Service", () => { // Comprobamos que los precios se transforman a number expect(products).toEqual( - mockedProducts.map((product) => ({ - ...product, - price: product.variantAttributeValues[0].price.toNumber(), - })) + mockedProducts.map(formattedProduct) ); }); it("should throw error when category slug does not exist", async () => { diff --git a/src/services/product.service.ts b/src/services/product.service.ts index 10eafb8..5b22cea 100644 --- a/src/services/product.service.ts +++ b/src/services/product.service.ts @@ -1,11 +1,11 @@ import { prisma } from "@/db/prisma"; import type { Category } from "@/models/category.model"; -import type { Product, ProductVariantValue } from "@/models/product.model"; +import type { Product } from "@/models/product.model"; import type { VariantAttributeValue } from "@/models/variant-attribute.model"; import { getCategoryBySlug } from "./category.service"; -const formattedProduct = (product: any): ProductVariantValue => { +export const formattedProduct = (product: any): Product => { const { variantAttributeValues, ...rest } = product; const prices = variantAttributeValues.map((v: VariantAttributeValue) => v.price.toNumber() @@ -61,7 +61,7 @@ export async function getProductById(id: number): Promise { })), }; - return productWithParsedPrices as unknown as ProductVariantValue; + return productWithParsedPrices as unknown as Product; } export async function getAllProducts(): Promise { @@ -78,7 +78,7 @@ export async function filterByMinMaxPrice( min?: number, max?: number ): Promise { - const priceFilter: any = {}; + const priceFilter: { gte?: number; lte?: number } = {}; if (min !== undefined) { priceFilter.gte = min; @@ -90,7 +90,7 @@ export async function filterByMinMaxPrice( const result = await prisma.product.findMany({ where: { category: { - slug: slug as any, // si slug es enum + slug: slug as Category["slug"], // si slug es enum }, variantAttributeValues: { some: {