diff --git a/src/function/debounce.spec.ts b/src/function/debounce.spec.ts index 3182de76d..f6a448a1e 100644 --- a/src/function/debounce.spec.ts +++ b/src/function/debounce.spec.ts @@ -3,110 +3,161 @@ import { debounce } from './debounce'; // adjust the import path as necessary import { delay } from '../promise'; +const DEBOUNCE_MS = 50; + describe('debounce', () => { it('should debounce function calls', async () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs); + const debouncedFunc = debounce(func, DEBOUNCE_MS); debouncedFunc(); debouncedFunc(); debouncedFunc(); - await delay(debounceMs * 2); + expect(func).not.toHaveBeenCalled(); + + await delay(DEBOUNCE_MS * 2); expect(func).toHaveBeenCalledTimes(1); }); it('should delay the function call by the specified wait time', async () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs); + const debouncedFunc = debounce(func, DEBOUNCE_MS); debouncedFunc(); - await delay(debounceMs / 2); + await delay(DEBOUNCE_MS / 2); expect(func).not.toHaveBeenCalled(); - await delay(debounceMs / 2 + 1); + await delay(DEBOUNCE_MS / 2 + 1); expect(func).toHaveBeenCalledTimes(1); }); it('should reset the wait time if called again before wait time ends', async () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs); + const debouncedFunc = debounce(func, DEBOUNCE_MS); debouncedFunc(); - await delay(debounceMs / 2); + await delay(DEBOUNCE_MS / 2); debouncedFunc(); - await delay(debounceMs / 2); + await delay(DEBOUNCE_MS / 2); debouncedFunc(); - await delay(debounceMs / 2); + await delay(DEBOUNCE_MS / 2); debouncedFunc(); expect(func).not.toHaveBeenCalled(); - await delay(debounceMs + 1); + await delay(DEBOUNCE_MS + 1); expect(func).toHaveBeenCalledTimes(1); }); it('should cancel the debounced function call', async () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs); + const debouncedFunc = debounce(func, DEBOUNCE_MS); debouncedFunc(); debouncedFunc.cancel(); - await delay(debounceMs); + await delay(DEBOUNCE_MS); expect(func).not.toHaveBeenCalled(); }); + it('should immediately invoke the delayed function when flush is called', async () => { + const func = vi.fn(); + const debouncedFunc = debounce(func, DEBOUNCE_MS); + + debouncedFunc(); + debouncedFunc.flush(); + + expect(func).toHaveBeenCalledTimes(1); + }); + it('should work correctly if the debounced function is called after the wait time', async () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs); + const debouncedFunc = debounce(func, DEBOUNCE_MS); debouncedFunc(); - await delay(debounceMs + 1); + await delay(DEBOUNCE_MS + 1); debouncedFunc(); - await delay(debounceMs + 1); + await delay(DEBOUNCE_MS + 1); expect(func).toHaveBeenCalledTimes(2); }); it('should have no effect if we call cancel when the function is not executed', () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs); + const debouncedFunc = debounce(func, DEBOUNCE_MS); expect(() => debouncedFunc.cancel()).not.toThrow(); }); it('should call the function with correct arguments', async () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs); + const debouncedFunc = debounce(func, DEBOUNCE_MS); debouncedFunc('test', 123); - await delay(debounceMs * 2); + await delay(DEBOUNCE_MS * 2); expect(func).toHaveBeenCalledTimes(1); expect(func).toHaveBeenCalledWith('test', 123); }); + it('should execute immediately on first call when edges is set to leading', async () => { + const func = vi.fn(); + const debouncedFunc = debounce(func, DEBOUNCE_MS, { edges: ['leading'] }); + + debouncedFunc(); + + expect(func).toHaveBeenCalledTimes(1); + + debouncedFunc(); + + await delay(DEBOUNCE_MS); + + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should execute immediately on last call when edges is set to trailing', async () => { + const func = vi.fn(); + const debouncedFunc = debounce(func, DEBOUNCE_MS, { edges: ['trailing'] }); + + debouncedFunc(); + + expect(func).not.toHaveBeenCalled(); + + debouncedFunc(); + + await delay(DEBOUNCE_MS); + + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should execute immediately on both edges when edges is set to both', async () => { + const func = vi.fn(); + const debouncedFunc = debounce(func, DEBOUNCE_MS, { edges: ['leading', 'trailing'] }); + + debouncedFunc(); + + expect(func).toHaveBeenCalledTimes(1); + + debouncedFunc(); + + await delay(DEBOUNCE_MS); + + expect(func).toHaveBeenCalledTimes(2); + }); + it('should cancel the debounced function call if aborted via AbortSignal', async () => { const func = vi.fn(); - const debounceMs = 50; const controller = new AbortController(); const signal = controller.signal; - const debouncedFunc = debounce(func, debounceMs, { signal }); + const debouncedFunc = debounce(func, DEBOUNCE_MS, { signal }); debouncedFunc(); controller.abort(); - await delay(debounceMs); + await delay(DEBOUNCE_MS); expect(func).not.toHaveBeenCalled(); }); @@ -119,24 +170,22 @@ describe('debounce', () => { const func = vi.fn(); - const debounceMs = 50; - const debouncedFunc = debounce(func, debounceMs, { signal }); + const debouncedFunc = debounce(func, DEBOUNCE_MS, { signal }); debouncedFunc(); - await delay(debounceMs); + await delay(DEBOUNCE_MS); expect(func).not.toHaveBeenCalled(); }); it('should not add multiple abort event listeners', async () => { const func = vi.fn(); - const debounceMs = 100; const controller = new AbortController(); const signal = controller.signal; const addEventListenerSpy = vi.spyOn(signal, 'addEventListener'); - const debouncedFunc = debounce(func, debounceMs, { signal }); + const debouncedFunc = debounce(func, DEBOUNCE_MS, { signal }); debouncedFunc(); debouncedFunc(); diff --git a/src/function/debounce.ts b/src/function/debounce.ts index c2880977c..167f4fe14 100644 --- a/src/function/debounce.ts +++ b/src/function/debounce.ts @@ -50,6 +50,7 @@ export interface DebouncedFunction void> { * @param {number} debounceMs - The number of milliseconds to delay. * @param {DebounceOptions} options - The options object * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function. + * @param {Array<'leading' | 'trailing'>} options.edges - An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both. * @returns A new debounced function with a `cancel` method. * * @example