An update to BrowserRouter inside a test was not wrapped in act - reactjs

I'm trying to implement my first tests in react with react-test-library, but I came across this certain problem where there's warning that my component is not wrapped in act(..)
down below is the test that I'm trying to implement
import { BrowserRouter as Router } from "react-router-dom";
beforeEach(() => {
container = render(
<Router>
<Search />
</Router>
);
});
it("handleClick", async () => {
const button = container.getByText("Search");
const event = fireEvent.click(button);
expect(event).toBeTruthy();
});
and Here is the function that I'm trying to test
const handleClick = async () => {
setLoading(true);
const data = await movieAPI.fetchMovieByTitle(movie);
setLoading(false);
navigate(`/movie/${data.Title}`, { state: data });
};

Turns out that before doing the assertions we need to wait for the component update to fully complete with waitFor
it("should render spinner", async () => {
const button = container.getByText("Search");
const event = await fireEvent.click(button);
const spinner = container.getByTestId("spinner");
await waitFor(() => {
expect(spinner).toBeInTheDocument();
});
});

Related

Mocking the content of an iframe with React Testing Library

I've got a React component that returns an iframe and handles sending messages to and receiving messages from the iframe. I am totally stumped as to how I can mock the iframe and simulate a message being posted in the iframe with React Testing Library. Any ideas or examples would be very helpful.
Component:
const ContextualHelpClient: React.VFC<CHCProps> = ({ onAction, data }) => {
const [iframe, setIframe] = React.useState<HTMLIFrameElement | null>(null);
const handleMessage = React.useCallback((event: MessageEvent<ContextualHelpAction>) => {
onAction(event.data);
}, [onAction])
const contentWindow = iframe?.contentWindow;
React.useEffect(() => {
if (contentWindow) {
contentWindow.addEventListener('message', handleMessage);
return () => contentWindow.removeEventListener('message', handleMessage);
}
}, [handleMessage, contentWindow]);
React.useEffect(() => {
if (contentWindow) {
contentWindow.postMessage({ type: CONTEXTUAL_HELP_CLIENT_DATA, data }, "/");
}
}, [data, contentWindow]);
return <iframe src={IFRAME_SOURCE} width={300} height={300} ref={setIframe} title={IFRAME_TITLE} />;
};
Test:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import ContextualHelpClient from '../ContextualHelpClient';
import { IFRAME_SOURCE, IFRAME_TITLE } from '../constants';
describe('<ContextualHelpClient />', () => {
it('should call onAction when a message is posted', () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE);
fireEvent(iframe.contentWindow, new MessageEvent('data'));
expect(handleAction).toBeCalled(); // fails
});
});
In my case, using the message type on fireEvent was enough to test this.
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import ContextualHelpClient from '../ContextualHelpClient';
import { IFRAME_SOURCE, IFRAME_TITLE } from '../constants';
describe('<ContextualHelpClient />', () => {
it('should call onAction when a message is posted', () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE);
fireEvent(
window,
new MessageEvent('message', {origin: window.location.origin}),
);
expect(handleAction).toBeCalled(); // fails
});
});
Solved!
it('should call onAction when a message is posted', async () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE) as HTMLIFrameElement;
const TEST_MESSAGE = 'TEST_MESSAGE';
// https://github.com/facebook/jest/issues/6765
iframe.contentWindow?.postMessage(TEST_MESSAGE, window.location.origin);
await waitFor(() => expect(handleAction).toBeCalledWith(TEST_MESSAGE));
});

Mocked useHistory is not called in async event handler

