166 lines
6.2 KiB
TypeScript
166 lines
6.2 KiB
TypeScript
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<ImageModalComponent>;
|
|
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<void> {
|
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
}
|