diff --git a/src/app/shared/components/cart/cart.component.html b/src/app/shared/components/cart/cart.component.html index 52b9f15..234cde6 100644 --- a/src/app/shared/components/cart/cart.component.html +++ b/src/app/shared/components/cart/cart.component.html @@ -10,7 +10,7 @@
- @for (item of items(); track item.product + item.discountedPrice + resetKey(); let idx = $index) { + @for (item of items(); track item.productVariantId || item.product + item.discountedPrice; let idx = $index) { diff --git a/src/app/shared/components/cart/cart.component.spec.ts b/src/app/shared/components/cart/cart.component.spec.ts index 75928f2..9bbe29b 100644 --- a/src/app/shared/components/cart/cart.component.spec.ts +++ b/src/app/shared/components/cart/cart.component.spec.ts @@ -6,7 +6,7 @@ import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { CartService } from '../../../core/services/cart/cart.service'; @@ -141,4 +141,136 @@ describe('CartComponent', () => { expect(openConfirmDelete).toHaveBeenCalled(); expect(removeItem).not.toHaveBeenCalled(); }); + + it('optimistically updates quantity and rolls back on error', async () => { + vi.useFakeTimers(); + const updateItemQuantity = vi.fn().mockReturnValue(throwError(() => new Error('Error'))); + const danger = vi.fn(); + + await TestBed.configureTestingModule({ + imports: [CartComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + updateItemQuantity, + removeItem: vi.fn() + } + }, + { + provide: ModalService, + useValue: {} + }, + { + provide: ToastService, + useValue: { + success: vi.fn(), + info: vi.fn(), + danger + } + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(CartComponent); + const component = fixture.componentInstance; + + const item = { + productVariantId: 10, + imageUrl: null, + product: 'Producto de prueba', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1 + }; + + fixture.componentRef.setInput('items', [item]); + fixture.detectChanges(); + + // Trigger quantity change to 3 + fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('quantityChange', 3); + fixture.detectChanges(); + + // Optimistic update should be active immediately in local getter + expect((component as any).getItemQuantity(item)).toBe(3); + + // Wait for the debounce time (1000ms) + vi.advanceTimersByTime(1000); + fixture.detectChanges(); + + // After failure, it should roll back to original quantity (1) + expect((component as any).getItemQuantity(item)).toBe(1); + expect(danger).toHaveBeenCalled(); + + vi.useRealTimers(); + }); + + it('optimistically updates quantity and clears override on success', async () => { + vi.useFakeTimers(); + const updateItemQuantity = vi.fn().mockReturnValue(of({ message: 'Success', data: {} })); + const success = vi.fn(); + + await TestBed.configureTestingModule({ + imports: [CartComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + updateItemQuantity, + removeItem: vi.fn() + } + }, + { + provide: ModalService, + useValue: {} + }, + { + provide: ToastService, + useValue: { + success, + info: vi.fn(), + danger: vi.fn() + } + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(CartComponent); + const component = fixture.componentInstance; + + const item = { + productVariantId: 10, + imageUrl: null, + product: 'Producto de prueba', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1 + }; + + fixture.componentRef.setInput('items', [item]); + fixture.detectChanges(); + + // Trigger quantity change to 3 + fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('quantityChange', 3); + fixture.detectChanges(); + + // Optimistic update should be active immediately in local getter + expect((component as any).getItemQuantity(item)).toBe(3); + + // Wait for the debounce time (1000ms) + vi.advanceTimersByTime(1000); + fixture.detectChanges(); + + // After success, it should clear override and use input quantity (which is 1 since we didn't update items input here) + expect((component as any).getItemQuantity(item)).toBe(1); + expect(success).toHaveBeenCalled(); + + vi.useRealTimers(); + }); }); diff --git a/src/app/shared/components/cart/cart.component.ts b/src/app/shared/components/cart/cart.component.ts index 9eb2d12..bea2ff2 100644 --- a/src/app/shared/components/cart/cart.component.ts +++ b/src/app/shared/components/cart/cart.component.ts @@ -43,7 +43,7 @@ export class CartComponent { readonly closed = output(); - protected readonly resetKey = signal(0); + protected readonly quantityOverrides = signal>({}); constructor() { this.quantityUpdates$.pipe( @@ -55,12 +55,13 @@ export class CartComponent { next: (res) => { const msg = res.message || 'Cantidad de producto actualizada.'; this.toastService.success(msg); + this.clearOverride(update.productVariantId); }, error: (err: HttpErrorResponse) => { console.error('Error updating cart quantity', err); const msg = err.error?.message || 'Error al actualizar la cantidad del producto.'; this.toastService.danger(msg); - this.resetKey.update(k => k + 1); + this.clearOverride(update.productVariantId); } }), catchError(() => EMPTY) @@ -70,10 +71,29 @@ export class CartComponent { ).subscribe(); } + protected getItemQuantity(item: CartItemMock): number { + if (item.productVariantId !== undefined && this.quantityOverrides()[item.productVariantId] !== undefined) { + return this.quantityOverrides()[item.productVariantId]; + } + return item.quantity; + } + + private clearOverride(productVariantId: number): void { + this.quantityOverrides.update((overrides) => { + const copy = { ...overrides }; + delete copy[productVariantId]; + return copy; + }); + } + protected onItemQuantityChange(index: number, newQuantity: number): void { const mockItem = this.items()[index]; const productVariantId = mockItem?.productVariantId; if (productVariantId) { + this.quantityOverrides.update((overrides) => ({ + ...overrides, + [productVariantId]: newQuantity + })); this.quantityUpdates$.next({ productVariantId, quantity: newQuantity @@ -81,6 +101,10 @@ export class CartComponent { } else { const item = this.cartService.cart()?.items[index]; if (item) { + this.quantityOverrides.update((overrides) => ({ + ...overrides, + [item.product_variant_id]: newQuantity + })); this.quantityUpdates$.next({ productVariantId: item.product_variant_id, quantity: newQuantity