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