I'm trying to test a component where its value changes based on the result of a promise from a callback that is called after a certain duration.
My component looks like this. I have verified that it works as expected in my browser.
import React, { useEffect, useState } from 'react';
export interface TestComponentProps {
callback: () => Promise<string>
}
const TestComponent: React.FC<TestComponentProps> = ({
callback,
}: TestComponentProps) => {
const [textDisplay, setTextDisplay] = useState('not resolved');
const [timeLeft, setTimeLeft] = useState(1000);
const [timerDone, setTimerDone] = useState(false);
const timeStart = Date.now();
const receiveText = async (text: Promise<string>) => {
setTextDisplay(await text);
}
useEffect(() => {
if (timerDone) {
return () => undefined;
}
// Update screen every 50 ms
const interval = setInterval(() => {
const newTimeLeft = 1000 - Date.now() + timeStart;
setTimeLeft(newTimeLeft);
if (newTimeLeft <= 0) {
setTimerDone(true);
receiveText(callback());
}
}, 50);
return () => clearInterval(interval);
}, [timerDone]);
if (!timerDone) {
return (
<p>{timeLeft} ms to go</p>
);
} else {
return (
<p>{textDisplay}</p>
)
}
};
export default TestComponent;
My test looks like this:
import { render, screen, act } from '@testing-library/react';
import React from 'react';
import TestComponent from './TestComponent';
it('renders the required information after the promise resolves', () => {
jest.useFakeTimers();
const callback = jest.fn().mockResolvedValue('Hello world');
render(<TestComponent callback={callback} />);
// Perform actions to update the state before waiting for the timer as required
// ...
// Now wait for the timer to finish - give it an extra 50ms just in case
act(() => jest.advanceTimersByTime(1050));
// The promise should have resolved and the text should have appeared in the document
expect(screen.getByText('Hello world')).toBeInTheDocument();
// The promise only resolves HERE
// after the test will already have failed
jest.clearAllTimers();
jest.useRealTimers();
});
I cannot await new Promise(process.nextTick)
(as per this question) as this combined with fake timers causes a deadlock.
I have tried instead using waitFor(() => expect(callback).toBeCalled());
as per this question, but it also fails the test - the breakpoints in my code that call the callback are never hit.
How can I ensure that the promise resolves and the component rerenders before I look for the text in the component whilst using fake timers?
I was able to fix this by making the act
callback asynchronous and awaiting it. I have no idea why this worked, but it did.
Here is the fixed test case. The edits are marked with HERE
import { render, screen, act } from '@testing-library/react';
import React from 'react';
import TestComponent from './TestComponent';
// HERE |
// V
it('renders the required information after the promise resolves', async () => {
jest.useFakeTimers();
const callback = jest.fn().mockResolvedValue('Hello world');
render(<TestComponent callback={callback} />);
// Perform actions to update the state before waiting for the timer as required
// ...
// Now wait for the timer to finish - give it an extra 50ms just in case
// HERE |
// V
await act(async () => jest.advanceTimersByTime(1050));
// The promise should have resolved and the text should have appeared in the document
expect(screen.getByText('Hello world')).toBeInTheDocument();
// The promise now resolves before HERE
// meaning the test now passes!
jest.clearAllTimers();
jest.useRealTimers();
});