diff --git a/README.md b/README.md index edb3be1..2cb9dec 100644 --- a/README.md +++ b/README.md @@ -399,3 +399,4 @@ This package provides a number of utility functions. | `removePhoneNumberCountryPrefix(phoneNumber, [, prefix])` | Removes a country prefix from a phone number based on the station config `country_code`. | | `onLocalStorageChange(key, callback)` | Calls `callback` when the value of `key` in `localStorage` changes. | | `decodeRadioToken(token)` | Decodes a JWT radio token. | +| `debounce(ms, fn)` | Returns a debounced version of `fn` that delays invocation until `ms` after the last call. | diff --git a/src/__test__/utils/debounce.test.js b/src/__test__/utils/debounce.test.js new file mode 100644 index 0000000..4009f27 --- /dev/null +++ b/src/__test__/utils/debounce.test.js @@ -0,0 +1,60 @@ +import { describe, expect, test, jest } from '@jest/globals' +import debounce from '../../utils/debounce.js' + +describe('debounce', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + test('it calls the function after the delay', () => { + const fn = jest.fn() + const debounced = debounce(100, fn) + + debounced() + expect(fn).not.toHaveBeenCalled() + + jest.advanceTimersByTime(100) + expect(fn).toHaveBeenCalledTimes(1) + }) + + test('it resets the timer on subsequent calls', () => { + const fn = jest.fn() + const debounced = debounce(100, fn) + + debounced() + jest.advanceTimersByTime(50) + debounced() + jest.advanceTimersByTime(50) + + expect(fn).not.toHaveBeenCalled() + + jest.advanceTimersByTime(50) + expect(fn).toHaveBeenCalledTimes(1) + }) + + test('it passes arguments to the function', () => { + const fn = jest.fn() + const debounced = debounce(100, fn) + + debounced('hello', 42) + jest.advanceTimersByTime(100) + + expect(fn).toHaveBeenCalledWith('hello', 42) + }) + + test('it uses the arguments from the last call', () => { + const fn = jest.fn() + const debounced = debounce(100, fn) + + debounced('first') + debounced('second') + jest.advanceTimersByTime(100) + + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith('second') + }) +}) diff --git a/src/index.js b/src/index.js index dae25eb..8a9553e 100644 --- a/src/index.js +++ b/src/index.js @@ -25,3 +25,4 @@ export { cdnImageUrl, cdnUrl } from './utils/cdnUrl.js' export { removePhoneNumberCountryPrefix } from './utils/phoneNumber.js' export { setupAirbrake, gtmFilter } from './utils/setupAirbrake.js' export { onLocalStorageChange } from './utils/onLocalStorageChange.js' +export { default as debounce } from './utils/debounce.js' diff --git a/src/utils/debounce.js b/src/utils/debounce.js new file mode 100644 index 0000000..513f0b2 --- /dev/null +++ b/src/utils/debounce.js @@ -0,0 +1,7 @@ +export default function debounce(ms, fn) { + let timer + return (...args) => { + clearTimeout(timer) + timer = setTimeout(() => fn(...args), ms) + } +}