import { renderHook, act } from '@testing-library/react'; import useMediaCardWidth from '../useMediaCardWidth'; import { IMedia } from '../../types'; import {RefObject} from 'react'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const makePost = (overrides: Partial = {}): IMedia => ({ id: 'post-1', type: 'image', caption: '', source: 'instagram', post: 'https://www.instagram.com/p/abc', image: 'https://cdn.example.com/image.jpg', productsLinked: {}, postIndex: 0, ...overrides, }); // --------------------------------------------------------------------------- // Image mock // // jsdom's Image implementation never loads URLs, so we intercept the // constructor, capture the last-created instance, and let each test control // naturalWidth / naturalHeight and fire onload / onerror manually. // --------------------------------------------------------------------------- let capturedImage: { onload: (() => void) | null; onerror: (() => void) | null; src: string; naturalWidth: number; naturalHeight: number; }; beforeEach(() => { capturedImage = { onload: null, onerror: null, src: '', naturalWidth: 0, naturalHeight: 0 }; (global as any).Image = jest.fn().mockImplementation(() => capturedImage); }); afterEach(() => { jest.restoreAllMocks(); }); // --------------------------------------------------------------------------- // Helper: attach mock layout dimensions to the containerRef returned by the hook. // The ref starts as null until consumer code would attach it to a DOM node; // we replicate that here, then re-render to trigger the effect. // --------------------------------------------------------------------------- const attachContainer = ( containerRef: RefObject, clientWidth: number, clientHeight: number, ) => { Object.defineProperty(containerRef, 'current', { value: { clientWidth, clientHeight }, writable: true, configurable: true, }); }; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('useMediaCardWidth', () => { it('returns "auto" before the containerRef is attached', () => { const { result } = renderHook(() => useMediaCardWidth(makePost())); // containerRef.current is still null → effect bails out expect(result.current.cardWidth).toBe('auto'); }); it('returns "auto" when the post has no image or thumbnail URL', () => { const post = makePost({ image: undefined, thumbnail: undefined }); const { result } = renderHook(() => useMediaCardWidth(post)); attachContainer(result.current.containerRef, 1000, 600); expect(result.current.cardWidth).toBe('auto'); }); it('returns "auto" when the image fails to load', () => { const post = makePost(); const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p), { initialProps: { p: post }, }); attachContainer(result.current.containerRef, 1000, 600); // Trigger effect re-run, then fire onerror rerender({ p: { ...post, id: 'post-error' } }); act(() => { capturedImage.onerror!(); }); expect(result.current.cardWidth).toBe('auto'); }); it('returns "auto" when the container has zero dimensions', () => { const post = makePost(); const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p), { initialProps: { p: post }, }); attachContainer(result.current.containerRef, 0, 0); rerender({ p: { ...post, id: 'post-zero' } }); act(() => { capturedImage.naturalWidth = 800; capturedImage.naturalHeight = 600; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('auto'); }); it('uses the thumbnail URL when no image URL is present', () => { const post = makePost({ image: undefined, thumbnail: 'https://cdn.example.com/thumb.jpg' }); const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p), { initialProps: { p: post }, }); attachContainer(result.current.containerRef, 1000, 600); rerender({ p: { ...post, id: 'post-thumb' } }); act(() => { // Square thumbnail → width = height = 600 → no cap capturedImage.naturalWidth = 600; capturedImage.naturalHeight = 600; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('600px'); }); it('computes the natural card width for a portrait image (no cap applied)', () => { // 400 × 600 → ratio 0.667 → naturalCardWidth = 600 × 0.667 = 400 // maxCardWidth = 1000 × 0.8 = 800 → min(400, 800) = 400 const post = makePost(); const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p), { initialProps: { p: post }, }); attachContainer(result.current.containerRef, 1000, 600); rerender({ p: { ...post, id: 'post-portrait' } }); act(() => { capturedImage.naturalWidth = 400; capturedImage.naturalHeight = 600; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('400px'); }); it('computes the natural card width for a square image (no cap applied)', () => { // 600 × 600 → ratio 1 → naturalCardWidth = 600 × 1 = 600 // maxCardWidth = 1000 × 0.8 = 800 → min(600, 800) = 600 const post = makePost(); const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p), { initialProps: { p: post }, }); attachContainer(result.current.containerRef, 1000, 600); rerender({ p: { ...post, id: 'post-square' } }); act(() => { capturedImage.naturalWidth = 600; capturedImage.naturalHeight = 600; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('600px'); }); it('caps the width at 80 % of the container for a wide landscape image', () => { // 1600 × 600 → ratio 2.667 → naturalCardWidth = 600 × 2.667 = 1600 // maxCardWidth = 1000 × 0.8 = 800 → min(1600, 800) = 800 const post = makePost(); const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p), { initialProps: { p: post }, }); attachContainer(result.current.containerRef, 1000, 600); rerender({ p: { ...post, id: 'post-wide' } }); act(() => { capturedImage.naturalWidth = 1600; capturedImage.naturalHeight = 600; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('800px'); }); it('respects a custom maxWidthRatio', () => { // 800 × 400 → ratio 2 → naturalCardWidth = 400 × 2 = 800 // maxCardWidth = 1000 × 0.5 = 500 → min(800, 500) = 500 const post = makePost(); const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p, 0.5), { initialProps: { p: post }, }); attachContainer(result.current.containerRef, 1000, 400); rerender({ p: { ...post, id: 'post-custom-ratio' } }); act(() => { capturedImage.naturalWidth = 800; capturedImage.naturalHeight = 400; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('500px'); }); it('recomputes when the post changes', () => { const post1 = makePost({ id: 'post-a', image: 'https://cdn.example.com/a.jpg' }); const post2 = makePost({ id: 'post-b', image: 'https://cdn.example.com/b.jpg' }); // Start with a blank post (no image) so every subsequent rerender triggers // the effect (deps change because the id changes each time). const { result, rerender } = renderHook(({ p }) => useMediaCardWidth(p), { initialProps: { p: makePost({ id: 'initial', image: undefined }) }, }); attachContainer(result.current.containerRef, 1000, 600); // First post: portrait 400 × 600 → 400px rerender({ p: post1 }); act(() => { capturedImage.naturalWidth = 400; capturedImage.naturalHeight = 600; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('400px'); // Second post: landscape 1200 × 600 → capped at 800px rerender({ p: post2 }); act(() => { capturedImage.naturalWidth = 1200; capturedImage.naturalHeight = 600; capturedImage.onload!(); }); expect(result.current.cardWidth).toBe('800px'); }); });