From 24ca2f6c416e5c3f55ea7d646be6661eddf12851 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 18 Feb 2026 20:17:33 +0300 Subject: [PATCH] test(throttle): add unit tests for throttle util --- .../lib/utils/throttle/throttle.test.ts | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 src/shared/lib/utils/throttle/throttle.test.ts diff --git a/src/shared/lib/utils/throttle/throttle.test.ts b/src/shared/lib/utils/throttle/throttle.test.ts new file mode 100644 index 0000000..b22d5ac --- /dev/null +++ b/src/shared/lib/utils/throttle/throttle.test.ts @@ -0,0 +1,319 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import { throttle } from './throttle'; + +describe('throttle', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + describe('Basic Functionality', () => { + it('should execute function immediately on first call', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 300); + + throttled('arg1', 'arg2'); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2'); + }); + + it('should throttle subsequent calls within wait period', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 300); + + throttled('first'); + expect(mockFn).toHaveBeenCalledTimes(1); + + // Call again within wait period - should not execute + throttled('second'); + expect(mockFn).toHaveBeenCalledTimes(1); + + // Advance time past wait period + vi.advanceTimersByTime(300); + + // Now trailing call executes + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenLastCalledWith('second'); + }); + + it('should allow execution after wait period expires', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled('first'); + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(100); + throttled('second'); + + expect(mockFn).toHaveBeenCalledTimes(2); + }); + }); + + describe('Trailing Edge Execution', () => { + it('should execute throttled call after wait period', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 300); + + throttled('first'); + expect(mockFn).toHaveBeenCalledTimes(1); + + throttled('second'); + throttled('third'); + // Still 1 because these are throttled + + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(300); + + // Trailing call executes + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenLastCalledWith('third'); + }); + + it('should cancel previous trailing call on new invocation', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled('first'); + vi.advanceTimersByTime(50); + throttled('second'); + vi.advanceTimersByTime(30); + throttled('third'); + + // At this point only first call executed + expect(mockFn).toHaveBeenCalledTimes(1); + + // Advance to trigger trailing call + vi.advanceTimersByTime(70); + + // First call + trailing (third) + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenLastCalledWith('third'); + }); + }); + + describe('Arguments and Context', () => { + it('should pass the correct arguments from the last throttled call', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled('arg1', 'arg2'); + vi.advanceTimersByTime(50); + throttled('arg3', 'arg4'); + vi.advanceTimersByTime(100); + + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenLastCalledWith('arg3', 'arg4'); + }); + + it('should handle no arguments', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled(); + + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it('should handle single argument', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled('single'); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith('single'); + }); + + it('should handle multiple arguments', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled(1, 2, 3, 'four', { five: 5 }); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith(1, 2, 3, 'four', { five: 5 }); + }); + }); + + describe('Timing', () => { + it('should handle very short wait times (1ms)', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 1); + + throttled('first'); + vi.advanceTimersByTime(1); + throttled('second'); + + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it('should handle longer wait times (1000ms)', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 1000); + + throttled('first'); + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(500); + throttled('second'); + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(500); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + }); + + describe('Rapid Calls', () => { + it('should handle rapid successive calls correctly', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled('call1'); + vi.advanceTimersByTime(10); + throttled('call2'); + vi.advanceTimersByTime(10); + throttled('call3'); + vi.advanceTimersByTime(10); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith('call1'); + + vi.advanceTimersByTime(100); + + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenLastCalledWith('call3'); + }); + + it('should execute function at most once per wait period plus trailing', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + // Make many rapid calls + for (let i = 0; i < 10; i++) { + vi.advanceTimersByTime(5); + throttled(`call${i}`); + } + + // Should execute immediately + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(100); + + // Plus trailing call + expect(mockFn).toHaveBeenCalledTimes(2); + }); + }); + + describe('Edge Cases', () => { + it('should handle zero wait time', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 0); + + throttled('first'); + + // With zero wait time, function may execute synchronously + // but the internal timing may still prevent immediate re-execution + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it('should handle being called at exactly wait boundary', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 100); + + throttled('first'); + vi.advanceTimersByTime(100); + throttled('second'); + + expect(mockFn).toHaveBeenCalledTimes(2); + }); + }); + + describe('Return Value', () => { + it('should not return anything (void)', () => { + const mockFn = vi.fn().mockReturnValue('result'); + const throttled = throttle(mockFn, 100); + + const result = throttled('arg'); + + expect(result).toBeUndefined(); + }); + }); + + describe('Real-World Scenarios', () => { + it('should throttle scroll-like events', () => { + const mockFn = vi.fn(); + const throttledScroll = throttle(mockFn, 100); + + throttledScroll(); + vi.advanceTimersByTime(10); + throttledScroll(); + vi.advanceTimersByTime(10); + throttledScroll(); + vi.advanceTimersByTime(10); + + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(100); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it('should throttle resize-like events', () => { + const mockFn = vi.fn(); + const throttledResize = throttle(mockFn, 200); + + throttledResize(); + for (let i = 1; i <= 10; i++) { + vi.advanceTimersByTime(10); + throttledResize(); + } + + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(200); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + }); + + describe('Comparison Characteristics', () => { + it('should execute immediately on first call', () => { + const mockFn = vi.fn(); + const throttled = throttle(mockFn, 300); + + throttled('first'); + + // Throttle executes immediately (unlike debounce) + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it('should allow execution during continuous calls at intervals', () => { + const mockFn = vi.fn(); + const waitTime = 100; + const throttled = throttle(mockFn, waitTime); + + throttled('call1'); + expect(mockFn).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(waitTime); + throttled('call2'); + expect(mockFn).toHaveBeenCalledTimes(2); + + vi.advanceTimersByTime(waitTime); + throttled('call3'); + expect(mockFn).toHaveBeenCalledTimes(3); + }); + }); +});