import { describe, it, expect, beforeEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useCropHandles, type CropBounds } from '@/hooks/useCropHandles'; describe('useCropHandles Hook', () => { const imageDimensions = { width: 800, height: 600 }; const initialCrop: CropBounds = { x: 100, y: 100, width: 200, height: 200 }; describe('Initialization', () => { it('should initialize with null crop when no initialCrop provided', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions }) ); expect(result.current.crop).toBeNull(); expect(result.current.isDragging).toBe(false); }); it('should initialize with provided initialCrop', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); expect(result.current.crop).toEqual(initialCrop); }); it('should enforce minimum size on initialCrop', () => { const tinyBounds: CropBounds = { x: 50, y: 50, width: 10, height: 10 }; const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop: tinyBounds, minSize: 100 }) ); expect(result.current.crop).toEqual({ x: 50, y: 50, width: 100, height: 100, }); }); }); describe('setCrop', () => { it('should set crop bounds', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions }) ); const newBounds: CropBounds = { x: 50, y: 50, width: 300, height: 300 }; act(() => { result.current.setCrop(newBounds); }); expect(result.current.crop).toEqual(newBounds); }); it('should clear crop with null', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.setCrop(null); }); expect(result.current.crop).toBeNull(); }); it('should constrain bounds within image', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions }) ); const outOfBounds: CropBounds = { x: 600, y: 400, width: 400, height: 400 }; act(() => { result.current.setCrop(outOfBounds); }); expect(result.current.crop!.x).toBeLessThanOrEqual(imageDimensions.width - result.current.crop!.width); expect(result.current.crop!.y).toBeLessThanOrEqual(imageDimensions.height - result.current.crop!.height); }); it('should enforce minimum size', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, minSize: 100 }) ); const smallBounds: CropBounds = { x: 50, y: 50, width: 20, height: 20 }; act(() => { result.current.setCrop(smallBounds); }); expect(result.current.crop!.width).toBeGreaterThanOrEqual(100); expect(result.current.crop!.height).toBeGreaterThanOrEqual(100); }); }); describe('resetCrop', () => { it('should clear crop bounds', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); expect(result.current.crop).not.toBeNull(); act(() => { result.current.resetCrop(); }); expect(result.current.crop).toBeNull(); }); }); describe('Drag Operations - Corner Handles', () => { it('should drag top-left corner inward', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('top-left', 100, 100); }); expect(result.current.isDragging).toBe(true); act(() => { result.current.moveDrag(120, 120); }); expect(result.current.crop!.x).toBe(120); expect(result.current.crop!.y).toBe(120); expect(result.current.crop!.width).toBe(180); expect(result.current.crop!.height).toBe(180); }); it('should drag top-right corner', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('top-right', 300, 100); }); act(() => { result.current.moveDrag(350, 130); }); expect(result.current.crop!.x).toBe(100); expect(result.current.crop!.y).toBe(130); expect(result.current.crop!.width).toBe(250); expect(result.current.crop!.height).toBe(170); }); it('should drag bottom-right corner', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('bottom-right', 300, 300); }); act(() => { result.current.moveDrag(350, 380); }); expect(result.current.crop!.x).toBe(100); expect(result.current.crop!.y).toBe(100); expect(result.current.crop!.width).toBe(250); expect(result.current.crop!.height).toBe(280); }); it('should drag bottom-left corner', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('bottom-left', 100, 300); }); act(() => { result.current.moveDrag(140, 380); }); expect(result.current.crop!.x).toBe(140); expect(result.current.crop!.y).toBe(100); expect(result.current.crop!.width).toBe(160); expect(result.current.crop!.height).toBe(280); }); }); describe('Drag Operations - Edge Handles', () => { it('should drag top edge', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('top', 200, 100); }); act(() => { result.current.moveDrag(200, 130); }); expect(result.current.crop!.x).toBe(100); expect(result.current.crop!.y).toBe(130); expect(result.current.crop!.width).toBe(200); expect(result.current.crop!.height).toBe(170); }); it('should drag right edge', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('right', 300, 200); }); act(() => { result.current.moveDrag(380, 200); }); expect(result.current.crop!.x).toBe(100); expect(result.current.crop!.y).toBe(100); expect(result.current.crop!.width).toBe(280); expect(result.current.crop!.height).toBe(200); }); it('should drag bottom edge', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('bottom', 200, 300); }); act(() => { result.current.moveDrag(200, 380); }); expect(result.current.crop!.x).toBe(100); expect(result.current.crop!.y).toBe(100); expect(result.current.crop!.width).toBe(200); expect(result.current.crop!.height).toBe(280); }); it('should drag left edge', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('left', 100, 200); }); act(() => { result.current.moveDrag(140, 200); }); expect(result.current.crop!.x).toBe(140); expect(result.current.crop!.y).toBe(100); expect(result.current.crop!.width).toBe(160); expect(result.current.crop!.height).toBe(200); }); }); describe('Drag Constraints', () => { it('should not allow dragging outside left boundary', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('left', 100, 200); }); act(() => { result.current.moveDrag(-100, 200); // Try to drag far left }); expect(result.current.crop!.x).toBeGreaterThanOrEqual(0); }); it('should not allow dragging outside right boundary', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('right', 300, 200); }); act(() => { result.current.moveDrag(900, 200); // Try to drag far right }); const { crop } = result.current; expect(crop!.x + crop!.width).toBeLessThanOrEqual(imageDimensions.width); }); it('should not allow dragging outside top boundary', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('top', 200, 100); }); act(() => { result.current.moveDrag(200, -100); // Try to drag far up }); expect(result.current.crop!.y).toBeGreaterThanOrEqual(0); }); it('should not allow dragging outside bottom boundary', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('bottom', 200, 300); }); act(() => { result.current.moveDrag(200, 800); // Try to drag far down }); const { crop } = result.current; expect(crop!.y + crop!.height).toBeLessThanOrEqual(imageDimensions.height); }); it('should not allow crop smaller than minSize', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop, minSize: 100 }) ); act(() => { result.current.startDrag('bottom-right', 300, 300); }); act(() => { result.current.moveDrag(180, 180); // Try to make tiny crop }); expect(result.current.crop!.width).toBeGreaterThanOrEqual(100); expect(result.current.crop!.height).toBeGreaterThanOrEqual(100); }); }); describe('Drag End', () => { it('should end drag and set isDragging to false', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('top-left', 100, 100); }); expect(result.current.isDragging).toBe(true); act(() => { result.current.moveDrag(120, 120); }); act(() => { result.current.endDrag(); }); expect(result.current.isDragging).toBe(false); }); it('should preserve crop bounds after endDrag', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); act(() => { result.current.startDrag('right', 300, 200); }); act(() => { result.current.moveDrag(350, 200); }); const boundsBeforeEnd = { ...result.current.crop! }; act(() => { result.current.endDrag(); }); expect(result.current.crop).toEqual(boundsBeforeEnd); }); }); describe('Edge Cases', () => { it('should not crash when dragging without starting', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, initialCrop }) ); expect(() => { act(() => { result.current.moveDrag(150, 150); }); }).not.toThrow(); }); it('should handle very large image dimensions', () => { const largeDimensions = { width: 10000, height: 8000 }; const { result } = renderHook(() => useCropHandles({ imageDimensions: largeDimensions, initialCrop }) ); act(() => { result.current.startDrag('bottom-right', 300, 300); }); act(() => { result.current.moveDrag(5000, 4000); }); expect(result.current.crop).toBeDefined(); expect(result.current.crop!.x + result.current.crop!.width).toBeLessThanOrEqual(largeDimensions.width); }); it('should handle custom minimum size', () => { const { result } = renderHook(() => useCropHandles({ imageDimensions, minSize: 50 }) ); const smallBounds: CropBounds = { x: 10, y: 10, width: 20, height: 20 }; act(() => { result.current.setCrop(smallBounds); }); expect(result.current.crop!.width).toBeGreaterThanOrEqual(50); expect(result.current.crop!.height).toBeGreaterThanOrEqual(50); }); }); });