import { renderHook, act, waitFor } from '@testing-library/react'; import type { RefObject } from 'react'; import { useUniformCenteredConfig, useSpotlightLayout, useSpotlightHeight, useSpotlightNavigation, } from '../hooks'; import { partialItemWidth, infiniteScrollTimeout, } from '../../../../consts'; import { useCarouselDimensions } from '../../../../../hooks/useCarouselDimensions'; import type { CarouselDimensions } from '../../../../../hooks/useCarouselDimensions'; import { useCompactDesktopWidth, getCompactContainerStyle, } from '../../../../../hooks/useCompactDesktopWidth'; import { useCarouselHeight } from '../../../../../hooks/useCarouselHeight'; import { useCarouselMinHeight } from '../../../../../hooks/useCarouselMinHeight'; jest.mock('../../../../../hooks/useCarouselDimensions', () => ({ useCarouselDimensions: jest.fn(), })); jest.mock('../../../../../hooks/useCompactDesktopWidth', () => ({ useCompactDesktopWidth: jest.fn(), getCompactContainerStyle: jest.fn(), })); jest.mock('../../../../../hooks/useCarouselHeight', () => ({ useCarouselHeight: jest.fn(), })); jest.mock('../../../../../hooks/useCarouselMinHeight', () => ({ useCarouselMinHeight: jest.fn(), })); type StoreStateSlice = { productListUniformMode?: boolean; currentPosition?: number; }; const createStoreSetter = (initialState: StoreStateSlice) => { let state = { ...initialState }; const setStoreState = jest.fn((updater: ((s: StoreStateSlice) => StoreStateSlice) | StoreStateSlice) => { state = typeof updater === 'function' ? updater(state) : updater; return state; }); return { setStoreState, getState: () => state }; }; const createSlideRef = (width: number) => { const element = document.createElement('div'); Object.defineProperty(element, 'clientWidth', { value: width, configurable: true, }); return { current: element } as RefObject; }; const createContainerRef = () => { const element = document.createElement('div'); return { current: element } as RefObject; }; const mockedUseCarouselDimensions = useCarouselDimensions as jest.MockedFunction; const mockedUseCompactDesktopWidth = useCompactDesktopWidth as jest.MockedFunction; const mockedGetCompactContainerStyle = getCompactContainerStyle as jest.MockedFunction; const mockedUseCarouselHeight = useCarouselHeight as jest.MockedFunction; const mockedUseCarouselMinHeight = useCarouselMinHeight as jest.MockedFunction; describe('SpotlightCarousel hooks', () => { beforeAll(() => { window.requestAnimationFrame = (callback: FrameRequestCallback): number => window.setTimeout(() => callback(0), 0); }); beforeEach(() => { jest.clearAllMocks(); }); describe('useUniformCenteredConfig', () => { it('disables partial width and cloning when content size is not reached', async () => { const { setStoreState, getState } = createStoreSetter({ productListUniformMode: false }); const { result } = renderHook(() => useUniformCenteredConfig(2, 4, setStoreState as unknown as (value: any) => void), ); expect(result.current.contentSizeNotReached).toBe(true); expect(result.current.partialItemWidth).toBe(0); expect(result.current.cloneCount).toBe(0); await waitFor(() => { expect(getState().productListUniformMode).toBe(true); }); }); it('enables cloning when there are enough items', () => { const { result } = renderHook(() => useUniformCenteredConfig(5, 4, jest.fn()), ); expect(result.current.contentSizeNotReached).toBe(false); expect(result.current.partialItemWidth).toBe(partialItemWidth); expect(result.current.cloneCount).toBe(4); }); }); describe('useSpotlightLayout', () => { it('forces medium arrows in compact mode and returns compact styles', () => { const mockDimensions: CarouselDimensions = { containerWidth: 800, arrowSize: 'small', arrowSpace: 0, contentWidth: 700, itemWidth: 140, }; mockedUseCarouselDimensions.mockReturnValue(mockDimensions); mockedUseCompactDesktopWidth.mockReturnValue({ width: 520, isCompact: true }); mockedGetCompactContainerStyle.mockReturnValue({ maxWidth: '520px' }); const slideRef = createSlideRef(600); const { result } = renderHook(() => useSpotlightLayout({ context: { widgetWidth: 900, contentSize: 4, isMobile: false, hideArrows: false, }, config: { contentSize: 4, itemCount: 5, partialItemWidth: 0.4 }, refs: { slideRef }, }), ); expect(result.current.arrowSize).toBe('medium'); expect(result.current.carouselDimensions).toBe(mockDimensions); expect(result.current.compactStyle).toEqual({ maxWidth: '520px' }); expect(mockedUseCarouselDimensions).toHaveBeenCalledWith(expect.objectContaining({ forcedArrowSize: 'medium' })); }); it('uses calculated arrow size when not compact', () => { const nonCompactDimensions: CarouselDimensions = { containerWidth: 700, arrowSize: 'large', arrowSpace: 0, contentWidth: 600, itemWidth: 150, }; mockedUseCarouselDimensions.mockReturnValue(nonCompactDimensions); mockedUseCompactDesktopWidth.mockReturnValue({ width: 700, isCompact: false }); mockedGetCompactContainerStyle.mockReturnValue(undefined); const slideRef = createSlideRef(600); const { result } = renderHook(() => useSpotlightLayout({ context: { widgetWidth: 700, contentSize: 5, isMobile: false, hideArrows: false, }, config: { contentSize: 5, itemCount: 5, partialItemWidth: 0.4 }, refs: { slideRef }, }), ); expect(result.current.arrowSize).toBe('large'); expect(result.current.compactStyle).toBeUndefined(); expect(mockedUseCarouselDimensions).toHaveBeenCalledWith(expect.objectContaining({ forcedArrowSize: undefined })); }); }); describe('useSpotlightHeight', () => { it('returns enforced minimum height from useCarouselMinHeight', () => { mockedUseCarouselHeight.mockReturnValue(320); mockedUseCarouselMinHeight.mockReturnValue(360); const outerRef = createContainerRef(); const { result } = renderHook(() => useSpotlightHeight({ refs: { outerRef }, config: { thumbnailShape: 'square', contentSize: 3, itemCount: 3 }, layout: { isCompact: false, constrainedWidgetWidth: 900, widgetWidth: 900 }, }), ); expect(result.current.enforcedMinHeight).toBe(360); expect(mockedUseCarouselHeight).toHaveBeenCalledWith(expect.objectContaining({ partialItemWidth })); expect(mockedUseCarouselMinHeight).toHaveBeenCalledWith( outerRef, 320, true, '3-900', ); }); }); describe('useSpotlightNavigation', () => { afterEach(() => { jest.useRealTimers(); }); it('prevents navigation when content size is not reached', () => { const slideRef = createSlideRef(600); const containerRef = createContainerRef(); const { setStoreState } = createStoreSetter({ currentPosition: 0 }); const { result } = renderHook(() => useSpotlightNavigation({ config: { itemCount: 2, contentSize: 4, cloneCount: 0, contentSizeNotReached: true }, refs: { slideRef, containerRef }, state: { itemWidth: 150, currentPosition: 0 }, actions: { setStoreState: setStoreState as unknown as (value: any) => void }, }), ); act(() => { result.current.handleSlide('right'); }); expect(result.current.activeIndex).toBe(0); expect(setStoreState).not.toHaveBeenCalled(); }); it('cycles through clones and updates store state after transition', () => { jest.useFakeTimers(); const slideRef = createSlideRef(600); const containerRef = createContainerRef(); const store = createStoreSetter({ currentPosition: 0 }); const { result } = renderHook(() => useSpotlightNavigation({ config: { itemCount: 3, contentSize: 3, cloneCount: 3, contentSizeNotReached: false }, refs: { slideRef, containerRef }, state: { itemWidth: 150, currentPosition: undefined }, actions: { setStoreState: store.setStoreState as unknown as (value: any) => void }, }), ); act(() => { result.current.handleSlide('right'); }); expect(result.current.isTransitioning).toBe(true); act(() => { jest.advanceTimersByTime(infiniteScrollTimeout + 5); }); expect(result.current.activeIndex).toBe(1); expect(store.getState().currentPosition).toBe(1); }); }); });