import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useCarousel } from '../useCarousel'; describe('useCarousel', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.runOnlyPendingTimers(); jest.useRealTimers(); }); it('initializes with correct default values', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); expect(result.current.currentPage).toBe(0); expect(result.current.totalPages).toBe(5); expect(result.current.isScrolling).toBe(false); }); it('calculates correct totalPages', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 3 })); expect(result.current.totalPages).toBe(4); }); it('provides navigation functions', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); expect(typeof result.current.handleNext).toBe('function'); expect(typeof result.current.handlePrevious).toBe('function'); expect(typeof result.current.scrollToPage).toBe('function'); }); it('provides mouse event handlers for drag functionality', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); expect(typeof result.current.handleMouseDown).toBe('function'); expect(typeof result.current.handleMouseUp).toBe('function'); expect(typeof result.current.handleMouseLeave).toBe('function'); }); it('handles single page correctly', () => { const { result } = renderHook(() => useCarousel(2, { itemsPerPage: 2 })); expect(result.current.totalPages).toBe(1); act(() => { result.current.handleNext(); }); expect(result.current.currentPage).toBe(0); }); it('handles empty items', () => { const { result } = renderHook(() => useCarousel(0, { itemsPerPage: 2 })); expect(result.current.totalPages).toBe(0); expect(result.current.currentPage).toBe(0); }); it('updates totalPages when totalItems changes', () => { const { result, rerender } = renderHook( ({ total }) => useCarousel(total, { itemsPerPage: 2 }), { initialProps: { total: 10 } } ); expect(result.current.totalPages).toBe(5); rerender({ total: 6 }); expect(result.current.totalPages).toBe(3); }); it('cleans up timeout on unmount', () => { const { unmount } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); unmount(); expect(jest.getTimerCount()).toBe(0); }); it('defaults to itemsPerPage of 2 when not provided', () => { const { result } = renderHook(() => useCarousel(10, {})); expect(result.current.totalPages).toBe(5); }); it('handles itemsPerPage of 1', () => { const { result } = renderHook(() => useCarousel(5, { itemsPerPage: 1 })); expect(result.current.totalPages).toBe(5); }); it('handles large number of items', () => { const { result } = renderHook(() => useCarousel(1000, { itemsPerPage: 10 }) ); expect(result.current.totalPages).toBe(100); }); it('handles odd number of items with even itemsPerPage', () => { const { result } = renderHook(() => useCarousel(5, { itemsPerPage: 2 })); expect(result.current.totalPages).toBe(3); }); it('provides carouselRef', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); expect(result.current.carouselRef).toBeDefined(); expect(typeof result.current.carouselRef).toBe('object'); }); it('updates when itemsPerPage option changes', () => { const { result, rerender } = renderHook( ({ itemsPerPage }) => useCarousel(10, { itemsPerPage }), { initialProps: { itemsPerPage: 2 } } ); expect(result.current.totalPages).toBe(5); rerender({ itemsPerPage: 5 }); expect(result.current.totalPages).toBe(2); }); it('handles itemsPerPage greater than total items', () => { const { result } = renderHook(() => useCarousel(3, { itemsPerPage: 10 })); expect(result.current.totalPages).toBe(1); }); it('calls onPageChange when provided in options', () => { const onPageChange = jest.fn(); const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2, onPageChange }) ); expect(result.current.currentPage).toBe(0); expect(onPageChange).not.toHaveBeenCalled(); }); it('handles mouseDown without carouselRef', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockEvent = ({ preventDefault: jest.fn(), pageX: 100 } as unknown) as React.MouseEvent; expect(result.current.carouselRef.current).toBeNull(); act(() => { result.current.handleMouseDown(mockEvent); }); expect(mockEvent.preventDefault).not.toHaveBeenCalled(); }); it('calls onPageChange when scrolling to a page', () => { const onPageChange = jest.fn(); const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2, onPageChange }) ); const mockCard1: Partial = { offsetLeft: 0 }; const mockCard2: Partial = { offsetLeft: 400 }; const mockChildren = [ mockCard1, mockCard2, mockCard1, mockCard2, mockCard1 ]; const mockCarousel = { scrollLeft: 0, children: mockChildren, scrollTo: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.scrollToPage(1); jest.runAllTimers(); }); expect(onPageChange).toHaveBeenCalledWith(1); expect(mockCarousel.scrollTo).toHaveBeenCalled(); }); it('returns undefined cleanup when carousel is not mounted', () => { const { result, unmount } = renderHook(() => useCarousel(10, { itemsPerPage: 2 }) ); expect(result.current.carouselRef.current).toBeNull(); unmount(); }); it('handles handleMouseUp when not dragging', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockEvent = ({ pageX: 100 } as unknown) as React.MouseEvent; act(() => { result.current.handleMouseUp(mockEvent); }); expect(result.current.currentPage).toBe(0); }); it('handles handleMouseLeave when not dragging', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockEvent = ({ pageX: 100 } as unknown) as React.MouseEvent; act(() => { result.current.handleMouseLeave(mockEvent); }); expect(result.current.currentPage).toBe(0); }); it('handles complete mouse drag interaction', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCarousel = { scrollLeft: 0, children: [], scrollTo: jest.fn(), style: { cursor: 'grab' }, addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; const mouseDownEvent = ({ preventDefault: jest.fn(), pageX: 200 } as unknown) as React.MouseEvent; const mouseUpEvent = ({ pageX: 100 } as unknown) as React.MouseEvent; act(() => { result.current.handleMouseDown(mouseDownEvent); }); expect(mouseDownEvent.preventDefault).toHaveBeenCalled(); expect(mockCarousel.style.cursor).toBe('grabbing'); act(() => { result.current.handleMouseUp(mouseUpEvent); }); expect(mockCarousel.style.cursor).toBe('grab'); }); it('handles mouseUp with small drag distance', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCarousel = { scrollLeft: 0, children: [], scrollTo: jest.fn(), style: { cursor: 'grab' }, addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleMouseDown(({ preventDefault: jest.fn(), pageX: 100 } as unknown) as React.MouseEvent); }); act(() => { result.current.handleMouseUp(({ pageX: 90 } as unknown) as React.MouseEvent); }); expect(result.current.currentPage).toBe(0); }); it('handles mouseLeave during active drag', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCarousel = { scrollLeft: 0, children: [], scrollTo: jest.fn(), style: { cursor: 'grab' }, addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleMouseDown(({ preventDefault: jest.fn(), pageX: 100 } as unknown) as React.MouseEvent); }); expect(mockCarousel.style.cursor).toBe('grabbing'); const mouseLeaveEvent = ({ pageX: 120 } as unknown) as React.MouseEvent; act(() => { result.current.handleMouseLeave(mouseLeaveEvent); }); expect(mockCarousel.style.cursor).toBe('grab'); }); it('scrollToPage handles case with no children', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCarousel = { children: [], scrollTo: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.scrollToPage(1); }); expect(mockCarousel.scrollTo).not.toHaveBeenCalled(); }); it('handlePrevious and handleNext navigate correctly', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCards = Array.from({ length: 5 }, (_, i) => ({ offsetLeft: i * 200 })) as HTMLElement[]; const mockCarousel = { children: mockCards, scrollTo: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleNext(); jest.runAllTimers(); }); expect(result.current.currentPage).toBe(1); act(() => { result.current.handlePrevious(); jest.runAllTimers(); }); expect(result.current.currentPage).toBe(0); }); it('drag triggers navigation', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCards = Array.from({ length: 5 }, (_, i) => ({ offsetLeft: i * 200 })) as HTMLElement[]; const mockCarousel = { children: mockCards, scrollTo: jest.fn(), style: { cursor: 'grab' }, addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleMouseDown(({ preventDefault: jest.fn(), pageX: 200 } as unknown) as React.MouseEvent); }); act(() => { result.current.handleMouseUp(({ pageX: 100 } as unknown) as React.MouseEvent); }); act(() => { jest.runAllTimers(); }); expect(result.current.currentPage).toBe(1); }); it('provides touch event handlers', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); expect(typeof result.current.handleTouchStart).toBe('function'); expect(typeof result.current.handleTouchMove).toBe('function'); expect(typeof result.current.handleTouchEnd).toBe('function'); }); it('handles touch swipe right to left (next)', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCards = Array.from({ length: 5 }, (_, i) => ({ offsetLeft: i * 200 })) as HTMLElement[]; const mockCarousel = { children: mockCards, scrollTo: jest.fn(options => { if (typeof options === 'object' && options.left !== undefined) { mockCarousel.scrollLeft = options.left; } }), addEventListener: jest.fn(), removeEventListener: jest.fn(), scrollLeft: 0 }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleTouchStart(({ touches: [{ clientX: 200, clientY: 0 }] } as unknown) as React.TouchEvent); }); act(() => { result.current.handleTouchMove(({ touches: [{ clientX: 140, clientY: 0 }], preventDefault: jest.fn() } as unknown) as React.TouchEvent); }); act(() => { jest.advanceTimersByTime(400); // Wait for scroll interval }); act(() => { jest.advanceTimersByTime(50); }); expect(result.current.currentPage).toBe(1); act(() => { result.current.handleTouchEnd(); }); }); it('handles touch swipe left to right (previous)', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCards = Array.from({ length: 5 }, (_, i) => ({ offsetLeft: i * 200 })) as HTMLElement[]; const mockCarousel = { children: mockCards, scrollTo: jest.fn(options => { if (typeof options === 'object' && options.left !== undefined) { mockCarousel.scrollLeft = options.left; } }), addEventListener: jest.fn(), removeEventListener: jest.fn(), scrollLeft: 0 }; (result.current.carouselRef as any).current = mockCarousel; // First navigate to page 1 act(() => { result.current.scrollToPage(1); jest.runAllTimers(); }); expect(result.current.currentPage).toBe(1); // Then swipe left to right (previous) act(() => { result.current.handleTouchStart(({ touches: [{ clientX: 100, clientY: 0 }] } as unknown) as React.TouchEvent); }); act(() => { result.current.handleTouchMove(({ touches: [{ clientX: 160, clientY: 0 }], preventDefault: jest.fn() } as unknown) as React.TouchEvent); }); act(() => { jest.advanceTimersByTime(400); // Wait for scroll interval }); // Advance time for the debounce in handleScroll act(() => { jest.advanceTimersByTime(50); }); expect(result.current.currentPage).toBe(0); act(() => { result.current.handleTouchEnd(); }); }); it('handles touch swipe with small distance (no navigation)', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCarousel = { children: [], scrollTo: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleTouchStart(({ touches: [{ clientX: 100, clientY: 0 }] } as unknown) as React.TouchEvent); }); act(() => { result.current.handleTouchMove(({ touches: [{ clientX: 115, clientY: 0 }] } as unknown) as React.TouchEvent); }); expect(result.current.currentPage).toBe(0); act(() => { result.current.handleTouchEnd(); }); }); it('ignores touch move when no touch start', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCarousel = { children: [], scrollTo: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleTouchMove(({ touches: [{ clientX: 150, clientY: 0 }] } as unknown) as React.TouchEvent); }); expect(result.current.currentPage).toBe(0); }); it('ignores touch move when already swiping', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); const mockCards = Array.from({ length: 5 }, (_, i) => ({ offsetLeft: i * 200 })) as HTMLElement[]; const mockCarousel = { children: mockCards, scrollTo: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn() }; (result.current.carouselRef as any).current = mockCarousel; act(() => { result.current.handleTouchStart(({ touches: [{ clientX: 200, clientY: 0 }] } as unknown) as React.TouchEvent); }); act(() => { result.current.handleTouchMove(({ touches: [{ clientX: 150, clientY: 0 }], preventDefault: jest.fn() } as unknown) as React.TouchEvent); }); // Second touch move should be ignored act(() => { result.current.handleTouchMove(({ touches: [{ clientX: 100, clientY: 0 }], preventDefault: jest.fn() } as unknown) as React.TouchEvent); }); act(() => { jest.runAllTimers(); }); expect(result.current.currentPage).toBe(1); }); it('handles touch start without carousel ref', () => { const { result } = renderHook(() => useCarousel(10, { itemsPerPage: 2 })); (result.current.carouselRef as any).current = null; act(() => { result.current.handleTouchStart(({ touches: [{ clientX: 100, clientY: 0 }] } as unknown) as React.TouchEvent); }); expect(result.current.currentPage).toBe(0); }); });