Here is my custom React-hook.
import { useEffect, useRef } from 'react'
function useInterval({ callback, interval, delay }) {
const savedTimerId = useRef<NodeJS.Timeout>()
useEffect(() => {
const loop = () => {
const res = callback()
const nextIteration = () => {
savedTimerId.current = setTimeout(loop, interval)
}
if (res instanceof Promise) {
res.then(nextIteration)
} else {
nextIteration()
}
}
let delayedTimerId: NodeJS.Timeout
if (!delay) {
loop()
} else {
delayedTimerId = setTimeout(loop, delay)
}
return () => {
// @ts-ignore
clearTimeout(savedTimerId.current)
if (delayedTimerId) {
clearTimeout(delayedTimerId)
}
}
}, [callback, interval, delay])
}
export { useInterval }
And here is the unit test
import { renderHook } from '@testing-library/react-hooks'
import { useInterval } from '../useInterval'
describe("Test scenarios for 'useInterval' hook", () => {
jest.useFakeTimers()
it("Should call 'callback' once", () => {
const callback = jest.fn()
const interval = 10000
const params = { callback, interval }
renderHook(() => useInterval(params))
expect(setTimeout).toHaveBeenCalledTimes(1)
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), interval)
})
})
But that's the output
Error: expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 2
I debugged this snippet. I found that before useInterval
invocation something has already triggered setTimeout
.
Seems like setTimeout
has been called internally. What am doing wrong? Any ideas?
You're absolutely right that @testing-library/react-hooks
is calling setTimeout
under the hood, and you can confirm this with:
jest.useFakeTimers()
renderHook(() => {})
expect(setTimeout).toHaveBeenCalledTimes(1)
You're probably better off focussing on how many times callback
is called, rather than setTimeout
:
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
describe("Test scenarios for 'useInterval' hook", () => {
it("Should call 'callback' immediately", () => {
jest.useFakeTimers()
const callback = jest.fn()
const interval = 10000
const params = { callback, interval }
renderHook(() => useInterval(params))
expect(callback).toHaveBeenCalledTimes(1)
})
it("Should call 'callback' repeatedly", () => {
jest.useFakeTimers()
const callback = jest.fn()
const interval = 10000
const params = { callback, interval }
renderHook(() => useInterval(params))
jest.advanceTimersByTime(interval * 2)
// Expect 3 calls: 1 immediate call and 2 interval calls:
expect(callback).toHaveBeenCalledTimes(3)
})
})