Files
tfm_ainventory/frontend/tests/components/PhotoModal.test.tsx
Daniel Bedeleanu 3df15cf68f feat(phase2): add photo display to inventory card with modal viewer
- Create PhotoModal component for full-res photo viewing
- Add photo thumbnail (200px square) to inventory item card
- Implement photo modal trigger on thumbnail click
- Add fallback text when no photo available
- Modal closeable via X button, click outside, or Escape key
- Image scales responsively without stretching
- Add comprehensive test coverage (30+ tests)
- All 427 tests passing, build successful
- TypeScript strict mode compliant
2026-04-21 14:53:27 +03:00

274 lines
7.0 KiB
TypeScript

import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import PhotoModal from '@/components/PhotoModal';
describe('PhotoModal', () => {
const mockOnClose = vi.fn();
const mockPhotoUrl = 'https://example.com/photo.jpg';
beforeEach(() => {
mockOnClose.mockClear();
});
describe('Rendering', () => {
it('should render photo modal with image', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const image = screen.getByAltText('Test Item');
expect(image).toBeInTheDocument();
expect(image).toHaveAttribute('src', mockPhotoUrl);
});
it('should display title in header', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
expect(screen.getByText('Test Item')).toBeInTheDocument();
});
it('should render with default title if not provided', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
/>
);
expect(screen.getByText('Photo')).toBeInTheDocument();
});
it('should render close button with rose color', () => {
const { container } = render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const closeButton = screen.getByLabelText('Close modal');
expect(closeButton).toBeInTheDocument();
// Check for rose-colored X icon
const xIcon = closeButton.querySelector('svg');
expect(xIcon).toHaveClass('text-rose-500');
});
});
describe('Close Interactions', () => {
it('should close when clicking close button', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const closeButton = screen.getByLabelText('Close modal');
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it('should close when clicking outside modal (on backdrop)', () => {
const { container } = render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
// Find the backdrop element (has onClick handler directly)
const backdrop = container.querySelector('[role="dialog"][class*="fixed"][class*="inset-0"]');
if (backdrop) {
fireEvent.click(backdrop);
expect(mockOnClose).toHaveBeenCalledTimes(1);
}
});
it('should NOT close when clicking on modal image', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const image = screen.getByAltText('Test Item');
fireEvent.click(image);
expect(mockOnClose).not.toHaveBeenCalled();
});
it('should close when pressing Escape key', async () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
fireEvent.keyDown(window, { key: 'Escape' });
await waitFor(() => {
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
});
it('should NOT close when pressing other keys', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
fireEvent.keyDown(window, { key: 'Enter' });
expect(mockOnClose).not.toHaveBeenCalled();
});
});
describe('Image Properties', () => {
it('should have lazy loading enabled', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const image = screen.getByAltText('Test Item');
expect(image).toHaveAttribute('loading', 'lazy');
});
it('should have object-contain class for proper scaling', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const image = screen.getByAltText('Test Item');
expect(image).toHaveClass('object-contain');
});
it('should render image with proper max-height constraint', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const image = screen.getByAltText('Test Item');
// Image has max-h constraint
expect(image).toHaveClass('max-h-[calc(90vh-120px)]');
});
});
describe('Accessibility', () => {
it('should have proper ARIA attributes for dialog', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const dialog = screen.getByRole('dialog');
expect(dialog).toHaveAttribute('aria-modal', 'true');
expect(dialog).toHaveAttribute('aria-label', 'Photo viewer for Test Item');
});
it('should have accessible close button', () => {
render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const closeButton = screen.getByLabelText('Close modal');
expect(closeButton).toBeInTheDocument();
});
});
describe('Responsive Design', () => {
it('should have responsive max-width classes', () => {
const { container } = render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
const backdrop = screen.getByRole('dialog');
// The modal content div is the second child of backdrop
const modalContent = backdrop.querySelector('div[class*="rounded-3xl"]');
expect(modalContent).toHaveClass('max-w-2xl', 'w-full', 'max-h-[90vh]');
});
it('should have responsive padding in header', () => {
const { container } = render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
// Header should have responsive padding
const header = screen.getByText('Test Item').closest('div');
expect(header).toHaveClass('p-4', 'md:p-6');
});
});
describe('Cleanup', () => {
it('should remove keyboard listener on unmount', () => {
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
const { unmount } = render(
<PhotoModal
photoUrl={mockPhotoUrl}
onClose={mockOnClose}
title="Test Item"
/>
);
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'keydown',
expect.any(Function)
);
removeEventListenerSpy.mockRestore();
});
});
});