test(createDebouncedState): create test coverage for createDebouncedState

This commit is contained in:
Ilia Mashkov
2026-01-16 14:00:20 +03:00
parent 3cd9b36411
commit 14f9b87680

View File

@@ -0,0 +1,444 @@
import { createDebouncedState } from '$shared/lib';
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
/**
* Test Suite for createDebouncedState Helper Function
*
* This suite tests the debounced state management logic,
* including immediate vs debounced updates, timing behavior,
* and reset functionality.
*/
describe('createDebouncedState - Basic Logic', () => {
it('creates state with initial value', () => {
const state = createDebouncedState('initial');
expect(state.immediate).toBe('initial');
expect(state.debounced).toBe('initial');
});
it('supports custom debounce delay', () => {
const state = createDebouncedState('test', 100);
expect(state.immediate).toBe('test');
expect(state.debounced).toBe('test');
});
it('uses default delay of 300ms when not specified', () => {
const state = createDebouncedState('test');
expect(state.immediate).toBe('test');
expect(state.debounced).toBe('test');
});
it('allows updating immediate value', () => {
const state = createDebouncedState('initial');
state.immediate = 'updated';
expect(state.immediate).toBe('updated');
});
});
describe('createDebouncedState - Debounce Timing', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('immediate value updates instantly', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'updated';
expect(state.immediate).toBe('updated');
expect(state.debounced).toBe('initial');
});
it('debounced value updates after delay', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'updated';
expect(state.debounced).toBe('initial');
vi.advanceTimersByTime(99);
expect(state.debounced).toBe('initial');
vi.advanceTimersByTime(1);
expect(state.debounced).toBe('updated');
});
it('rapid changes reset the debounce timer', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'change1';
vi.advanceTimersByTime(50);
state.immediate = 'change2';
vi.advanceTimersByTime(50);
state.immediate = 'change3';
vi.advanceTimersByTime(50);
expect(state.debounced).toBe('initial');
expect(state.immediate).toBe('change3');
vi.advanceTimersByTime(50);
expect(state.debounced).toBe('change3');
});
it('debounced value remains unchanged during rapid updates', () => {
const state = createDebouncedState('initial', 100);
for (let i = 0; i < 5; i++) {
state.immediate = `update${i}`;
vi.advanceTimersByTime(25);
}
expect(state.immediate).toBe('update4');
expect(state.debounced).toBe('initial');
});
});
describe('createDebouncedState - Reset Functionality', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('resets to initial value when called without argument', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'changed';
vi.advanceTimersByTime(100);
expect(state.immediate).toBe('changed');
expect(state.debounced).toBe('changed');
state.reset();
expect(state.immediate).toBe('initial');
expect(state.debounced).toBe('initial');
});
it('resets to custom value when argument provided', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'changed';
state.reset('custom');
expect(state.immediate).toBe('custom');
expect(state.debounced).toBe('custom');
});
it('resets immediately without debounce delay', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'changed';
vi.advanceTimersByTime(50);
state.reset();
expect(state.immediate).toBe('initial');
expect(state.debounced).toBe('initial');
// Pending debounce from 'changed' will still fire after the delay
vi.advanceTimersByTime(50);
expect(state.debounced).toBe('changed');
});
it('resets sets both values immediately', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'changed';
vi.advanceTimersByTime(50);
state.reset('new');
expect(state.immediate).toBe('new');
expect(state.debounced).toBe('new');
// Pending debounce from 'changed' will fire after remaining delay
vi.advanceTimersByTime(50);
expect(state.debounced).toBe('changed');
});
});
describe('createDebouncedState - Type Support', () => {
it('works with string type', () => {
const state = createDebouncedState<string>('hello', 100);
state.immediate = 'world';
expect(state.immediate).toBe('world');
});
it('works with number type', () => {
const state = createDebouncedState<number>(0, 100);
state.immediate = 42;
expect(state.immediate).toBe(42);
});
it('works with boolean type', () => {
const state = createDebouncedState<boolean>(false, 100);
state.immediate = true;
expect(state.immediate).toBe(true);
});
it('works with object type', () => {
interface TestObject {
value: number;
label: string;
}
const initial: TestObject = { value: 0, label: 'initial' };
const state = createDebouncedState<TestObject>(initial, 100);
const updated: TestObject = { value: 1, label: 'updated' };
state.immediate = updated;
expect(state.immediate).toBe(updated);
expect(state.immediate.value).toBe(1);
});
it('works with array type', () => {
const initial = [1, 2, 3];
const state = createDebouncedState<number[]>(initial, 100);
const updated = [4, 5, 6];
state.immediate = updated;
expect(state.immediate).toEqual(updated);
});
it('works with null type', () => {
const state = createDebouncedState<string | null>(null, 100);
state.immediate = 'not null';
expect(state.immediate).toBe('not null');
});
it('works with undefined type', () => {
const state = createDebouncedState<number | undefined>(undefined, 100);
state.immediate = 42;
expect(state.immediate).toBe(42);
});
});
describe('createDebouncedState - Corner Cases', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('handles empty string', () => {
const state = createDebouncedState('', 100);
state.immediate = '';
vi.advanceTimersByTime(100);
expect(state.immediate).toBe('');
expect(state.debounced).toBe('');
});
it('handles zero value', () => {
const state = createDebouncedState(0, 100);
expect(state.immediate).toBe(0);
expect(state.debounced).toBe(0);
state.immediate = 0;
vi.advanceTimersByTime(100);
expect(state.immediate).toBe(0);
expect(state.debounced).toBe(0);
});
it('handles very short debounce delay (1ms)', () => {
const state = createDebouncedState('initial', 1);
state.immediate = 'changed';
expect(state.debounced).toBe('initial');
vi.advanceTimersByTime(1);
expect(state.debounced).toBe('changed');
});
it('handles very long debounce delay (5000ms)', () => {
const state = createDebouncedState('initial', 5000);
state.immediate = 'changed';
vi.advanceTimersByTime(4999);
expect(state.debounced).toBe('initial');
vi.advanceTimersByTime(1);
expect(state.debounced).toBe('changed');
});
it('handles setting to same value multiple times', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'same';
vi.advanceTimersByTime(50);
state.immediate = 'same';
vi.advanceTimersByTime(50);
expect(state.immediate).toBe('same');
vi.advanceTimersByTime(100);
expect(state.debounced).toBe('same');
});
it('handles alternating between two values rapidly', () => {
const state = createDebouncedState('initial', 50);
for (let i = 0; i < 5; i++) {
state.immediate = 'value1';
vi.advanceTimersByTime(25);
state.immediate = 'value2';
vi.advanceTimersByTime(25);
}
expect(state.immediate).toBe('value2');
expect(state.debounced).toBe('initial');
vi.advanceTimersByTime(50);
expect(state.debounced).toBe('value2');
});
it('handles reset during pending debounce', () => {
const state = createDebouncedState('initial', 100);
state.immediate = 'changed';
vi.advanceTimersByTime(50);
state.reset();
expect(state.immediate).toBe('initial');
expect(state.debounced).toBe('initial');
// Pending debounce from 'changed' will fire after remaining delay
vi.advanceTimersByTime(50);
expect(state.debounced).toBe('changed');
});
it('handles immediate value changes after reset', () => {
const state = createDebouncedState('initial', 100);
state.reset('new');
expect(state.immediate).toBe('new');
state.immediate = 'newer';
vi.advanceTimersByTime(100);
expect(state.immediate).toBe('newer');
expect(state.debounced).toBe('newer');
});
});
describe('createDebouncedState - Multiple Instances', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('handles multiple independent instances', () => {
const state1 = createDebouncedState('one', 100);
const state2 = createDebouncedState('two', 100);
state1.immediate = 'changed1';
state2.immediate = 'changed2';
expect(state1.immediate).toBe('changed1');
expect(state2.immediate).toBe('changed2');
vi.advanceTimersByTime(100);
expect(state1.debounced).toBe('changed1');
expect(state2.debounced).toBe('changed2');
});
it('independent timers for each instance', () => {
const state1 = createDebouncedState('one', 100);
const state2 = createDebouncedState('two', 200);
state1.immediate = 'changed1';
state2.immediate = 'changed2';
vi.advanceTimersByTime(100);
expect(state1.debounced).toBe('changed1');
expect(state2.debounced).toBe('two');
vi.advanceTimersByTime(100);
expect(state2.debounced).toBe('changed2');
});
});
describe('createDebouncedState - Interface Compliance', () => {
it('exposes immediate getter', () => {
const state = createDebouncedState('test');
expect(() => {
const _ = state.immediate;
}).not.toThrow();
});
it('exposes immediate setter', () => {
const state = createDebouncedState('test');
expect(() => {
state.immediate = 'new';
}).not.toThrow();
});
it('exposes debounced getter', () => {
const state = createDebouncedState('test');
expect(() => {
const _ = state.debounced;
}).not.toThrow();
});
it('exposes reset method', () => {
const state = createDebouncedState('test');
expect(typeof state.reset).toBe('function');
});
it('does not expose debounced setter', () => {
const state = createDebouncedState('test');
// TypeScript should prevent this, but we can check the runtime behavior
expect(state).not.toHaveProperty('set debounced');
});
});