import { act, renderHook } from '@testing-library/react'; import { vi } from 'vitest'; import { useDebounce } from '../useDebounce'; describe('useDebounce hook', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('returns initial value immediately', () => { const { result } = renderHook(() => useDebounce('initial', 500)); expect(result.current).toBe('initial'); }); it('debounces value changes with default delay', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: undefined }, }); expect(result.current).toBe('initial'); // Update value rerender({ value: 'updated', delay: undefined }); // Should still be initial value before timeout expect(result.current).toBe('initial'); // Fast-forward time by default delay (500ms) act(() => { vi.advanceTimersByTime(500); }); expect(result.current).toBe('updated'); }); it('debounces value changes with custom delay', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 1000 }, }); expect(result.current).toBe('initial'); // Update value rerender({ value: 'updated', delay: 1000 }); // Should still be initial value before timeout expect(result.current).toBe('initial'); // Fast-forward time by custom delay (1000ms) act(() => { vi.advanceTimersByTime(1000); }); expect(result.current).toBe('updated'); }); it('cancels previous timeout when value changes quickly', () => { const { result, rerender } = renderHook(({ value }) => useDebounce(value, 500), { initialProps: { value: 'initial' }, }); // Update value multiple times quickly rerender({ value: 'first' }); act(() => { vi.advanceTimersByTime(200); }); rerender({ value: 'second' }); act(() => { vi.advanceTimersByTime(200); }); rerender({ value: 'final' }); // Should still be initial value expect(result.current).toBe('initial'); // Complete the final timeout act(() => { vi.advanceTimersByTime(500); }); // Should be the final value, not intermediate ones expect(result.current).toBe('final'); }); it('handles different value types', () => { const { result, rerender } = renderHook(({ value }) => useDebounce(value, 300), { initialProps: { value: 42 } }); expect(result.current).toBe(42); rerender({ value: 100 }); act(() => { vi.advanceTimersByTime(300); }); expect(result.current).toBe(100); }); it('handles object values', () => { const initialObj = { name: 'John', age: 30 }; const updatedObj = { name: 'Jane', age: 25 }; const { result, rerender } = renderHook(({ value }) => useDebounce(value, 200), { initialProps: { value: initialObj }, }); expect(result.current).toBe(initialObj); rerender({ value: updatedObj }); act(() => { vi.advanceTimersByTime(200); }); expect(result.current).toBe(updatedObj); }); it('handles zero delay', () => { const { result, rerender } = renderHook(({ value }) => useDebounce(value, 0), { initialProps: { value: 'initial' }, }); rerender({ value: 'updated' }); act(() => { vi.runAllTimers(); }); expect(result.current).toBe('updated'); }); it('uses default delay when delay is undefined', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: undefined }, }); rerender({ value: 'updated', delay: undefined }); act(() => { vi.advanceTimersByTime(400); }); expect(result.current).toBe('initial'); act(() => { vi.advanceTimersByTime(100); }); expect(result.current).toBe('updated'); }); it('handles delay change during debounce', () => { const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 1000 }, }); // Change value with 1000ms delay rerender({ value: 'updated', delay: 1000 }); // After 500ms, change delay to 200ms act(() => { vi.advanceTimersByTime(500); }); rerender({ value: 'updated', delay: 200 }); // Should restart timer with new delay act(() => { vi.advanceTimersByTime(200); }); expect(result.current).toBe('updated'); }); });