feat: add product detail page with variant selection and cart integration

This commit is contained in:
2026-07-01 16:18:39 -03:00
parent 8f7f9445b7
commit cb77ef9d20
3 changed files with 127 additions and 2 deletions

View File

@@ -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()) {
<div class="spinner-border spinner-border-sm" role="status"></div>
} @else {
Agregar al carrito

View File

@@ -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<any>;
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.');
});
});

View File

@@ -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<ElementRef<HTMLElement>>('carouselHost');
private readonly descriptionBody = viewChild<ElementRef<HTMLElement>>('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<string | null>(null);
protected readonly selectedVariant = signal<ProductVariantMap | null>(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);
}