import '@angular/compiler'; import { signal } from '@angular/core'; import { TestBed, getTestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; import { of, throwError } from 'rxjs'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { CartService } from '../../../core/services/cart/cart.service'; import { ModalService } from '../../../core/services/modal.service'; import { ToastService } from '../../../core/services/toast.service'; import { CartComponent } from './cart.component'; describe('CartComponent', () => { beforeAll(() => { try { getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); } catch { // Test environment may already be initialized by another setup entrypoint. } }); afterEach(() => { TestBed.resetTestingModule(); }); it('shows an empty cart message when there are no items', async () => { await TestBed.configureTestingModule({ imports: [CartComponent], providers: [ { provide: CartService, useValue: { cart: signal(null).asReadonly(), updateItemQuantity: vi.fn(), removeItem: vi.fn(), }, }, { provide: ModalService, useValue: {}, }, { provide: ToastService, useValue: { success: vi.fn(), info: vi.fn(), danger: vi.fn(), }, }, ], }).compileComponents(); const fixture = TestBed.createComponent(CartComponent); fixture.detectChanges(); const emptyMessage = fixture.debugElement.query(By.css('.cart-empty-message')); expect(emptyMessage).not.toBeNull(); expect(emptyMessage.nativeElement.textContent.trim()).toBe('El carrito está vacío'); expect(fixture.debugElement.query(By.css('app-cart-item'))).toBeNull(); expect(fixture.debugElement.query(By.css('.cart-footer'))).toBeNull(); }); it('opens a confirm delete modal before removing an item', async () => { const removeItem = vi.fn().mockReturnValue(of({ message: 'Producto eliminado.' })); const openConfirmDelete = vi.fn().mockReturnValue(of(true)); await TestBed.configureTestingModule({ imports: [CartComponent], providers: [ { provide: CartService, useValue: { cart: signal(null).asReadonly(), updateItemQuantity: vi.fn(), removeItem, }, }, { provide: ModalService, useValue: { openConfirmDelete, }, }, { provide: ToastService, useValue: { success: vi.fn(), info: vi.fn(), danger: vi.fn(), }, }, ], }).compileComponents(); const fixture = TestBed.createComponent(CartComponent); fixture.componentRef.setInput('items', [ { cartItemId: 10, imageUrl: null, product: 'Producto de prueba', originalPrice: null, discountedPrice: 1000, discountPercentage: null, attributes: [], quantity: 1, }, ]); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('.cart-item-media'))).toBeNull(); fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('remove'); expect(openConfirmDelete).toHaveBeenCalledWith({ title: 'Eliminar producto', content: 'Se eliminara "Producto de prueba" del carrito. Esta accion no se puede deshacer.', confirmLabel: 'Eliminar', cancelLabel: 'Cancelar', }); expect(removeItem).toHaveBeenCalledWith(10); }); it('does not remove the item when the delete confirmation is cancelled', async () => { const removeItem = vi.fn().mockReturnValue(of({})); const openConfirmDelete = vi.fn().mockReturnValue(of(false)); await TestBed.configureTestingModule({ imports: [CartComponent], providers: [ { provide: CartService, useValue: { cart: signal(null).asReadonly(), updateItemQuantity: vi.fn(), removeItem, }, }, { provide: ModalService, useValue: { openConfirmDelete, }, }, { provide: ToastService, useValue: { success: vi.fn(), info: vi.fn(), danger: vi.fn(), }, }, ], }).compileComponents(); const fixture = TestBed.createComponent(CartComponent); fixture.componentRef.setInput('items', [ { cartItemId: 10, imageUrl: null, product: 'Producto de prueba', originalPrice: null, discountedPrice: 1000, discountPercentage: null, attributes: [], quantity: 1, }, ]); fixture.detectChanges(); fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('remove'); 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 = { cartItemId: 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 = { cartItemId: 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(); }); });