MENU
Timers
(See React's archived Testing Recipes: Timers.)
Your code might use timer-based functions like setTimeout to schedule more work in the future. In this example, a multiple choice panel waits for a selection and advances, timing out if a selection isn't made in 5 seconds:
// card.js
import { useEffect } from "react";
export default function Card(props) {
useEffect(() => {
const timeoutID = setTimeout(() => {
props.onSelect(null);
}, 5000);
return () => {
clearTimeout(timeoutID);
};
}, [props.onSelect]);
return [1, 2, 3, 4].map(choice => (
<button
key={choice}
data-testid={choice}
onClick={() => props.onSelect(choice)}
>
{choice}
</button>
));
}We can write tests for this component by leveraging Jest's timer mocks, and testing the different states it can be in.
// card.test.js
import { createRoot } from "react-dom/client";
import { act } from "react";
import Card from "./card";
let container = null;
let root = null;
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
jest.useFakeTimers();
});
afterEach(() => {
// cleanup on exiting
act(() => {
root.unmount();
});
container.remove();
container = null;
jest.useRealTimers();
});
it("should select null after timing out", () => {
const onSelect = jest.fn();
act(() => {
root.render(<Card onSelect={onSelect} />);
});
// move ahead in time by 100ms
act(() => {
jest.advanceTimersByTime(100);
});
expect(onSelect).not.toHaveBeenCalled();
// and then move ahead by 5 seconds
act(() => {
jest.advanceTimersByTime(5000);
});
expect(onSelect).toHaveBeenCalledWith(null);
});
it("should cleanup on being removed", () => {
const onSelect = jest.fn();
act(() => {
root.render(<Card onSelect={onSelect} />);
});
act(() => {
jest.advanceTimersByTime(100);
});
expect(onSelect).not.toHaveBeenCalled();
// unmount the app by rendering nothing (runs the Effect's cleanup function)
act(() => {
root.render(null);
});
act(() => {
jest.advanceTimersByTime(5000);
});
expect(onSelect).not.toHaveBeenCalled();
});
it("should accept selections", () => {
const onSelect = jest.fn();
act(() => {
root.render(<Card onSelect={onSelect} />);
});
act(() => {
container
.querySelector("[data-testid='2']")
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onSelect).toHaveBeenCalledWith(2);
});You can use fake timers only in some tests. Above, we enabled them by calling jest.useFakeTimers(). The main advantage they provide is that your test doesn't have to wait five seconds to execute, and you also didn't need to make the component code more convoluted just for testing.