I'm having trouble using react-testing-library to test a toggle ponent.
On click of an icon (wrapped in a button ponent), I am expecting text to go from 'verified' to 'unverified'. In addition, a function is called where there are state updates.
However, the click event doesn't seem to work, and I am getting the below error:
> jest "MyFile.spec.tsx"
FAIL src/my/path/__tests__/MyFile.spec.tsx
ponent MyFile
✓ renders when opened (94 ms)
✓ renders with items (33 ms)
✕ toggles verification status on click of icon button (100 ms)
console.error
Warning: An update to MyFile inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at
at MyFile (/path/to/myfile.tsx:44:3)
at ThemeProvider (/users/node_modules/@material-ui/styles/ThemeProvider/ThemeProvider.js:48:24)
123 | );
124 | } finally {
> 125 | setIsLoading(false);
| ^
126 | }
127 | };
128 |
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-dom/cjs/react-dom.development.js:24064:9)
at dispatchAction (node_modules/react-dom/cjs/react-dom.development.js:16135:9)
at handleConfirm (src/modules/myfile.tsx:125:7)
In my code, I have a function like this:
const handleSubmit = async() => {
if(isLoading) {
return;
}
try {
setIsLoading(true);
await myFunctionCalls();
} catch (error){
console.log(error)
} finally {
setIsLoading(false)
}
};
My test looks similar to this:
test('toggles verification status on click of icon button', async () => {
renderWithTheme(
<MyComponent/>,
);
const updateVerificationMock = jest.fn();
const callFunctionWithSerializedPayloadMock =
callFunctionWithSerializedPayload as jest.Mock;
callFunctionWithSerializedPayloadMock.mockImplementation(
() => updateVerificationMock,
);
const button = screen.getByRole('button', {name: 'Remove approval'});
fireEvent.click(button);
await act(async () => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1);
});
expect(await screen.findByText('unverified')).toBeInTheDocument();
});
The first expect passes as the function calls are called once, however I have the act() error from above, and also there is a failure as it seems that the text does not toggle from verified
to unverified
.
I am aware that usually the act error is an issue of async/waiting for calls to happen, but I thought that findByText should wait, and it seems like there is another issue I'm not catching here. Any help on what to do to debug/improve this test?
I'm having trouble using react-testing-library to test a toggle ponent.
On click of an icon (wrapped in a button ponent), I am expecting text to go from 'verified' to 'unverified'. In addition, a function is called where there are state updates.
However, the click event doesn't seem to work, and I am getting the below error:
> jest "MyFile.spec.tsx"
FAIL src/my/path/__tests__/MyFile.spec.tsx
ponent MyFile
✓ renders when opened (94 ms)
✓ renders with items (33 ms)
✕ toggles verification status on click of icon button (100 ms)
console.error
Warning: An update to MyFile inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs/link/wrap-tests-with-act
at MyFile (/path/to/myfile.tsx:44:3)
at ThemeProvider (/users/node_modules/@material-ui/styles/ThemeProvider/ThemeProvider.js:48:24)
123 | );
124 | } finally {
> 125 | setIsLoading(false);
| ^
126 | }
127 | };
128 |
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-dom/cjs/react-dom.development.js:24064:9)
at dispatchAction (node_modules/react-dom/cjs/react-dom.development.js:16135:9)
at handleConfirm (src/modules/myfile.tsx:125:7)
In my code, I have a function like this:
const handleSubmit = async() => {
if(isLoading) {
return;
}
try {
setIsLoading(true);
await myFunctionCalls();
} catch (error){
console.log(error)
} finally {
setIsLoading(false)
}
};
My test looks similar to this:
test('toggles verification status on click of icon button', async () => {
renderWithTheme(
<MyComponent/>,
);
const updateVerificationMock = jest.fn();
const callFunctionWithSerializedPayloadMock =
callFunctionWithSerializedPayload as jest.Mock;
callFunctionWithSerializedPayloadMock.mockImplementation(
() => updateVerificationMock,
);
const button = screen.getByRole('button', {name: 'Remove approval'});
fireEvent.click(button);
await act(async () => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1);
});
expect(await screen.findByText('unverified')).toBeInTheDocument();
});
The first expect passes as the function calls are called once, however I have the act() error from above, and also there is a failure as it seems that the text does not toggle from verified
to unverified
.
I am aware that usually the act error is an issue of async/waiting for calls to happen, but I thought that findByText should wait, and it seems like there is another issue I'm not catching here. Any help on what to do to debug/improve this test?
Share Improve this question edited Jun 30, 2021 at 11:13 stonerose036 asked Jun 30, 2021 at 10:22 stonerose036stonerose036 3014 silver badges18 bronze badges2 Answers
Reset to default 1There are 3 async functions that are called here when you click on the Remove Approval
button.
First, you are setting the loading state to true, so it will load then the async function (myFunctionCalls) is called, and finally, the loader will disappear after the loading state is set to false.
In order to solve it, we have to wait for the loading to appear first, then myFunctionCalls is called, and then later we have to wait for loading to disappear.
test("toggles verification status on click of icon button", async () => {
renderWithTheme(<MyComponent />);
const updateVerificationMock = jest.fn();
const callFunctionWithSerializedPayloadMock =
callFunctionWithSerializedPayload as jest.Mock;
callFunctionWithSerializedPayloadMock.mockImplementation(
() => updateVerificationMock
);
const button = screen.getByRole("button", { name: "Remove approval" });
fireEvent.click(button);
expect(await screen.findByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1);
});
await waitForTheElementToBeRemoved(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
expect(await screen.findByText("unverified")).toBeInTheDocument();
});
If you do not have loading text then you can use act(() => jest.advanceTimersByTime(500));
for extending the time till 500ms. When the time reaches 500ms, the async function would have been resolved.
beforeEach(() => {
jest.useFakeTimers();
})
afterEach(() => {
jest.runAllPendingTimers();
jest.useRealTimers()
})
test("toggles verification status on click of icon button", async () => {
renderWithTheme(<MyComponent />);
const updateVerificationMock = jest.fn();
const callFunctionWithSerializedPayloadMock =
callFunctionWithSerializedPayload as jest.Mock;
callFunctionWithSerializedPayloadMock.mockImplementation(
() => updateVerificationMock
);
const button = screen.getByRole("button", { name: "Remove approval" });
fireEvent.click(button);
act(() => jest.advanceTimersByTime(500));
await waitFor(() => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1);
});
act(() => jest.advanceTimersByTime(500));
expect(await screen.findByText("unverified")).toBeInTheDocument();
});
Try this:
// [...]
fireEvent.click(button);
await waitFor(() => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1),
expect(screen.findByText('unverified')).toBeInTheDocument()
});
// End of test