useCancellableTimeout.test.ts 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  2. import { renderHook, act } from '@testing-library/react';
  3. import { useCancellableTimeout } from '../../hooks/useCancellableTimeout';
  4. describe('useCancellableTimeout', () => {
  5. beforeEach(() => vi.useFakeTimers());
  6. afterEach(() => vi.useRealTimers());
  7. it('runs the callback after the delay', () => {
  8. const fn = vi.fn();
  9. const { result } = renderHook(() => useCancellableTimeout());
  10. act(() => result.current.schedule(fn, 1500));
  11. expect(fn).not.toHaveBeenCalled();
  12. act(() => void vi.advanceTimersByTime(1500));
  13. expect(fn).toHaveBeenCalledTimes(1);
  14. });
  15. it('does not run the callback after unmount', () => {
  16. // The bug this exists for: a modal that defers its own close by 1.5s fired
  17. // setState and onClose after the component was gone — which throws outright
  18. // once the DOM around it has been torn down.
  19. const fn = vi.fn();
  20. const { result, unmount } = renderHook(() => useCancellableTimeout());
  21. act(() => result.current.schedule(fn, 1500));
  22. unmount();
  23. act(() => void vi.advanceTimersByTime(5000));
  24. expect(fn).not.toHaveBeenCalled();
  25. });
  26. it('cancel() stops a pending callback', () => {
  27. const fn = vi.fn();
  28. const { result } = renderHook(() => useCancellableTimeout());
  29. act(() => result.current.schedule(fn, 1000));
  30. act(() => result.current.cancel());
  31. act(() => void vi.advanceTimersByTime(2000));
  32. expect(fn).not.toHaveBeenCalled();
  33. });
  34. it('scheduling again replaces the pending callback', () => {
  35. const first = vi.fn();
  36. const second = vi.fn();
  37. const { result } = renderHook(() => useCancellableTimeout());
  38. act(() => result.current.schedule(first, 1000));
  39. act(() => result.current.schedule(second, 1000));
  40. act(() => void vi.advanceTimersByTime(1000));
  41. expect(first).not.toHaveBeenCalled();
  42. expect(second).toHaveBeenCalledTimes(1);
  43. });
  44. it('is safe to cancel when nothing is pending', () => {
  45. const { result } = renderHook(() => useCancellableTimeout());
  46. expect(() => act(() => result.current.cancel())).not.toThrow();
  47. });
  48. });