diff --git a/src/app/core/services/modal.service.spec.ts b/src/app/core/services/modal.service.spec.ts index b25464e..c6ece6b 100644 --- a/src/app/core/services/modal.service.spec.ts +++ b/src/app/core/services/modal.service.spec.ts @@ -7,6 +7,7 @@ import { firstValueFrom } from 'rxjs'; import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component'; import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component'; +import { ImageModalComponent } from '../../shared/components/image-modal/image-modal.component'; import { QrModalComponent } from '../../shared/components/qr-modal/qr-modal.component'; import { SimpleModalComponent } from '../../shared/components/simple-modal/simple-modal.component'; import { MODAL_DATA, ModalRef, ModalService } from './modal.service'; @@ -258,6 +259,36 @@ describe('ModalService', () => { showCloseButton: true, }); }); + + it('opens the image viewer fullscreen with normalized zoom limits', () => { + service.openImage({ + title: 'Producto', + src: '/images/producto.webp', + alt: 'Producto visto de frente', + initialZoom: 8, + minZoom: 0, + maxZoom: 3, + }); + + const activeModal = service.activeModal(); + + expect(activeModal?.component).toBe(ImageModalComponent); + expect(activeModal?.config).toEqual({ + title: 'Producto', + size: 'full', + presentation: 'fullscreen-media', + data: { + src: '/images/producto.webp', + alt: 'Producto visto de frente', + initialZoom: 3, + minZoom: 0.1, + maxZoom: 3, + }, + closeOnBackdrop: true, + closeOnEscape: true, + showCloseButton: true, + }); + }); }); @Component({ diff --git a/src/app/shared/components/image-modal/image-modal.component.spec.ts b/src/app/shared/components/image-modal/image-modal.component.spec.ts new file mode 100644 index 0000000..eb58fc4 --- /dev/null +++ b/src/app/shared/components/image-modal/image-modal.component.spec.ts @@ -0,0 +1,106 @@ +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 } from 'vitest'; + +import { ImageModalData, MODAL_DATA } from '../../../core/services/modal.service'; +import { ImageModalComponent } from './image-modal.component'; + +describe('ImageModalComponent', () => { + let fixture: ComponentFixture; + 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 () => { + await TestBed.configureTestingModule({ + imports: [ImageModalComponent], + providers: [{ provide: MODAL_DATA, useValue: data }], + }).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', () => { + 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)); + fixture.detectChanges(); + + expect(zoom.textContent).toContain('150%'); + }); + + 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; +}