Skip to content
Open
115 changes: 82 additions & 33 deletions src/function/debounce.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,110 +3,161 @@ import { debounce } from './debounce';
// adjust the import path as necessary
import { delay } from '../promise';

const DEBOUNCE_MS = 50;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce code duplication, change debounceMs to a constant.


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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was added to more clearly test the basic debounce behavior.


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);
});
Comment on lines +65 to +73
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test code for the debounced.flush function.


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);
});
Comment on lines +106 to +149
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test code for the edges option of the debounce function.


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();
});
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/function/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface DebouncedFunction<F extends (...args: any[]) => 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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added missing comments.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's also an export missing for the DebounceOption type, I have a PR waiting for approval, but nobody reviewed it.

Copy link
Contributor Author

@ssi02014 ssi02014 Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type DebounceOptions = Parameters<typeof debounce<() => void>>[2];

@codecov-commenter It's a shame that the review is delayed 🥲
DebounceOptions can be approached in the same way as above

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but this is a ridiculous way to use it... hopefully we get proper responses on our PRs.

* @returns A new debounced function with a `cancel` method.
*
* @example
Expand Down