feature/test-coverage #27
319
src/shared/lib/utils/throttle/throttle.test.ts
Normal file
319
src/shared/lib/utils/throttle/throttle.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user