import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ManualCropUI from '@/components/ManualCropUI'; import type { CropBounds } from '@/hooks/useCropHandles'; describe('ManualCropUI Component', () => { const mockOnCropChange = vi.fn(); const testImageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; const imageDimensions = { width: 800, height: 600 }; beforeEach(() => { vi.clearAllMocks(); }); describe('Rendering', () => { it('should render component with image element', () => { render( ); const image = screen.getByAltText('Photo preview'); expect(image).toBeInTheDocument(); }); it('should render container with proper structure', () => { const { container } = render( ); const cropContainer = container.querySelector('div'); expect(cropContainer).toBeInTheDocument(); }); it('should show loading state when dimensions not provided', () => { render( ); // Component should render image element even without dimensions const image = screen.getByAltText('Photo preview'); expect(image).toBeInTheDocument(); // Should show loading message expect(screen.getByText('Loading image...')).toBeInTheDocument(); }); it('should render "Use Full Photo" button when crop is active', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; render( ); const button = screen.getByRole('button', { name: /use full photo/i }); expect(button).toBeInTheDocument(); }); it('should not render "Use Full Photo" button when no crop is set', () => { render( ); const button = screen.queryByRole('button', { name: /use full photo/i }); expect(button).not.toBeInTheDocument(); }); }); describe('Crop Handle Rendering', () => { it('should render 8 drag handles when crop is active', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; const { container } = render( ); const handles = container.querySelectorAll('button[aria-label*="Drag"]'); expect(handles.length).toBe(8); // 4 corners + 4 edges }); it('should render corner handles', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; render( ); expect(screen.getByLabelText('Drag top-left handle')).toBeInTheDocument(); expect(screen.getByLabelText('Drag top-right handle')).toBeInTheDocument(); expect(screen.getByLabelText('Drag bottom-left handle')).toBeInTheDocument(); expect(screen.getByLabelText('Drag bottom-right handle')).toBeInTheDocument(); }); it('should render edge handles', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; render( ); expect(screen.getByLabelText('Drag top handle')).toBeInTheDocument(); expect(screen.getByLabelText('Drag right handle')).toBeInTheDocument(); expect(screen.getByLabelText('Drag bottom handle')).toBeInTheDocument(); expect(screen.getByLabelText('Drag left handle')).toBeInTheDocument(); }); it('should not render handles when no crop is active', () => { const { container } = render( ); const handles = container.querySelectorAll('button[aria-label*="Drag"]'); expect(handles.length).toBe(0); }); }); describe('Semi-transparent Overlay', () => { it('should render overlay elements when crop is active', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; const { container } = render( ); const overlays = container.querySelectorAll('div[class*="bg-black"]'); expect(overlays.length).toBeGreaterThan(0); }); it('should render bounding box with cyan border', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; const { container } = render( ); const bbox = container.querySelector('div[class*="border-cyan"]'); expect(bbox).toBeInTheDocument(); expect(bbox).toHaveClass('border-cyan-400'); }); it('should not render overlay when no crop is active', () => { const { container } = render( ); const overlays = container.querySelectorAll('div[class*="bg-black/40"]'); expect(overlays.length).toBe(0); }); }); describe('onCropChange Callback', () => { it('should call onCropChange with initial crop', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; render( ); expect(mockOnCropChange).toHaveBeenCalledWith(initialCrop); }); it('should call onCropChange with null when no crop', () => { render( ); expect(mockOnCropChange).toHaveBeenCalledWith(null); }); }); describe('Use Full Photo Button', () => { it('should clear crop when "Use Full Photo" is clicked', async () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; render( ); const user = userEvent.setup(); const button = screen.getByRole('button', { name: /use full photo/i }); await user.click(button); await waitFor(() => { expect(mockOnCropChange).toHaveBeenCalledWith(null); }); }); it('should hide handles after clearing crop', async () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; const { container } = render( ); const user = userEvent.setup(); const button = screen.getByRole('button', { name: /use full photo/i }); await user.click(button); await waitFor(() => { const handles = container.querySelectorAll('button[aria-label*="Drag"]'); expect(handles.length).toBe(0); }); }); }); describe('Error Handling', () => { it('should have error handling for image load failures', () => { const { container } = render( ); // Component should render without crashing const image = screen.getByAltText('Photo preview'); expect(image).toBeInTheDocument(); }); }); describe('Image Dimension Handling', () => { it('should use provided imageDimensions prop', () => { const { container } = render( ); // Component should render successfully with imageDimensions const image = screen.getByAltText('Photo preview'); expect(image).toBeInTheDocument(); }); }); describe('Touch Support', () => { it('should render handles with touch support', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; render( ); const handle = screen.getByLabelText('Drag top-left handle'); // Check that ontouchstart is defined (event handler exists) expect(handle).toBeDefined(); }); }); describe('Mouse Support', () => { it('should render handles with mouse support', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; render( ); const handle = screen.getByLabelText('Drag top-left handle'); // Check that onmousedown is defined (event handler exists) expect(handle).toBeDefined(); }); }); describe('Responsive Behavior', () => { it('should render handles with consistent sizing', () => { const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; const { container } = render( ); const handles = container.querySelectorAll('button[aria-label*="Drag"]'); handles.forEach((handle) => { // Each handle should have width and height of 12px expect(handle).toHaveStyle('width: 12px'); expect(handle).toHaveStyle('height: 12px'); }); }); }); describe('Minimum Crop Size Enforcement', () => { it('should enforce minimum crop size', () => { const tinyBounds: CropBounds = { x: 100, y: 100, width: 10, height: 10 }; render( ); // onCropChange should be called with constrained bounds const calls = mockOnCropChange.mock.calls; const constrainedCrop = calls[calls.length - 1]?.[0]; if (constrainedCrop) { expect(constrainedCrop.width).toBeGreaterThanOrEqual(100); expect(constrainedCrop.height).toBeGreaterThanOrEqual(100); } }); }); describe('Crop Bounds Display', () => { it('should display crop bounds debug information', () => { const initialCrop: CropBounds = { x: 150, y: 200, width: 250, height: 300 }; render( ); const debugText = screen.getByText(/crop:/i); expect(debugText).toBeInTheDocument(); }); it('should not display bounds when no crop is set', () => { render( ); const debugText = screen.queryByText(/crop:/i); expect(debugText).not.toBeInTheDocument(); }); }); describe('TypeScript Strict Mode Compliance', () => { it('should accept required and optional props', () => { const { container } = render( ); expect(container).toBeInTheDocument(); }); it('should work with minimal required props', () => { const { container } = render( ); expect(container).toBeInTheDocument(); }); }); });