Summary
I'm writing test code for my react app, but somehow, it always fails.
My app code is very simple, there is only one button, and if it's clicked, a function handleSubmit is fired.
What the handler does are
Fetching data from backend(This is async function)
Move to /complete page.
What I did
I mocked the function fetching data from API in test code
I mocked the useHistory in test code
Note
I realized that if the line that is fetching data from API is commented out, the test will pass.
Code
My main app code
import { useFetchDataFromAPI } from '#/usecase/useFetchDataFromAPI';
:
const { fetchDataFromAPI } = useFetchDataFromAPI();
:
const handleSubmit = async () => {
// If the line below is not commented out, test will fail
// const { id } = await fetchDataFromAPI();
history.push(`/complete`);
};
return (
<>
<button onClick={handleSubmit}>Button</button>
</>
My test code
:
jest.mock('#/usecase/useFetchDataFromAPI', () => ({
useFetchDataFromAPI: () => {
return { fetchDataFromAPI: jest.fn((): number => {
return 1;
})}
}
}));
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom') as any,
useHistory: () => ({
push: mockHistoryPush,
}),
}));
:
const renderApplicationWithRouterHistory = () => {
const history = createMemoryHistory();
const wrapper = render(
<Router history={history}>
<Application />
</Router>
);
return { ...wrapper, history };
};
:
describe('Test onClick handler', async () => {
test('Submit', () => {
const { getByText, getByRole } = renderApplication();
const elementSubmit = getByText('Button');
expect(elementSubmit).toBeInTheDocument();
fireEvent.click(elementSubmit);
expect(mockHistoryPush).toHaveBeenCalled();
});
});
Your event handler is called on button click, but because it is asynchronous, its result is not evaluated until after your test runs. In this particular case, you don't need the async behavior, so just use:
const handleSubmit = () => {
history.push(`/complete`)
}
testing-library provides a method waitFor for this if your handler did need to await something:
await waitFor(() => expect(mockHistoryPush).toHaveBeenCalled())
Though another simple way is to simply await a promise in your test so that the expectation is delayed by a tick:
fireEvent.click(elementSubmit);
await Promise.resolve();
expect(mockHistoryPush).toHaveBeenCalled();

error when creating multiple asynchronous tests with act function jest js

in this case I am doing an example project which tries to show a phrase and the author of the phrase the first time the component is loaded and when a button is clicked to obtain a new phrase. The problem with testing is I am trying to simulate the way the user interacts with the application and having two tests asynchronously where each one has the act () function I get the following error:
console.error node_modules/react-dom/cjs/react-dom-test-utils.development.js:87
Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer 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.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer 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.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer 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.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
The test is passing but it generates this alert. I already looked for alternatives such as avoiding asynchronous testing but when doing it the test does not pass, it forces me to put the await in the act.
This example is simple but I would like to apply this same test in more complex applications to verify the correct operation of it. So it is probably necessary to have more asynchronous tests.
Here is the container component:
import React, {
useEffect,
useState,
} from "react";
import Quote from "./quote.component";
import { getQuote } from "../../repository/quote.repository";
const QuoteContainer = () => {
const [quote, setQuote] = useState("");
const [author, setAuthor] = useState("");
const [isLoading, setLoading] = useState(true);
useEffect(() => {
getQuoteData();
}, []);
const newQuoteHandler = () => {
getQuoteData();
};
const getQuoteData = async () => {
const {
quoteText,
quoteAuthor,
} = await getQuote();
setAuthor(quoteAuthor);
setQuote(quoteText);
setLoading(false);
};
return (
<Quote
isLoading={isLoading}
quote={quote}
author={author}
newQuoteHandler={newQuoteHandler}
/>
);
};
export default QuoteContainer;
And the test related to the container:
import React from "react";
import { server, rest } from "../../mocks/server";
import waitForExpect from "wait-for-expect";
import { mount } from "../../enzymeConfig";
import QuoteContainer from "./quote.container";
import { act } from "react-dom/test-utils";
import { QuoteText } from "../quote/quote.style";
import { SpinnerContainer } from "../withSpinner/withSpinner.style";
describe("testing quote api", () => {
it("should render spinner on start", () => {
const wrapper = mount(<QuoteContainer />);
expect(
wrapper.find(SpinnerContainer)
).toHaveLength(1);
});
it("should render actual component on load information", async (done) => {
expect.assertions(1);
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
});
it("should change quote and author when Quote Button click", async (done) => {
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
rest.get(
"apiURL",
(req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
quoteText: "this is an other frase",
quoteAuthor: "Plutarch ",
senderName: "",
senderLink: "",
})
)
);
wrapper
.find(CustomButton)
.at(0)
.simulate("click");
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual("this is an other frase");
done();
});
});
});
});
Thanks from now

