feature/test-coverage #27

Merged
ilia merged 12 commits from feature/test-coverage into main 2026-02-22 07:46:55 +00:00
Showing only changes of commit 24ca2f6c41 - Show all commits

View 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);
});
});
});