import { renderHook, act } from '@testing-library/react'; import { RefObject } from 'react'; import useAdjacentCardWidth from '../useAdjacentCardWidth'; import { IMedia } from '../../types'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const makePost = (id: string, image = 'https://cdn.example.com/img.jpg'): IMedia => ({ id, type: 'image', caption: '', source: 'instagram', post: `https://www.instagram.com/p/${id}`, image, productsLinked: {}, postIndex: 0, }); const makeContainerRef = ( clientWidth: number, clientHeight: number, ): RefObject => ({ current: { clientWidth, clientHeight } as HTMLDivElement, }); // --------------------------------------------------------------------------- // Image mock — same pattern as useMediaCardWidth tests. // We intercept window.Image so we can control naturalWidth/Height and fire // onload/onerror manually without a real network request. // --------------------------------------------------------------------------- interface MockImage { onload: (() => void) | null; onerror: (() => void) | null; src: string; naturalWidth: number; naturalHeight: number; } let capturedImages: MockImage[] = []; beforeEach(() => { capturedImages = []; (global as any).Image = jest.fn().mockImplementation(() => { const img: MockImage = { onload: null, onerror: null, src: '', naturalWidth: 0, naturalHeight: 0, }; capturedImages.push(img); return img; }); }); afterEach(() => { jest.restoreAllMocks(); }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('useAdjacentCardWidth', () => { const containerRef = makeContainerRef(1000, 800); const handleNextPost = jest.fn(); const handlePreviousPost = jest.fn(); const defaultProps = { containerRef, nextPost: null, prevPost: null, currentCardWidth: 'auto', handleNextPost, handlePreviousPost, }; beforeEach(() => { jest.clearAllMocks(); }); // ── initial state ────────────────────────────────────────────────────────── it('starts with pendingCardWidth as null', () => { const { result } = renderHook(() => useAdjacentCardWidth(defaultProps)); expect(result.current.pendingCardWidth).toBeNull(); }); // ── precompute on mount / post change ───────────────────────────────────── it('pre-loads images for both adjacent posts on mount', () => { const next = makePost('next-1'); const prev = makePost('prev-1'); renderHook(() => useAdjacentCardWidth({ ...defaultProps, nextPost: next, prevPost: prev }), ); // Two Image instances should have been created (one per adjacent post). expect(capturedImages).toHaveLength(2); const srcs = capturedImages.map((img) => img.src); expect(srcs).toContain(next.image); expect(srcs).toContain(prev.image); }); it('does not create an Image when a post has no image or thumbnail', () => { const noImage = makePost('no-img', undefined as unknown as string); noImage.image = undefined; noImage.thumbnail = undefined; renderHook(() => useAdjacentCardWidth({ ...defaultProps, nextPost: noImage }), ); expect(capturedImages).toHaveLength(0); }); it('re-runs precompute when adjacent posts change', () => { const next1 = makePost('next-1'); const next2 = makePost('next-2'); const { rerender } = renderHook( ({ nextPost }) => useAdjacentCardWidth({ ...defaultProps, nextPost }), { initialProps: { nextPost: next1 } }, ); const firstCount = capturedImages.length; rerender({ nextPost: next2 }); expect(capturedImages.length).toBeGreaterThan(firstCount); const newSrc = capturedImages[capturedImages.length - 1].src; expect(newSrc).toBe(next2.image); }); // ── width computation ────────────────────────────────────────────────────── it('stores the computed px width after onload fires', () => { const next = makePost('next-compute'); const { result, rerender } = renderHook( ({ nextPost }) => useAdjacentCardWidth({ ...defaultProps, containerRef, nextPost }), { initialProps: { nextPost: next } }, ); // Fire onload with a 2:1 landscape image. // Container: 1000w × 800h → natural card width = 800 * 2 = 1600, capped at 1000*0.8 = 800px. act(() => { const img = capturedImages.find((i) => i.src === next.image)!; img.naturalWidth = 1600; img.naturalHeight = 800; img.onload!(); }); // Now navigate to next → pendingCardWidth should be 800px. act(() => { result.current.handleNextWithWidth(); }); expect(result.current.pendingCardWidth).toBe('800px'); expect(handleNextPost).toHaveBeenCalledTimes(1); }); it('stores "auto" when the image fails to load', () => { const next = makePost('next-error'); const { result } = renderHook(() => useAdjacentCardWidth({ ...defaultProps, nextPost: next }), ); act(() => { const img = capturedImages.find((i) => i.src === next.image)!; img.onerror!(); }); act(() => { result.current.handleNextWithWidth(); }); expect(result.current.pendingCardWidth).toBe('auto'); }); it('stores "auto" when the container has zero dimensions', () => { const zeroRef = makeContainerRef(0, 0); const next = makePost('next-zero'); const { result } = renderHook(() => useAdjacentCardWidth({ ...defaultProps, containerRef: zeroRef, nextPost: next }), ); act(() => { const img = capturedImages.find((i) => i.src === next.image)!; img.naturalWidth = 800; img.naturalHeight = 600; img.onload!(); }); act(() => { result.current.handleNextWithWidth(); }); expect(result.current.pendingCardWidth).toBe('auto'); }); it('respects a custom maxWidthRatio', () => { const next = makePost('next-ratio'); const { result } = renderHook(() => useAdjacentCardWidth({ ...defaultProps, containerRef: makeContainerRef(1000, 800), nextPost: next, maxWidthRatio: 0.5, }), ); // 1:1 image → natural card width = 800px, capped at 1000 * 0.5 = 500px. act(() => { const img = capturedImages.find((i) => i.src === next.image)!; img.naturalWidth = 800; img.naturalHeight = 800; img.onload!(); }); act(() => { result.current.handleNextWithWidth(); }); expect(result.current.pendingCardWidth).toBe('500px'); }); // ── handleNextWithWidth ─────────────────────────────────────────────────── it('calls handleNextPost even when no width is precomputed', () => { const { result } = renderHook(() => useAdjacentCardWidth(defaultProps)); act(() => { result.current.handleNextWithWidth(); }); expect(handleNextPost).toHaveBeenCalledTimes(1); expect(result.current.pendingCardWidth).toBeNull(); }); it('sets pendingCardWidth to null when nextPost is null', () => { const { result } = renderHook(() => useAdjacentCardWidth({ ...defaultProps, nextPost: null }), ); act(() => { result.current.handleNextWithWidth(); }); expect(result.current.pendingCardWidth).toBeNull(); }); // ── handlePrevWithWidth ─────────────────────────────────────────────────── it('sets pendingCardWidth for the previous post and calls handlePreviousPost', () => { const prev = makePost('prev-compute'); const { result } = renderHook(() => useAdjacentCardWidth({ ...defaultProps, prevPost: prev }), ); // 1:1 image → natural = 800px, capped at 800px (< 1000 * 0.8 = 800), so exactly 800px. act(() => { const img = capturedImages.find((i) => i.src === prev.image)!; img.naturalWidth = 800; img.naturalHeight = 800; img.onload!(); }); act(() => { result.current.handlePrevWithWidth(); }); expect(result.current.pendingCardWidth).toBe('800px'); expect(handlePreviousPost).toHaveBeenCalledTimes(1); }); it('calls handlePreviousPost even when no width is precomputed', () => { const { result } = renderHook(() => useAdjacentCardWidth(defaultProps)); act(() => { result.current.handlePrevWithWidth(); }); expect(handlePreviousPost).toHaveBeenCalledTimes(1); expect(result.current.pendingCardWidth).toBeNull(); }); // ── pendingCardWidth reset ──────────────────────────────────────────────── it('clears pendingCardWidth when currentCardWidth changes', () => { const next = makePost('next-reset'); const { result, rerender } = renderHook( ({ currentCardWidth }) => useAdjacentCardWidth({ ...defaultProps, nextPost: next, currentCardWidth }), { initialProps: { currentCardWidth: 'auto' } }, ); act(() => { const img = capturedImages.find((i) => i.src === next.image)!; img.naturalWidth = 400; img.naturalHeight = 800; img.onload!(); }); act(() => { result.current.handleNextWithWidth(); }); expect(result.current.pendingCardWidth).not.toBeNull(); // Simulate useMediaCardWidth settling on the real width. rerender({ currentCardWidth: '400px' }); expect(result.current.pendingCardWidth).toBeNull(); }); });