Timers

Loading "Timers"
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 for foo('one').
  • bar('two') gets called, cancelling the previous timer and starting a new one for foo('two').
  • bar('three') gets called, cancelling the previous timer and starting a new one for foo('three').
  • After 250ms pass without any calls to bar(), the foo('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.
πŸ‘¨β€πŸ’Ό Complete the test suite for by handling the setTimeout() in test and having the scenarios pass by running npm test.

Access Denied

You must login or register for the workshop to view the diff.

Check out this video to see how the diff tab works.