feat: add product detail page with variant selection and cart integration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user