import '@angular/compiler'; import { ComponentFixture, TestBed, getTestBed } from '@angular/core/testing'; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { ImageModalData, MODAL_DATA, ModalRef } from '../../../core/services/modal.service'; import { ImageModalComponent } from './image-modal.component'; describe('ImageModalComponent', () => { let fixture: ComponentFixture; const modalRef = { dismiss: vi.fn() }; const data: ImageModalData = { src: '/images/producto.webp', alt: 'Vista frontal del producto', initialZoom: 1, minZoom: 1, maxZoom: 2, }; beforeAll(() => { try { getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); } catch { // Test environment may already be initialized by another setup entrypoint. } }); beforeEach(async () => { modalRef.dismiss.mockReset(); await TestBed.configureTestingModule({ imports: [ImageModalComponent], providers: [ { provide: MODAL_DATA, useValue: data }, { provide: ModalRef, useValue: modalRef }, ], }).compileComponents(); fixture = TestBed.createComponent(ImageModalComponent); fixture.detectChanges(); }); afterEach(() => { fixture.destroy(); TestBed.resetTestingModule(); }); it('renders the image and accessible zoom controls', () => { const element = fixture.nativeElement as HTMLElement; const image = element.querySelector('img') as HTMLImageElement; expect(image.getAttribute('src')).toBe('/images/producto.webp'); expect(image.alt).toBe('Vista frontal del producto'); expect(element.querySelector('[aria-label="Acercar"]')).not.toBeNull(); expect(element.querySelector('[aria-label="Alejar"]')).not.toBeNull(); }); it('zooms with controls, respects limits, and resets', () => { const element = fixture.nativeElement as HTMLElement; const zoomIn = element.querySelector('[aria-label="Acercar"]') as HTMLButtonElement; const zoomOut = element.querySelector('[aria-label="Alejar"]') as HTMLButtonElement; const reset = element.querySelector('[aria-label="Restablecer zoom"]') as HTMLButtonElement; zoomIn.click(); fixture.detectChanges(); expect(reset.textContent).toContain('150%'); expect(zoomOut.disabled).toBe(false); zoomIn.click(); fixture.detectChanges(); expect(reset.textContent).toContain('200%'); expect(zoomIn.disabled).toBe(true); reset.click(); fixture.detectChanges(); expect(reset.textContent).toContain('100%'); expect(zoomOut.disabled).toBe(true); }); it('supports pinch zoom through touch pointer events', async () => { const viewport = fixture.nativeElement.querySelector('.image-modal__viewport') as HTMLElement; const zoom = fixture.nativeElement.querySelector( '[aria-label="Restablecer zoom"]', ) as HTMLButtonElement; viewport.dispatchEvent(pointerEvent('pointerdown', 1, 100, 100)); viewport.dispatchEvent(pointerEvent('pointerdown', 2, 200, 100)); viewport.dispatchEvent(pointerEvent('pointermove', 2, 250, 100)); await renderNextFrame(); fixture.detectChanges(); expect(zoom.textContent).toContain('150%'); }); it('dismisses with a downward swipe while the image is at its base zoom', async () => { const viewport = fixture.nativeElement.querySelector('.image-modal__viewport') as HTMLElement; const modal = fixture.nativeElement.querySelector('.image-modal') as HTMLElement; viewport.dispatchEvent(pointerEvent('pointerdown', 1, 150, 100)); viewport.dispatchEvent(pointerEvent('pointermove', 1, 155, 230)); await renderNextFrame(); fixture.detectChanges(); expect(modal.style.transform).toBe('translate3d(0, 130px, 0)'); expect(Number(modal.style.opacity)).toBeLessThan(1); viewport.dispatchEvent(pointerEvent('pointerup', 1, 155, 230)); expect(modalRef.dismiss).toHaveBeenCalledWith('swipe'); }); it('returns smoothly to its position when the swipe is too short', async () => { const viewport = fixture.nativeElement.querySelector('.image-modal__viewport') as HTMLElement; const modal = fixture.nativeElement.querySelector('.image-modal') as HTMLElement; viewport.dispatchEvent(pointerEvent('pointerdown', 1, 150, 100)); viewport.dispatchEvent(pointerEvent('pointermove', 1, 150, 160)); await renderNextFrame(); fixture.detectChanges(); expect(modal.style.transform).toBe('translate3d(0, 60px, 0)'); viewport.dispatchEvent(pointerEvent('pointerup', 1, 150, 160)); fixture.detectChanges(); expect(modalRef.dismiss).not.toHaveBeenCalled(); expect(modal.classList.contains('image-modal--settling')).toBe(true); expect(modal.style.transform).toBe('translate3d(0, 0px, 0)'); }); it('does not dismiss with a downward gesture while the image is zoomed', () => { const element = fixture.nativeElement as HTMLElement; const viewport = element.querySelector('.image-modal__viewport') as HTMLElement; const zoomIn = element.querySelector('[aria-label="Acercar"]') as HTMLButtonElement; zoomIn.click(); fixture.detectChanges(); viewport.dispatchEvent(pointerEvent('pointerdown', 1, 150, 100)); viewport.dispatchEvent(pointerEvent('pointermove', 1, 155, 230)); viewport.dispatchEvent(pointerEvent('pointerup', 1, 155, 230)); expect(modalRef.dismiss).not.toHaveBeenCalled(); }); it('shows a fallback when the image cannot be loaded', () => { const image = fixture.nativeElement.querySelector('img') as HTMLImageElement; image.dispatchEvent(new Event('error')); fixture.detectChanges(); expect(fixture.nativeElement.querySelector('[role="alert"]')?.textContent).toContain( 'No se pudo cargar la imagen', ); }); }); function pointerEvent(type: string, pointerId: number, clientX: number, clientY: number): Event { const event = new MouseEvent(type, { bubbles: true, clientX, clientY }); Object.defineProperties(event, { pointerId: { value: pointerId }, pointerType: { value: 'touch' }, }); return event; } async function renderNextFrame(): Promise { await new Promise((resolve) => requestAnimationFrame(() => resolve())); }