import { renderHook, act } from '@testing-library/react'; import { useDebounce } from '@/lib/hooks/useDebounce'; describe('useDebounce', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('returns the initial value immediately', () => { const { result } = renderHook(() => useDebounce('initial', 500)); expect(result.current).toBe('initial'); }); it('updates the debounced value after the delay', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 500 }, }); // Change the value rerender({ value: 'updated', delay: 500 }); // Value should still be initial before delay expect(result.current).toBe('initial'); // Fast forward time act(() => { jest.advanceTimersByTime(500); }); // Value should now be updated expect(result.current).toBe('updated'); }); it('does not update the value before the delay', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 500 }, }); rerender({ value: 'updated', delay: 500 }); // Only advance 300ms (not enough) act(() => { jest.advanceTimersByTime(300); }); expect(result.current).toBe('initial'); }); it('resets the timer when value changes rapidly', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 500 }, }); // First change rerender({ value: 'first', delay: 500 }); // Advance 300ms act(() => { jest.advanceTimersByTime(300); }); // Second change (should reset timer) rerender({ value: 'second', delay: 500 }); // Advance another 300ms (total 600ms from first, but only 300ms from second) act(() => { jest.advanceTimersByTime(300); }); // Value should still be initial (timer was reset) expect(result.current).toBe('initial'); // Advance the remaining 200ms act(() => { jest.advanceTimersByTime(200); }); // Now should show 'second' expect(result.current).toBe('second'); }); it('cleans up timeout on unmount', () => { const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); const { unmount, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 500 }, }); rerender({ value: 'updated', delay: 500 }); unmount(); expect(clearTimeoutSpy).toHaveBeenCalled(); clearTimeoutSpy.mockRestore(); }); it('works with different delay values', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 1000 }, }); rerender({ value: 'updated', delay: 1000 }); act(() => { jest.advanceTimersByTime(500); }); expect(result.current).toBe('initial'); act(() => { jest.advanceTimersByTime(500); }); expect(result.current).toBe('updated'); }); it('works with different value types', () => { // Test with number const { result: numberResult } = renderHook(() => useDebounce(42, 500)); expect(numberResult.current).toBe(42); // Test with object const obj = { foo: 'bar' }; const { result: objectResult } = renderHook(() => useDebounce(obj, 500)); expect(objectResult.current).toEqual({ foo: 'bar' }); // Test with null const { result: nullResult } = renderHook(() => useDebounce(null, 500)); expect(nullResult.current).toBeNull(); }); it('handles zero delay', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 0 }, }); rerender({ value: 'updated', delay: 0 }); act(() => { jest.advanceTimersByTime(0); }); expect(result.current).toBe('updated'); }); });