import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useDebounce } from '../useDebounce'; describe('useDebounce', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should return initial value immediately', () => { const { result } = renderHook(() => useDebounce('initial', 300)); expect(result.current).toBe('initial'); }); it('should debounce value updates', () => { const { result, rerender } = renderHook( ({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 300 } } ); expect(result.current).toBe('initial'); // Update value rerender({ value: 'updated', delay: 300 }); // Value should still be initial before delay expect(result.current).toBe('initial'); // Fast forward time by 300ms act(() => { vi.advanceTimersByTime(300); }); // Value should now be updated expect(result.current).toBe('updated'); }); it('should reset timer on rapid value changes', () => { const { result, rerender } = renderHook( ({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 300 } } ); // First update rerender({ value: 'first', delay: 300 }); act(() => { vi.advanceTimersByTime(100); }); // Second update before first delay completes rerender({ value: 'second', delay: 300 }); act(() => { vi.advanceTimersByTime(100); }); // Third update before second delay completes rerender({ value: 'third', delay: 300 }); // Value should still be initial expect(result.current).toBe('initial'); // Complete the delay act(() => { vi.advanceTimersByTime(300); }); // Should have the last value expect(result.current).toBe('third'); }); it('should use default delay of 300ms', () => { const { result, rerender } = renderHook(({ value }) => useDebounce(value), { initialProps: { value: 'initial' }, }); rerender({ value: 'updated' }); act(() => { vi.advanceTimersByTime(299); }); expect(result.current).toBe('initial'); act(() => { vi.advanceTimersByTime(1); }); expect(result.current).toBe('updated'); }); it('should work with different value types', () => { // Test with number const { result: numberResult, rerender: numberRerender } = renderHook( ({ value }) => useDebounce(value, 100), { initialProps: { value: 0 } } ); numberRerender({ value: 42 }); act(() => { vi.advanceTimersByTime(100); }); expect(numberResult.current).toBe(42); // Test with object const { result: objectResult, rerender: objectRerender } = renderHook( ({ value }) => useDebounce(value, 100), { initialProps: { value: { key: 'initial' } } } ); objectRerender({ value: { key: 'updated' } }); act(() => { vi.advanceTimersByTime(100); }); expect(objectResult.current).toEqual({ key: 'updated' }); // Test with array const { result: arrayResult, rerender: arrayRerender } = renderHook( ({ value }) => useDebounce(value, 100), { initialProps: { value: [1, 2, 3] } } ); arrayRerender({ value: [4, 5, 6] }); act(() => { vi.advanceTimersByTime(100); }); expect(arrayResult.current).toEqual([4, 5, 6]); }); it('should handle delay changes', () => { const { result, rerender } = renderHook( ({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 300 } } ); rerender({ value: 'updated', delay: 300 }); // Change delay before first one completes rerender({ value: 'updated', delay: 500 }); act(() => { vi.advanceTimersByTime(300); }); // Should still be initial because delay changed expect(result.current).toBe('initial'); act(() => { vi.advanceTimersByTime(200); }); // Now should be updated expect(result.current).toBe('updated'); }); it('should cleanup timer on unmount', () => { const { unmount, rerender } = renderHook( ({ value }) => useDebounce(value, 300), { initialProps: { value: 'initial' } } ); rerender({ value: 'updated' }); // Unmount before delay completes unmount(); // Advance timers - should not cause errors act(() => { vi.advanceTimersByTime(300); }); }); });