Jest testing useInterval React Hook not working - reactjs

Trying to test Custom useInterval Hook but jest.advanceTimersByTime(199); and jest.advanceTimersToNextTimer(1); don't seem to be working.
I log jest.getTimerCount() anywhere and it returns 0;
Custom Hook:
import { useRef, useEffect } from 'react';
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void | null>();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
});
// Set up the interval.
useEffect(() => {
function tick() {
console.log("here"); // This never gets logged !!!!
if (typeof savedCallback?.current !== 'undefined') {
console.log(delay, savedCallback);
}
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
export default useInterval;
Test:
import useInterval from "./useInterval";
import { renderHook } from '#testing-library/react-hooks';
describe("useInterval Hook:", () => {
let callback = jest.fn();
beforeAll(() => {
// we're using fake timers because we don't want to
// wait a full second for this test to run.
jest.useFakeTimers();
});
afterEach(() => {
callback.mockRestore();
jest.clearAllTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test('should init hook with delay', () => {
const { result } = renderHook(() => useInterval(callback, 5000));
expect(result.current).toBeUndefined();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 5000);
});
test('should repeatedly calls provided callback with a fixed time delay between each call', () => {
const { result } = renderHook(() => useInterval(callback, 200));
expect(callback).not.toHaveBeenCalled();
// fast-forward time until 1s before it should be executed
jest.advanceTimersByTime(199);
expect(callback).not.toHaveBeenCalled(); // FAILS
// jest.getTimerCount() here returns 0
// fast-forward until 1st call should be executed
jest.advanceTimersToNextTimer(1);
expect(callback).toHaveBeenCalledTimes(1);
// fast-forward until next timer should be executed
jest.advanceTimersToNextTimer();
expect(callback).toHaveBeenCalledTimes(2);
// fast-forward until 3 more timers should be executed
jest.advanceTimersToNextTimer(3);
expect(callback).toHaveBeenCalledTimes(5);
});
});

I solved it by moving jest.useFakeTimers(); to the beforeEach block instead of beforeAll.

I struggled with this for hours until finally I found this article. https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Just copy the useInterval function verbatim, and then use it with the syntax also provided. It just works correctly with no fuss.

Related

setTimeout callback is not being called in UNIT TESTS even after using fakeTimer and runAllTimers

I can't figure out why the callback passed to setTimeout is not called in my unit tests even after using jest.runAllTimers. The useEffect is called but the timeout callbacks aren't called and not covered as per Istanbul code coverage report as well.
The implementation is somewhat like this:
React Component:
const MyComponent = () => {
const [timer, setTimer] = useState(5);
useEffect(() => {
const timeout = setTimeout(() => {
console.log('***** Timer Ran!!! *********');
if(timer <= 5 && timer > 0) setTimer(timer - 1);
else {
return () => clearTimeout(timeout);
}
}, 1000);
}, [timer]);
<>
// some JSX
</>
};
Test:
jest.useFakeTimers(); // at top of file
it('should run code inside useEffect', () => {
const startBtn = screen.getByTestId('start-btn');
expect(startBtn).toBeEnabled();
// more code
jest.runAllTimers();
});
Note: I have tried wrapping jest.runAllTimers in waitFor and act and it doesn't work.
My issue was resolved when I wrapped the rendering of the component with act.
Sample code:
...
jest.useFakeTimers();
...
describe('Test Component', () => {
beforeEach(async () => {
await act(async () => {
render(<MyComponent />);
});
await act(async () => {
jest.runAllTimers();
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should check if ...', async () => {
// tests
});
});

Testing React component -> setInterval

My goal is to test simple component that count down from 60s to 0 and then run a function.
Comonent:
import { useEffect, useState } from "react";
export default function Clock({ dispatch }) {
const [clock, setClock] = useState(60);
useEffect(() => {
const tick = setInterval(() => {
setClock(clock - 1);
}, 1000);
if (clock === 0) {
dispatch({ type: "breakDone" });
}
return function cleanUp() {
clearInterval(tick);
};
});
console.log(clock);
return (
<div className="clock__container">
<h1>{clock}s</h1>
<p data-testid="clock-paragraph">
lorem ipsum
</p>
</div>
);
}
I have a problem with testing component. I have tried to test it with different methods, however my timer have never go down to 59s.
What I have tired:
Gives the worst feedback -> fake positive
test("Value of clock after 1s", async () => {
render(<Clock />);
expect(screen.getByText("60s")).toBeInTheDocument();
setTimeout(() => {
expect(screen.getByText("59s")).toBeInTheDocument();
}, 1000);
});
test("Value of clock after 1s", async () => {
render(<Clock />);
expect(screen.getByText("60s")).toBeInTheDocument();
jest.advanceTimersByTime(10000);
expect(await screen.findByText("59s")).toBeInTheDocument();
});
This solution correctly test 1s warrant, however fails with 10s.
test("Value of clock after 1s", async () => {
render(<Clock />);
const headerElement = screen.getByRole("heading");
jest.advanceTimersByTime(1000);
await waitFor(() => expect(headerElement.innerHTML).toBe("59s"));
});
I have no clue what the problem is to be honest.
Your useEffect hook needs a dependency array for the clock state
like so:
useEffect(() => {
const tick = setInterval(() => {
setClock(clock - 1);
}, 1000);
if (clock === 0) {
dispatch({ type: "breakDone" });
}
return function cleanUp() {
clearInterval(tick);
};
},[clock]);

Jest testing: How do I correctly write expect assertions witin a setTimeout?

I want to JEST test a React Component which updates state after a timeout both before and after rendering state. The expect() assertion works fine in a test() but breaks when used inside a SetTimeout callback. An error states that expect is not defined within the setTimeout callback.
I have created a sandbox here:
https://codesandbox.io/s/jest-delayed-render-test-j5b7l
Simple component update state after SetTimeout.
// delayed.js
import { useEffect, useState } from "react";
export const Delayed = () => {
let [value, setValue] = useState("foo");
useEffect(() => {
let handle = setTimeout(() => {
setValue("bar");
}, 3000);
return () => {
clearTimeout(handle);
};
}, []);
return (
<>
<h1>MyPromise</h1>
<p>value = {value}</p>
</>
);
};
Simple test asserts the component before and update timeout
// delayed.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import { Delayed } from "./delayed";
let container = null;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("checks initial value", async () => {
await act(async () => {
render(<Delayed />, container);
});
expect(container.textContent).toContain("foo");
});
it("checks final value", async () => {
await act(async () => {
render(<Delayed />, container);
});
setTimeout(() => {
// this doesnt get executed!
// error: expect is not defined
expect(container.textContent).toContain("1234");
}, 2000);
});
this is easily fixed passing in done as a param on the test.
it("checks final value", async (done) => {
await act(async () => {
render(<Delayed />, container);
});
setTimeout(() => {
expect(container.textContent).toContain("bar");
done();
}, 3000);
});
as described here:
https://www.pluralsight.com/guides/test-asynchronous-code-jest

Do an API call every few seconds using the Context API and React Hooks

Is it possible to set an auto-refresh interval every few seconds when using the Context API from React? The getData() function runs axios.get() on the API, but still when I try setInterval() and cleanup in the return function of the useEffect hook, it doesn't clean up the interval. getData() sets to the app level state the current and loading variables.
I simply want to refresh and re-do the API call every few seconds. I tried with the useRef() hook and I got it to working, but still the useEffect doesn't clear up the interval once it's finished.
I want to access the current property in the return function of the component and display some data every time an API call is ran.
Here's the code:
const { loading, current, getData } = appContext;
useEffect(() => {
const interval = setInterval(() => {
getData();
console.log('updated');
}, 1000);
return () => clearInterval(interval);
}, []); // eslint-disable-line // also tried without the []
getData() code:
const getData = async () => {
setLoading();
const res = await axios.get(process.env.REACT_APP_APIT);
dispatch({ type: GET_CURRENT, payload: res.data });
};
I had a similar problem and I used to solution described here: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Here is a simple example:
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function App() {
const [intervalTime, setIntervalTime] = useState(2000);
useInterval(() => {
// Do some API call here
setTimeout(() => {
console.log('API call');
}, 500);
}, intervalTime);
return (
<div className="App">
<button onClick={() => setIntervalTime(2000)}>Set interval to 2 seconds</button>
<button onClick={() => setIntervalTime(null)}>Stop interval</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Using the state variable intervalTime you can control the interval time. By setting it to null the interval will stop running.

React hook to wait until the previous call is complete

I am working on a react project which uses hooks. And I was assigned a task
"change the useInterval hook, or create a new one (usePoll?). This should operate the same as useInterval, but should wait until the ajax request is complete before starting the timer".
I am new to react hooks and was looking for a solution for this but could not find. Current useInterval function is as follows.
import React, { useEffect, useRef } from 'react';
export function useInterval(callback, delay, immediate = true) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
if (immediate) {
tick();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
and it use in the program as follows.
useInterval(() => {
get(`/api/v1/streams/1`).then(({ data: { data } }) => {
setStream(data);
});
}, 5000);
and I need to change the useInterval function to wait until the ajax request is complete before starting the timer. It would be great if anyone can help me on this. Thanks
Give this a shot.. it requires calling the next function inside of then but it should come close to what you're looking for.
function useInterval(handler, delay, immediate = true) {
React.useEffect(() => {
let interval
const start = () => {
clearInterval(interval)
interval = setInterval(() => handler(start), delay)
}
handler(start)
return () => clearInterval(interval)
}, [])
}
usage:
useInterval((next) => {
get('/api/v1/streams/1').then(data => {
// tell the timer to begin
next()
})
}, 5000)
You can use async\await to await for first call completes.
Modify internal useEffect like so
export function useInterval(callback, delay, immediate = true) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
// useEffect doesn't like async callbacks (https://github.com/facebook/react/issues/14326) so create nested async callback
(async () => {
// Make tick() async
async function tick() {
await savedCallback.current();
}
if (delay !== null) {
if (immediate) {
await tick(); // Here we should await for tick()
}
let id = setInterval(tick, delay); // Unfortunately setInterval is not async/await compatible. So it will not await for tick
return () => clearInterval(id);
}
})(); // Call nested async function
}, [delay]);
}
And you callback should return Promise so async\await to work properly
useInterval(() => {
// After .then promise will be resolved, so our useInterval will know about that
return get(`/api/v1/streams/1`).then(({ data: { data } }) => {
setStream(data);
});
}, 5000);

Resources