import { renderHook, act } from '@testing-library/react'; import React, { RefObject, ReactNode, createElement } from 'react'; import { useProductCarousel } from '../useProductCarousel'; import { StoreContext } from '../../services/store'; import { IProduct } from '../../types'; // Mock the productCarouselGap constant jest.mock('../../components/consts', () => ({ productCarouselGap: 15, })); describe('useProductCarousel', () => { let mockScrollContainerRef: RefObject; let mockContainerRef: RefObject; let mockProducts: IProduct[]; let mockStoreContext: any; beforeEach(() => { // Mock DOM elements const mockScrollContainer = { clientWidth: 400, } as HTMLDivElement; const mockContainer = { clientWidth: 400, } as HTMLDivElement; mockScrollContainerRef = { current: mockScrollContainer, }; mockContainerRef = { current: mockContainer, }; // Mock products mockProducts = [ { id: '1', catalog_id: 'cat1', company_id: 'comp1', description: 'Product 1', image_link: 'image1.jpg', link: 'link1', title: 'Product 1', updated_at: '2024-01-01', }, { id: '2', catalog_id: 'cat2', company_id: 'comp2', description: 'Product 2', image_link: 'image2.jpg', link: 'link2', title: 'Product 2', updated_at: '2024-01-02', }, { id: '3', catalog_id: 'cat3', company_id: 'comp3', description: 'Product 3', image_link: 'image3.jpg', link: 'link3', title: 'Product 3', updated_at: '2024-01-03', }, { id: '4', catalog_id: 'cat4', company_id: 'comp4', description: 'Product 4', image_link: 'image4.jpg', link: 'link4', title: 'Product 4', updated_at: '2024-01-04', }, { id: '5', catalog_id: 'cat5', company_id: 'comp5', description: 'Product 5', image_link: 'image5.jpg', link: 'link5', title: 'Product 5', updated_at: '2024-01-05', }, ]; // Mock store context mockStoreContext = { getProductImageWidth: jest.fn((id: string) => { // Return larger widths so total content is wider than container const widths: { [key: string]: number } = { '1': 200, '2': 250, '3': 180, '4': 220, '5': 190, }; return widths[id] || 0; }), }; jest.clearAllMocks(); jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); const renderHookWithContext = (props: any) => { const wrapper = ({ children }: { children: ReactNode }) => createElement(StoreContext.Provider, { value: mockStoreContext }, children); return renderHook( () => useProductCarousel({ products: props.products || mockProducts, containerRef: mockContainerRef, scrollContainerRef: mockScrollContainerRef, }), { wrapper }, ); }; describe('Initialization', () => { it('initializes with correct default values', () => { const { result } = renderHookWithContext({}); expect(result.current.scrollPosition).toBe(0); expect(result.current.canScrollPrev).toBe(false); expect(result.current.canScrollNext).toBe(true); // enableTransition starts as false due to products useEffect, then becomes true expect(result.current.enableTransition).toBe(false); // After timeout, transition should be enabled act(() => { jest.advanceTimersByTime(60); }); expect(result.current.enableTransition).toBe(true); }); it('initializes with disabled transition and enables it after timeout', () => { const { result } = renderHookWithContext({}); // Initially enableTransition should be false due to products useEffect expect(result.current.enableTransition).toBe(false); // After timeout, transition should be enabled act(() => { jest.advanceTimersByTime(60); }); expect(result.current.enableTransition).toBe(true); }); }); describe('getItemWidth', () => { it('returns actual image width when available', () => { const { result } = renderHookWithContext({}); expect(result.current.getItemWidth(0)).toBe(200); // Product 1 expect(result.current.getItemWidth(1)).toBe(250); // Product 2 expect(result.current.getItemWidth(2)).toBe(180); // Product 3 expect(result.current.getItemWidth(3)).toBe(220); // Product 4 expect(result.current.getItemWidth(4)).toBe(190); // Product 5 }); it('returns fallback width when image width is not available', () => { mockStoreContext.getProductImageWidth.mockReturnValue(0); const { result } = renderHookWithContext({}); const fallbackWidth = Math.min(400 * 0.4, 200); // 160px expect(result.current.getItemWidth(0)).toBe(fallbackWidth); }); it('handles invalid product index gracefully', () => { const { result } = renderHookWithContext({}); const fallbackWidth = Math.min(400 * 0.4, 200); // 160px expect(result.current.getItemWidth(999)).toBe(fallbackWidth); }); }); describe('getScrollPosition', () => { it('returns current scroll position', () => { const { result } = renderHookWithContext({}); expect(result.current.getScrollPosition()).toBe(0); }); }); describe('Scroll button states', () => { it('disables prev button at the beginning', () => { const { result } = renderHookWithContext({}); expect(result.current.canScrollPrev).toBe(false); expect(result.current.canScrollNext).toBe(true); }); it('enables both buttons when in the middle', () => { const { result } = renderHookWithContext({}); // Scroll to middle position act(() => { result.current.scrollNext(); }); expect(result.current.canScrollPrev).toBe(true); expect(result.current.canScrollNext).toBe(true); }); it('handles single product correctly', () => { const singleProduct = [mockProducts[0]]; const { result } = renderHookWithContext({ products: singleProduct }); expect(result.current.canScrollPrev).toBe(false); expect(result.current.canScrollNext).toBe(false); }); }); describe('scrollNext', () => { it('scrolls to next view based on partially visible items', () => { const { result } = renderHookWithContext({}); const initialPosition = result.current.getScrollPosition(); expect(initialPosition).toBe(0); act(() => { result.current.scrollNext(); }); const newPosition = result.current.getScrollPosition(); expect(newPosition).toBeGreaterThan(initialPosition); expect(result.current.canScrollPrev).toBe(true); }); it('does not scroll beyond maximum position', () => { const { result } = renderHookWithContext({}); // Scroll multiple times to reach the end let previousPosition = -1; let currentPosition = 0; let scrollAttempts = 0; // Keep scrolling until position stops changing or we hit a reasonable limit while (previousPosition !== currentPosition && scrollAttempts < 10) { previousPosition = result.current.getScrollPosition(); act(() => { result.current.scrollNext(); }); currentPosition = result.current.getScrollPosition(); scrollAttempts++; } const finalPosition = result.current.getScrollPosition(); const canScrollNext = result.current.canScrollNext; // Try to scroll one more time - neither position nor state should change act(() => { result.current.scrollNext(); }); expect(result.current.getScrollPosition()).toBe(finalPosition); expect(result.current.canScrollNext).toBe(canScrollNext); }); }); describe('scrollPrev', () => { it('scrolls back by view width', () => { const { result } = renderHookWithContext({}); // First scroll forward act(() => { result.current.scrollNext(); }); const middlePosition = result.current.getScrollPosition(); expect(middlePosition).toBeGreaterThan(0); // Then scroll back act(() => { result.current.scrollPrev(); }); const backPosition = result.current.getScrollPosition(); expect(backPosition).toBeLessThan(middlePosition); }); it('does not scroll before position 0', () => { const { result } = renderHookWithContext({}); expect(result.current.getScrollPosition()).toBe(0); act(() => { result.current.scrollPrev(); }); expect(result.current.getScrollPosition()).toBe(0); expect(result.current.canScrollPrev).toBe(false); }); }); describe('Products change', () => { it('resets scroll position when products change', () => { const { result, rerender } = renderHookWithContext({}); // Scroll to some position act(() => { result.current.scrollNext(); }); expect(result.current.getScrollPosition()).toBeGreaterThan(0); // Change products const newProducts = [ { ...mockProducts[0], id: '4', title: 'New Product', }, ]; const wrapper = ({ children }: { children: ReactNode }) => createElement(StoreContext.Provider, { value: mockStoreContext }, children); const { result: newResult } = renderHook( () => useProductCarousel({ products: newProducts, containerRef: mockContainerRef, scrollContainerRef: mockScrollContainerRef, }), { wrapper }, ); expect(newResult.current.getScrollPosition()).toBe(0); }); it('temporarily disables transition when products change', () => { const { result } = renderHookWithContext({}); // Fast forward to get past initial timeout act(() => { jest.advanceTimersByTime(60); }); expect(result.current.enableTransition).toBe(true); // Change products const newProducts = [mockProducts[0]]; const wrapper = ({ children }: { children: ReactNode }) => createElement(StoreContext.Provider, { value: mockStoreContext }, children); const { result: newResult } = renderHook( () => useProductCarousel({ products: newProducts, containerRef: mockContainerRef, scrollContainerRef: mockScrollContainerRef, }), { wrapper }, ); expect(newResult.current.enableTransition).toBe(false); // Fast forward the timer act(() => { jest.advanceTimersByTime(60); }); expect(newResult.current.enableTransition).toBe(true); }); }); describe('Edge cases', () => { it('handles empty products array', () => { const { result } = renderHookWithContext({ products: [] }); expect(result.current.scrollPosition).toBe(0); expect(result.current.canScrollPrev).toBe(false); expect(result.current.canScrollNext).toBe(false); expect(result.current.getItemWidth(0)).toBeGreaterThan(0); // Should return fallback width }); it('handles null scroll container ref', () => { const nullScrollContainerRef = { current: null }; const wrapper = ({ children }: { children: ReactNode }) => createElement(StoreContext.Provider, { value: mockStoreContext }, children); const { result } = renderHook( () => useProductCarousel({ products: mockProducts, containerRef: mockContainerRef, scrollContainerRef: nullScrollContainerRef, }), { wrapper }, ); expect(result.current.getItemWidth(0)).toBe(200); // Should still use image width from context }); it('handles container with zero width', () => { const zeroWidthScrollContainer = { clientWidth: 0, } as HTMLDivElement; const zeroWidthScrollContainerRef = { current: zeroWidthScrollContainer, }; const wrapper = ({ children }: { children: ReactNode }) => createElement(StoreContext.Provider, { value: mockStoreContext }, children); const { result } = renderHook( () => useProductCarousel({ products: mockProducts, containerRef: mockContainerRef, scrollContainerRef: zeroWidthScrollContainerRef, }), { wrapper }, ); expect(result.current.canScrollNext).toBe(true); }); }); });