From cb77ef9d206410bbf1d3448cc17ccb3d885ce053 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 1 Jul 2026 16:18:39 -0300 Subject: [PATCH] feat: add product detail page with variant selection and cart integration --- .../product-detail-page.component.html | 5 +- .../product-detail-page.component.spec.ts | 100 ++++++++++++++++++ .../product-detail-page.component.ts | 24 +++++ 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.html b/src/app/features/store/pages/product-detail-page/product-detail-page.component.html index 64e0457..892f588 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.html +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.html @@ -81,9 +81,10 @@ class="product-detail__cta" variant="secondary" type="button" - [disabled]="!selectedVariant() || variantLoading()" + [disabled]="!selectedVariant() || variantLoading() || addingToCart()" + (click)="addToCart()" > - @if (variantLoading()) { + @if (variantLoading() || addingToCart()) {
} @else { Agregar al carrito diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts index c37fcc7..1fa9295 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts @@ -11,6 +11,8 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { ProductDetail } from '../../../../core/services/catalog/catalog.interface'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; +import { CartService } from '../../../../core/services/cart/cart.service'; +import { ToastService } from '../../../../core/services/toast.service'; import { ProductDetailPageComponent } from './product-detail-page.component'; describe('ProductDetailPageComponent', () => { @@ -33,6 +35,8 @@ describe('ProductDetailPageComponent', () => { let paramMapSubject: BehaviorSubject; let catalogServiceStub: any; let routerStub: any; + let cartServiceStub: any; + let toastServiceStub: any; beforeAll(() => { try { @@ -54,6 +58,14 @@ describe('ProductDetailPageComponent', () => { routerStub = { navigate: vi.fn() }; + cartServiceStub = { + addItem: vi.fn().mockReturnValue(of({})) + }; + toastServiceStub = { + success: vi.fn(), + danger: vi.fn(), + info: vi.fn() + }; }); async function configureTestingModule() { @@ -73,6 +85,14 @@ describe('ProductDetailPageComponent', () => { { provide: Router, useValue: routerStub + }, + { + provide: CartService, + useValue: cartServiceStub + }, + { + provide: ToastService, + useValue: toastServiceStub } ] }).compileComponents(); @@ -329,4 +349,84 @@ describe('ProductDetailPageComponent', () => { expect(routerStub.navigate).not.toHaveBeenCalled(); }); + + it('calls CartService.addItem when variant is selected and Add to Cart is clicked', async () => { + const detailProduct: ProductDetail = { + ...mockProduct, + variant: { + id: 123, + stock: 5, + images: [], + definitions: {} + }, + variants_map: [ + { + variant_id: 123, + stock: 5, + attributes: {} + } + ] + }; + catalogServiceStub.getProducto.mockReturnValue(of(detailProduct)); + + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + + // Select the variant + fixture.componentInstance['selectedVariant'].set(detailProduct.variants_map[0]); + fixture.componentInstance['quantity'].set(3); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + const addToCartButton = Array.from(element.querySelectorAll('app-button button')).find( + (btn) => btn.textContent?.trim() === 'Agregar al carrito' + ) as HTMLButtonElement; + + expect(addToCartButton).toBeDefined(); + addToCartButton.click(); + fixture.detectChanges(); + + expect(cartServiceStub.addItem).toHaveBeenCalledWith(123, 3); + expect(toastServiceStub.success).toHaveBeenCalledWith('Producto agregado al carrito'); + }); + + it('shows error toast when CartService.addItem fails', async () => { + const detailProduct: ProductDetail = { + ...mockProduct, + variant: { + id: 123, + stock: 5, + images: [], + definitions: {} + }, + variants_map: [ + { + variant_id: 123, + stock: 5, + attributes: {} + } + ] + }; + catalogServiceStub.getProducto.mockReturnValue(of(detailProduct)); + cartServiceStub.addItem.mockReturnValue(throwError(() => new Error('Failed to add'))); + + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + + fixture.componentInstance['selectedVariant'].set(detailProduct.variants_map[0]); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + const addToCartButton = Array.from(element.querySelectorAll('app-button button')).find( + (btn) => btn.textContent?.trim() === 'Agregar al carrito' + ) as HTMLButtonElement; + + addToCartButton.click(); + fixture.detectChanges(); + + expect(cartServiceStub.addItem).toHaveBeenCalled(); + expect(toastServiceStub.danger).toHaveBeenCalledWith('No se pudo agregar el producto al carrito.'); + }); }); diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts index a761ce7..b7d0878 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts @@ -18,6 +18,7 @@ import { Subscription } from 'rxjs'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { ToastService } from '../../../../core/services/toast.service'; +import { CartService } from '../../../../core/services/cart/cart.service'; import { ProductAttribute, ProductAttributeOption, @@ -42,6 +43,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { private readonly router = inject(Router); private readonly catalogService = inject(CatalogService); private readonly toastService = inject(ToastService); + private readonly cartService = inject(CartService); private readonly attributeSelector = viewChild(ProductAttributeSelectorComponent); private readonly carouselHost = viewChild>('carouselHost'); private readonly descriptionBody = viewChild>('descriptionBody'); @@ -68,6 +70,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { }); protected readonly loading = signal(false); protected readonly variantLoading = signal(false); + protected readonly addingToCart = signal(false); protected readonly error = signal(null); protected readonly selectedVariant = signal(null); protected readonly quantity = signal(1); @@ -213,6 +216,27 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { this.quantity.update((current) => Math.max(1, current - 1)); } + protected addToCart(): void { + const variant = this.selectedVariant(); + if (!variant) { + this.toastService.danger('Por favor, selecciona una variante.'); + return; + } + + this.addingToCart.set(true); + this.cartService.addItem(variant.variant_id, this.quantity()).subscribe({ + next: () => { + this.toastService.success('Producto agregado al carrito'); + this.addingToCart.set(false); + }, + error: (err: HttpErrorResponse) => { + const errorMessage = err.error?.message || 'No se pudo agregar el producto al carrito.'; + this.toastService.danger(errorMessage); + this.addingToCart.set(false); + } + }); + } + protected toggleDescription(): void { this.descriptionExpanded.update((current) => !current); }