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