test(createDebouncedState): create test coverage for createDebouncedState
This commit is contained in:
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user