Timers
Loading "Timers"
Run locally for transcripts
Debounce and throttle have to be some of my favorite utility functions, and they both happened to rely on timers to work. Let's study them in more detail and also see how we would test them.
Take a look at the
debounce
function implementation below:/**
* Debounce the given callback function.
*/
export function debounce<Args extends Array<unknown>>(
callback: (...args: Args) => unknown,
duration: number,
): (...args: Args) => void {
let timeout: NodeJS.Timeout | null = null
return function (...args) {
const effect = () => {
timeout = null
return callback.apply(this, args)
}
clearTimeout(timeout)
timeout = setTimeout(effect, duration)
}
}
The intention behind the
debounce()
function is to accept a callback
and keep delaying its execution until a certain time duration
has passed since its last call.For example, consider this usage scenario:
// Create a debounced function of "foo"
// with the debounce duration of 250ms.
const bar = debounce(foo, 250)
bar('one')
bar('two')
bar('three')
Here, the
bar()
function is just a debounced wrapped over foo()
. Not all calls to bar()
will translate to the calls of foo()
(hence, the whole point of debouncing). Here's what's happening in the example above:bar('one')
gets called, starting a 250ms timer forfoo('one')
.bar('two')
gets called, cancelling the previous timer and starting a new one forfoo('two')
.bar('three')
gets called, cancelling the previous timer and starting a new one forfoo('three')
.- After 250ms pass without any calls to
bar()
, thefoo('three')
finally gets called.
Let's try putting this very use case into an automated test:
test('executes the callback after the debounce timeout', () => {
const foo = vi.fn<(input: string) => void>()
const bar = debounce(foo, 250)
bar('one')
bar('two')
bar('three')
expect(foo).toHaveBeenCalledOnce()
expect(foo).toHaveBeenCalledWith('three')
})
Running this test, however, will produce something unexpected:
FAIL debounce.test.ts > executes the callback after the debounce timeout
AssertionError: expected "spy" to be called once, but got 0 times
β― debounce.test.ts:19:15
7| bar('three')
8|
9| expect(foo).toHaveBeenCalledOnce()
The
foo()
function is not called at all!Our assertions throw because they run immediately after the debounced calls. We aren't taking the debounce timeout (250ms) into account at all. I think it's time we did.
You already know how to mock date and time using the
vi.useFakeTimers()
and vi.setSystemTime()
methods, but this exercise doesn't just require you to mock them, it requires you to advance the time.