I'm rendering an element that makes use of a setTimeout to change the inner text from a loading state to a desired message:
function Message({ message }: any) {
const [showMessage, setShowMessage] = useState(false);
useEffect(() => {
const CTATimer = setTimeout(() => {
setShowMessage(true);
}, 1500);
return () => {
clearTimeout(CTATimer);
};
}, []);
if (!showMessage) {
return <p>Loading...</p>;
}
return (
<>
<div>{message.text}</div>
</>
);
}
The corresponding test renders, then advances time by 1500ms, and then should show the message. However, currently the test fails and the terminal shows that the text is still Loading...
. The test is written like so:
const mockMessage = {
text: "this is a message",
answers: [],
id: 1,
};
afterEach(() => {
jest.useRealTimers();
});
it("should show message after setTimeout", () => {
jest.useFakeTimers();
jest.advanceTimersByTime(1500);
customRender(<Message message={mockMessage} />); // my customRender is just the default render but with a ThemeProvider wrapper.
const message = screen.getByText(/this is a message/i);
expect(message).toBeInTheDocument();
});
Why would my test still be rendering the loading state when 1500ms have passed?
I'm rendering an element that makes use of a setTimeout to change the inner text from a loading state to a desired message:
function Message({ message }: any) {
const [showMessage, setShowMessage] = useState(false);
useEffect(() => {
const CTATimer = setTimeout(() => {
setShowMessage(true);
}, 1500);
return () => {
clearTimeout(CTATimer);
};
}, []);
if (!showMessage) {
return <p>Loading...</p>;
}
return (
<>
<div>{message.text}</div>
</>
);
}
The corresponding test renders, then advances time by 1500ms, and then should show the message. However, currently the test fails and the terminal shows that the text is still Loading...
. The test is written like so:
const mockMessage = {
text: "this is a message",
answers: [],
id: 1,
};
afterEach(() => {
jest.useRealTimers();
});
it("should show message after setTimeout", () => {
jest.useFakeTimers();
jest.advanceTimersByTime(1500);
customRender(<Message message={mockMessage} />); // my customRender is just the default render but with a ThemeProvider wrapper.
const message = screen.getByText(/this is a message/i);
expect(message).toBeInTheDocument();
});
Why would my test still be rendering the loading state when 1500ms have passed?
Share Improve this question edited Feb 21, 2022 at 5:37 Lin Du 102k135 gold badges332 silver badges564 bronze badges asked Feb 18, 2022 at 13:13 crevuluscrevulus 2,4383 gold badges24 silver badges63 bronze badges2 Answers
Reset to default 11If working with an asynchronous test because you need to use userEvent
for typing etc. I found a solution on this blog: https://onestepcode./testing-library-user-event-with-fake-timers/
The trick is to set the delay
option on the userEvent
to null
.
const user = userEvent.setup({ delay: null });
Here is a full test case
test("Pressing the button hides the text (fake timers)", async () => {
const user = userEvent.setup({ delay: null });
jest.useFakeTimers();
render(<Demo />);
const button = screen.getByRole("button");
await user.click(button);
act(() => {
jest.runAllTimers();
});
const text = screen.queryByText("Hello World!");
expect(text).not.toBeInTheDocument();
jest.useRealTimers();
});
You should advance timers after rendering the ponent. Besides, you should call jest.advanceTimersByTime()
inside act function. Otherwise, it will throws an warning: Warning: An update to Message inside a test was not wrapped in act(...).
index.tsx
:
import React from 'react';
import { useEffect, useState } from 'react';
export function Message({ message }: any) {
const [showMessage, setShowMessage] = useState(false);
useEffect(() => {
const CTATimer = setTimeout(() => {
setShowMessage(true);
}, 1500);
return () => {
clearTimeout(CTATimer);
};
}, []);
if (!showMessage) {
return <p>Loading...</p>;
}
return (
<>
<div>{message.text}</div>
</>
);
}
index.test.tsx
:
import React from 'react';
import { render, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Message } from './';
describe('Message', () => {
const mockMessage = {
text: 'this is a message',
answers: [],
id: 1,
};
afterEach(() => {
jest.useRealTimers();
});
it('should show message after setTimeout', () => {
jest.useFakeTimers();
render(<Message message={mockMessage} />);
act(() => {
jest.advanceTimersByTime(1500);
});
const message = screen.getByText(/this is a message/i);
expect(message).toBeInTheDocument();
});
});
Test result:
PASS stackoverflow/71174071/index.test.tsx (9.705 s)
Message
✓ should show message after setTimeout (27 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.903 s
Ran all test suites related to changed files.