Skip to content

Commit

Permalink
Convert tests to Node.js test runner
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Nov 15, 2023
1 parent 95eef87 commit 1f6cd9b
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 93 deletions.
6 changes: 4 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
type AnyFunction = (...arguments_: readonly any[]) => unknown;

/**
Returns a function, that, as long as it continues to be invoked, will not be triggered. The function will be called after it stops being called for N milliseconds.
Creates a debounced function that delays execution until `wait` milliseconds have passed since its last invocation.
If `immediate` is passed, trigger the function on the leading edge, instead of the trailing. The function also has a property 'clear' that is a function which will clear the timer to prevent previously scheduled executions.
Set the `immediate` parameter to `true` to invoke the function immediately at the start of the `wait` interval, preventing issues such as double-clicks on a button.
The returned function has a `.clear()` method to cancel scheduled executions, and a `.flush()` method for immediate execution and resetting the timer for future calls.
*/
declare function debounce<F extends AnyFunction>(
function_: F,
Expand Down
19 changes: 12 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "debounce",
"version": "1.2.1",
"description": "Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked",
"description": "Delay function calls until a set time elapses after the last invocation",
"license": "MIT",
"repository": "sindresorhus/debounce",
"funding": "https://github.com/sponsors/sindresorhus",
Expand All @@ -16,22 +16,27 @@
"node": ">=18"
},
"scripts": {
"test": "xo && minijasminenode test.js"
"test": "xo && node --test"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"debounce",
"debouncing",
"function",
"throttle",
"invoke"
"invoke",
"limit",
"limited",
"interval",
"rate",
"batch",
"ratelimit"
],
"devDependencies": {
"minijasminenode": "^1.1.1",
"mocha": "^10.2.0",
"should": "^13.2.3",
"sinon": "^1.17.0",
"sinon": "^17.0.1",
"xo": "^0.56.0"
},
"xo": {
Expand Down
12 changes: 7 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# debounce

> Useful for implementing behavior that should only happen after a repeated action has completed
> Delay function calls until a set time elapses after the last invocation
## Install

Expand Down Expand Up @@ -37,10 +37,12 @@ window.onresize.flush();

### debounce(fn, wait, immediate?)

Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked.
Creates a debounced function that delays execution until `wait` milliseconds have passed since its last invocation.

Pass `true` for the `immediate` parameter to cause debounce to trigger the function on the leading edge instead of the trailing edge of the wait interval. Useful in circumstances like preventing accidental double-clicks on a "submit" button from firing a second time.
Set the `immediate` parameter to `true` to invoke the function immediately at the start of the `wait` interval, preventing issues such as double-clicks on a button.

The debounced function returned has a property 'clear' that is a function that will clear any scheduled future executions of your function.
The returned function has a `.clear()` method to cancel scheduled executions, and a `.flush()` method for immediate execution and resetting the timer for future calls.

The debounced function returned has a property 'flush' that is a function that will immediately execute the function if and only if execution is scheduled, and reset the execution timer for subsequent invocations of the debounced function.
## Related

- [p-debounce](https://github.com/sindresorhus/p-debounce) - Similar but handles promises.
128 changes: 49 additions & 79 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,163 +1,134 @@
/* eslint-env jasmine */
const test = require('node:test');
const assert = require('node:assert');
const sinon = require('sinon');
const debounce = require('./index.js');

describe('housekeeping', () => {
it('should be defined as a function', () => {
expect(typeof debounce).toEqual('function');
});
// TODO: Use Node.js test timer mocking.

test('housekeeping', async () => {
assert.strictEqual(typeof debounce, 'function', 'debounce should be a function');
});

describe('catch issue #3 - Debounced function executing early?', () => {
// Use sinon to control the clock
test('catch issue #3 - Debounced function executing early?', async t => {
let clock;

beforeEach(() => {
await t.test('should debounce with fast timeout', async () => {
clock = sinon.useFakeTimers();
});

afterEach(() => {
clock.restore();
});

it('should debounce with fast timeout', () => {
const callback = sinon.spy();

// Set up debounced function with wait of 100
const fn = debounce(callback, 100);

// Call debounced function at interval of 50
setTimeout(fn, 100);
setTimeout(fn, 150);
setTimeout(fn, 200);
setTimeout(fn, 250);

// Set the clock to 100 (period of the wait) ticks after the last debounced call
clock.tick(350);

// The callback should have been triggered once
expect(callback.callCount).toEqual(1);
assert.strictEqual(callback.callCount, 1, 'Callback should be triggered once');
clock.restore();
});
});

describe('forcing execution', () => {
// Use sinon to control the clock
test('forcing execution', async t => {
let clock;

beforeEach(() => {
await t.test('should not execute prior to timeout', async () => {
clock = sinon.useFakeTimers();
});

afterEach(() => {
clock.restore();
});

it('should not execute prior to timeout', () => {
const callback = sinon.spy();

// Set up debounced function with wait of 100
const fn = debounce(callback, 100);

// Call debounced function at interval of 50
setTimeout(fn, 100);
setTimeout(fn, 150);

// Set the clock to 25 (period of the wait) ticks after the last debounced call
clock.tick(175);

// The callback should not have been called yet
expect(callback.callCount).toEqual(0);
assert.strictEqual(callback.callCount, 0, 'Callback should not be called yet');
clock.restore();
});

it('should execute prior to timeout when flushed', () => {
await t.test('should execute prior to timeout when flushed', async () => {
clock = sinon.useFakeTimers();
const callback = sinon.spy();

// Set up debounced function with wait of 100
const fn = debounce(callback, 100);

// Call debounced function at interval of 50
setTimeout(fn, 100);
setTimeout(fn, 150);

// Set the clock to 25 (period of the wait) ticks after the last debounced call
clock.tick(175);

fn.flush();

// The callback has been called
expect(callback.callCount).toEqual(1);
assert.strictEqual(callback.callCount, 1, 'Callback should have been called');
clock.restore();
});

it('should not execute again after timeout when flushed before the timeout', () => {
await t.test('should not execute again after timeout when flushed before the timeout', async () => {
clock = sinon.useFakeTimers();
const callback = sinon.spy();

// Set up debounced function with wait of 100
const fn = debounce(callback, 100);

// Call debounced function at interval of 50
setTimeout(fn, 100);
setTimeout(fn, 150);

// Set the clock to 25 (period of the wait) ticks after the last debounced call
clock.tick(175);

fn.flush();

// The callback has been called here
expect(callback.callCount).toEqual(1);
assert.strictEqual(callback.callCount, 1, 'Callback should have been called once');

// Move to past the timeout
clock.tick(225);

// The callback should have only been called once
expect(callback.callCount).toEqual(1);
assert.strictEqual(callback.callCount, 1, 'Callback should not be called again after timeout');
clock.restore();
});

it('should not execute on a timer after being flushed', () => {
await t.test('should not execute on a timer after being flushed', async () => {
clock = sinon.useFakeTimers();
const callback = sinon.spy();

// Set up debounced function with wait of 100
const fn = debounce(callback, 100);

// Call debounced function at interval of 50
setTimeout(fn, 100);
setTimeout(fn, 150);

// Set the clock to 25 (period of the wait) ticks after the last debounced call
clock.tick(175);

fn.flush();

// The callback has been called here
expect(callback.callCount).toEqual(1);
assert.strictEqual(callback.callCount, 1, 'Callback should have been called once');

// Schedule again
setTimeout(fn, 250);

// Move to past the new timeout
clock.tick(400);

// The callback should have been called again
expect(callback.callCount).toEqual(2);
assert.strictEqual(callback.callCount, 2, 'Callback should be called again after new timeout');
clock.restore();
});

it('should not execute when flushed if nothing was scheduled', () => {
await t.test('should not execute when flushed if nothing was scheduled', async () => {
const callback = sinon.spy();

// Set up debounced function with wait of 100
const fn = debounce(callback, 100);

fn.flush();

// The callback should not have been called
expect(callback.callCount).toEqual(0);
assert.strictEqual(callback.callCount, 0, 'Callback should not be called when flushed without scheduling');
});

it('should execute with correct args when called again from within timeout', () => {
const callback = sinon.spy(n =>
// Recursively call debounced function until n == 0
--n && fn(n),
);
await t.test('should execute with correct args when called again from within timeout', async () => {
clock = sinon.useFakeTimers();

const callback = sinon.spy(n => {
--n;

if (n > 0) {
fn(n);
}
});

const fn = debounce(callback, 100);

Expand All @@ -167,34 +138,33 @@ describe('forcing execution', () => {
clock.tick(250);
clock.tick(375);

expect(callback.callCount).toEqual(3);
expect(callback.args[0]).toEqual([3]);
expect(callback.args[1]).toEqual([2]);
expect(callback.args[2]).toEqual([1]);
assert.strictEqual(callback.callCount, 3, 'Callback should be called three times');
assert.deepStrictEqual(callback.args[0], [3], 'First call args should match');
assert.deepStrictEqual(callback.args[1], [2], 'Second call args should match');
assert.deepStrictEqual(callback.args[2], [1], 'Third call args should match');
clock.restore();
});
});

describe('context check in debounced function', () => {
it('should throw an error if debounced method is called with different contexts', () => {
test('context check in debounced function', async t => {
await t.test('should throw an error if debounced method is called with different contexts', async () => {
function MyClass() {}

MyClass.prototype.debounced = debounce(() => {});

const instance1 = new MyClass();
const instance2 = new MyClass();

// Call the debounced function on the first instance
instance1.debounced();

let errorThrown = false;
try {
// Attempt to call the same debounced function on a different instance
instance2.debounced();
} catch (error) {
errorThrown = true;
expect(error.message).toEqual('Debounced method called with different contexts.');
assert.strictEqual(error.message, 'Debounced method called with different contexts.', 'Error message should match');
}

expect(errorThrown).toBeTruthy();
assert.ok(errorThrown, 'An error should have been thrown');
});
});

0 comments on commit 1f6cd9b

Please sign in to comment.