import { renderHook, act } from '@testing-library/react-native'; import { useDebounce } from '../useDebounce'; jest.useFakeTimers(); describe('useDebounce', () => { beforeEach(() => { jest.clearAllTimers(); }); describe('leading edge (default)', () => { it('should execute callback immediately on first call', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000 })); act(() => { result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(1); }); it('should ignore subsequent calls within delay period', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000 })); // First call - executes immediately act(() => { result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(1); // Second call within delay - ignored act(() => { jest.advanceTimersByTime(500); result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(1); // Third call within delay - ignored act(() => { jest.advanceTimersByTime(400); result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(1); }); it('should allow execution after delay period has passed', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000 })); // First call act(() => { result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(1); // Wait for delay to pass act(() => { jest.advanceTimersByTime(1000); }); // Second call after delay - executes act(() => { result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(2); }); it('should pass arguments to callback', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000 })); act(() => { result.current.debouncedFn('test', 123, { key: 'value' }); }); expect(callback).toHaveBeenCalledWith('test', 123, { key: 'value' }); }); }); describe('trailing edge', () => { it('should delay execution on trailing edge', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000, leading: false })); act(() => { result.current.debouncedFn(); }); // Should not execute immediately expect(callback).not.toHaveBeenCalled(); // Should execute after delay act(() => { jest.advanceTimersByTime(1000); }); expect(callback).toHaveBeenCalledTimes(1); }); it('should reset timer on subsequent calls', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000, leading: false })); // First call act(() => { result.current.debouncedFn(); }); // Second call before timer fires - resets timer act(() => { jest.advanceTimersByTime(500); result.current.debouncedFn(); }); // Advance to original timeout - should not fire act(() => { jest.advanceTimersByTime(500); }); expect(callback).not.toHaveBeenCalled(); // Advance to new timeout - should fire once act(() => { jest.advanceTimersByTime(500); }); expect(callback).toHaveBeenCalledTimes(1); }); }); describe('isDebouncing', () => { it('should return true within delay period', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000 })); act(() => { result.current.debouncedFn(); }); expect(result.current.isDebouncing()).toBe(true); act(() => { jest.advanceTimersByTime(500); }); expect(result.current.isDebouncing()).toBe(true); }); it('should return false after delay period', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000 })); act(() => { result.current.debouncedFn(); }); act(() => { jest.advanceTimersByTime(1000); }); expect(result.current.isDebouncing()).toBe(false); }); }); describe('cancel', () => { it('should cancel pending trailing edge execution', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000, leading: false })); act(() => { result.current.debouncedFn(); }); // Cancel before timer fires act(() => { jest.advanceTimersByTime(500); result.current.cancel(); }); // Advance past original timeout act(() => { jest.advanceTimersByTime(600); }); expect(callback).not.toHaveBeenCalled(); }); }); describe('custom delay', () => { it('should respect custom delay value', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 2000 })); act(() => { result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(1); // Call within 2 seconds - ignored act(() => { jest.advanceTimersByTime(1500); result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(1); // Call after 2 seconds - executes act(() => { jest.advanceTimersByTime(500); result.current.debouncedFn(); }); expect(callback).toHaveBeenCalledTimes(2); }); }); describe('rapid clicks simulation', () => { it('should handle rapid refresh button clicks correctly', () => { const callback = jest.fn(); const { result } = renderHook(() => useDebounce(callback, { delay: 1000 })); // Simulate 5 rapid clicks within 2 seconds act(() => { result.current.debouncedFn(); // Click 1 - executes jest.advanceTimersByTime(200); result.current.debouncedFn(); // Click 2 - ignored jest.advanceTimersByTime(300); result.current.debouncedFn(); // Click 3 - ignored jest.advanceTimersByTime(400); result.current.debouncedFn(); // Click 4 - ignored jest.advanceTimersByTime(400); result.current.debouncedFn(); // Click 5 - executes (>1000ms since last execution) }); // Only first and last should execute expect(callback).toHaveBeenCalledTimes(2); }); }); });