274 lines
7.0 KiB
TypeScript
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-error');
|
|
});
|
|
});
|
|
|
|
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*=""]');
|
|
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();
|
|
});
|
|
});
|
|
});
|