import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook } from '@testing-library/react'; import { useRef } from 'react'; import { useIntersectionObserver } from './useIntersectionObserver'; // Mock IntersectionObserver class MockIntersectionObserver { observe = vi.fn(); disconnect = vi.fn(); unobserve = vi.fn(); constructor( public callback: IntersectionObserverCallback, public options?: IntersectionObserverInit, ) {} } describe('useIntersectionObserver', () => { let mockObserver: MockIntersectionObserver; beforeEach(() => { mockObserver = new MockIntersectionObserver(() => {}); global.IntersectionObserver = MockIntersectionObserver as any; vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should return undefined when element ref is null', () => { const elementRef = { current: null }; const { result } = renderHook(() => useIntersectionObserver(elementRef as React.RefObject, {}), ); // Initially undefined, and stays undefined when ref is null expect(result.current).toBeUndefined(); expect(mockObserver.observe).not.toHaveBeenCalled(); }); it('should observe element when ref is provided', () => { const element = document.createElement('div'); const elementRef = { current: element }; renderHook(() => useIntersectionObserver(elementRef as React.RefObject, {}), ); // Wait for effect to run expect(mockObserver.observe).toHaveBeenCalled(); }); it('should disconnect observer on unmount', () => { const element = document.createElement('div'); const elementRef = { current: element }; const { unmount } = renderHook(() => useIntersectionObserver(elementRef as React.RefObject, {}), ); unmount(); expect(mockObserver.disconnect).toHaveBeenCalled(); }); it('should use default options', () => { const element = document.createElement('div'); const elementRef = { current: element }; renderHook(() => useIntersectionObserver(elementRef as React.RefObject, {}), ); // Verify IntersectionObserver was called expect(MockIntersectionObserver).toHaveBeenCalled(); const call = (MockIntersectionObserver as any).mock.calls[0]; expect(call[1]).toMatchObject({ threshold: 0, root: null, rootMargin: '0%', }); }); it('should use custom options', () => { const element = document.createElement('div'); const elementRef = { current: element }; renderHook(() => useIntersectionObserver(elementRef as React.RefObject, { threshold: 0.5, rootMargin: '10px', }), ); expect(MockIntersectionObserver).toHaveBeenCalled(); const call = (MockIntersectionObserver as any).mock.calls[0]; expect(call[1]).toMatchObject({ threshold: 0.5, rootMargin: '10px', }); }); it('should freeze when freezeOnceVisible is true and element is intersecting', () => { const element = document.createElement('div'); const elementRef = { current: element }; const { result, rerender } = renderHook( ({ freeze }) => useIntersectionObserver(elementRef as React.RefObject, { freezeOnceVisible: freeze, }), { initialProps: { freeze: true }, }, ); // Get the callback from the constructor call const constructorCall = (MockIntersectionObserver as any).mock.calls.find( (call: any[]) => call[0] && typeof call[0] === 'function', ); if (constructorCall) { const callback = constructorCall[0]; const entry = { isIntersecting: true, } as IntersectionObserverEntry; callback([entry], {} as IntersectionObserver); } rerender({ freeze: true }); // Should not observe again when frozen expect(mockObserver.observe).toHaveBeenCalledTimes(1); }); it('should handle missing IntersectionObserver gracefully', () => { const originalIO = global.IntersectionObserver; // @ts-ignore delete global.IntersectionObserver; const element = document.createElement('div'); const elementRef = { current: element }; const { result } = renderHook(() => useIntersectionObserver(elementRef as React.RefObject, {}), ); expect(result.current).toBeUndefined(); global.IntersectionObserver = originalIO; }); });