Warning: An update to App inside a test was not wrapped in act(...) in enzyme and hooks

I have written this component. it fetchs data using hooks and state. Once it is fetched the loading state is changed to false and show the sidebar.
I faced a problem with Jest and Enzyme, as it does throw a warning for Act in my unit test. once I add the act to my jest and enzyme the test is failed!
// #flow
import React, { useEffect, useState } from 'react';
import Sidebar from '../components/Sidebar';
import fetchData from '../apiWrappers/fetchData';
const App = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const getData = async () => {
try {
const newData = await fetchData();
setData(newData);
setLoading(false);
}
catch (e) {
setLoading(false);
}
};
getData();
// eslint-disable-next-line
}, []);
return (
<>
{!loading
? <Sidebar />
: <span>Loading List</span>}
</>
);
};
export default App;
And, I have added a test like this which works perfectly.
import React from 'react';
import { mount } from 'enzyme';
import fetchData from '../apiWrappers/fetchData';
import data from '../data/data.json';
import App from './App';
jest.mock('../apiWrappers/fetchData');
const getData = Promise.resolve(data);
fetchData.mockReturnValue(getData);
describe('<App/> Rendering using enzyme', () => {
beforeEach(() => {
fetchData.mockClear();
});
test('After loading', async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
wrapper.update();
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
});
So, I got a warning:
Warning: An update to App 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 */
});
I did resolve the warning like this using { act } react-dom/test-utils.
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import fetchData from '../apiWrappers/fetchData';
import data from '../data/data.json';
import App from './App';
jest.mock('../apiWrappers/fetchData');
const getData = Promise.resolve(data);
fetchData.mockReturnValue(getData);
describe('<App/> Rendering using enzyme', () => {
beforeEach(() => {
fetchData.mockClear();
});
test('After loading', async () => {
await act(async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
wrapper.update();
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
});
});
But, then my test is failed.
<App/> Rendering using enzyme › After loading
expect(received).toEqual(expected) // deep equality
Expected: false
Received: true
35 |
36 | wrapper.update();
> 37 | expect(wrapper.find('span').exists()).toEqual(false);
Does anybody know why it fails? Thanks!
"react": "16.13.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
This issue is not new at all. You can read the full discussion here: https://github.com/enzymejs/enzyme/issues/2073.
To sum up, currently in order to fix act warning, you have to wait a bit before update your wrapper as following:
const waitForComponentToPaint = async (wrapper) => {
await act(async () => {
await new Promise(resolve => setTimeout(resolve));
wrapper.update();
});
};
test('After loading', async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
// before the state updated
await waitForComponentToPaint(wrapper);
// after the state updated
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
You should not wrap your whole test in act, just the part that will cause state of your component to update.
Something like the below should solve your problem.
test('After loading', async () => {
await act(async () => {
const wrapper = mount(<App />);
});
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
await act(async () => {
wrapper.update();
})
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});

How do I test next/link (Nextjs) getting page redirect after onClick in React?

I want to test the page get redirected after click on div, but I dont know how, this is my code. Thank you so much
<div className="bellTest">
<NextLink href="/notifications">
<Bell />
</NextLink>
</div>
test.tsx
jest.mock('next/link', () => {
return ({ children }) => {
return children;
};
});
describe('Test of <HeaderContainer>', () => {
test('Test the page redirect after click', async done => {
const wrapper = mount( <HeaderComponent /> );
await wrapper
.find('.bellTest')
.at(0)
.simulate('click');
// expect the page getting redirect
});
});
Instead of mocking next/link you can register a spy on router events, and check if it was called.
The test will look like this:
import Router from 'next/router';
describe('Test of <HeaderContainer>', () => {
const spies: any = {};
beforeEach(() => {
spies.routerChangeStart = jest.fn();
Router.events.on('routeChangeStart', spies.routerChangeStart);
});
afterEach(() => {
Router.events.off('routeChangeStart', spies.routerChangeStart);
});
test('Test the page redirect after click', async done => {
const wrapper = mount(<HeaderComponent />);
await wrapper
.find('.bellTest')
.at(0)
.simulate('click');
expect(spies.routerChangeStart).toHaveBeenCalledWith('expect-url-here');
});
});

Resources