Test intermediary state in async handler with React and Enzyme - reactjs

Despite reading the documentation of enzyme, and act, I could not find a response to my use case because the examples only show simple use cases.
I have a React component displaying a button. The onClick handler sets a loading boolean and calls an external API. I want to assert that the component shows the loading indicator when we click on the button.
Here is the component:
export default function MyButton(): ReactElement {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<any>(null);
const onClick = async (): Promise<void> => {
setLoading(true);
const response = await fetch('/uri');
setData(await response.json());
setLoading(false);
};
if (loading) {
return <small>Loading...</small>;
}
return (
<div>
<button onClick={onClick}>Click Me!</button>
<div>
{data}
</div>
</div>
);
}
And here is the test:
test('should display Loading...', async () => {
window.fetch = () => Promise.resolve({
json: () => ({
item1: 'item1',
item2: 'item2',
}),
});
const component = mount(<MyButton />);
// Case 1 ✅ => validates the assertion BUT displays the following warning
component.find('button').simulate('click');
// Warning: An update to MyButton 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 [URL to fb removed because SO does not accept it]
// Case 2 ❌ => fails the assertion AND displays the warning above
act(() => {
component.find('button').simulate('click');
});
// Case 3 ❌ => fails the assertion BUT does not display the warning
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
});
As you can see, if I get rid of the warning, my test is not satisfying anymore as it waits for the promise to resolve. How can we assert the intermediate state change while using act?
Thanks.

Just resolve promise manually:
const mockedData = {
json: () => ({
item1: 'item1',
item2: 'item2',
}),
};
let resolver;
window.fetch = () => new Promise((_resolver) => {
resolver = _resolver;
});
// ....
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
resolver(mockedData);
expect(component.debug()).not.toContain('Loading...');
PS but in sake of readability I'd rather have 2 separate tests: one with new Promise(); that never resolves and another with Promise.resolve(mockedData) that would be resolved automatically

Related

Mocking a promise inside useEffect with CRA, React Testing Library and Jest

I am having an issue mocking a returned Promise using:
Create React app
Jest
RTL
I have a file:
const books = [{
id: 1,
name: 'book'
}];
export const getBooks = () =>
new Promise((res) => res(books));
I have a useEffect in my app:
export const App = () => {
const [books, setBooks] = useState(undefined);
useEffect(() => {
const fetchData = async () => {
try {
const response = await getBooks();
setBooks(response);
} catch (error) {
setError("There seems to be an issue. Error:", error);
}
};
fetchData();
}, []);
return (
<div>
{books &&
books.map((book) => {
return (
<li key={book.id}>
{book.name}
</li>
);
})
}
</div>
I have a test:
import { App } from './App';
import { getBooks } from './books';
jest.mock('./books', () => ({
getBooks: jest.fn(),
}));
getBlocks.mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
describe('App', () => {
it('should render blocks', async () => {
await render(<App />);
expect(screen.getByText('mock book')).toBeInTheDocument();
});
});
I just can't mock the return value! I can assert it's been called and I can console log the getBooks to see that it's mocked I just can't get any results. I also want to reject it so I can test the unhappy path but it won't work. Any ideas?
Few things:
You have a typo, it's not getBlocks but getBooks.
The await keyword is not necessary before rendering the component with render.
getBooks returns a promise that resolves with the value of books, yet when you're trying to mock it, you are making it return a function that returns a promise. Very different things.
You have to move the mocking to the test block in which it'll be used, or if you need this mocked value from getBooks on each one of your tests, you can move it inside a beforeEach hook. You can always override it for a specific test in which you are testing some edge case (e.g. an exception being thrown by the function, A.K.A "Unhappy path").
On the component's first render, books will be undefined, so you need to wait for the state to be updated. getByText query won't work, since it will immediately throw an error because it won't find the text you're expecting. You need to use the findByText query for this. It returns a promise that resolves when an element that matches the given query is found and rejects if the element is not found after the default timeout of 1000ms.
Since getBooks returns a promise, it makes more sense to use mockResolvedValue instead of mockReturnValue.
import { render, screen } from "#testing-library/react"
import { App } from "./App"
import { getBooks } from "./books"
jest.mock("./books", () => ({
getBooks: jest.fn()
}))
describe("App", () => {
it("should render blocks", async () => {
getBooks.mockResolvedValueOnce([{ id: 1, name: "mock book" }])
render(<App />)
expect(await screen.findByText("mock book")).toBeInTheDocument()
})
})
Try this:
jest.mock('./books', () => ({
getBooks: jest.fn().mockReturnValue(() => Promise.resolve([{
id: 1,
name: 'mock book'
}]));
}));

Updating state during initialization of a react hook test

When I run my test it kicks off the process of trying to add listeners and enable midi for the hook. This code works in browser but doesn't (intentionally, as per the spec) for the tests (which use jsdom) and I get the following warning
Warning: An update to TestComponent 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 */
Due to the way my code is structured I cannot seperate out the state update.
My test;
describe("midiConnection", () => {
it("Should fail", () => {
act(() => {
const midiPorts = renderHook(() => {MidiConnection()})
})
})
})
My hook;
export function MidiConnection() {
const {array: midiInputs, push: midiPush, filter: midiFilter} = useArray(["none"])
const [error, setError] = useState<Error | undefined>();
useEffect(() => {
WebMidi.addListener("connected", (e) => { if (isInput(e)) {midiPush(e.port.name)}});
WebMidi.addListener("disconnected", (e) => {
e.port.removeListener()
if (isInput(e)) {midiFilter((str) => {return str != e.port.name})}
});
WebMidi.
enable().
catch((err) => {
setError(err)
})
}, [])
return ({ports: midiInputs, error})
}
RenderHook() comes from #testing-library/react-hooks/dom, WebMidi comes from webmidi and UseEffect() comes from next.js
i have tried wrapping RenderHook() in an act() but that doesn't fix the error and im not sure what else to try. What stategy should I use to resolve this warning in my test?

How to correctly test React with act

I'm trying to test a component but it errors with
console.error
Warning: An update to Example 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.org/link/wrap-tests-with-act
at Example (/Users/thr15/Developmemt/Boost/basket-creator/frontend/src/Page/F1/Example.tsx:5:29)
at WrapperComponent (/Users/thr15/Developmemt/Boost/basket-creator/frontend/node_modules/enzyme-adapter-utils/src/createMountWrapper.jsx:49:26)
Here's a simplified version of my component
import {useState} from 'react';
function Example(): JSX.Element {
const [name, setName] = useState('');
const [, setLoading] = useState(false);
const [results, setResults] = useState<number[]>([]);
/**
* Search the baskets.
*/
const search = async () => {
// Let the UI know we're loading
setLoading(true);
// Get the baskets
try {
const baskets: number[] = await (await fetch('/test?name=' + name)).json();
// Give the UI the data
setLoading(false);
setResults(baskets);
} catch (e) {
console.error(e);
}
};
return <div className={"content"}>
<input value={name} onChange={(e) => setName(e.target.value)}/>
<button onClick={search}>Search</button>
{results.length}
</div>
}
export default Example;
and my test so far
import Enzyme, {mount} from 'enzyme';
import Adapter from '#wojtekmaj/enzyme-adapter-react-17';
import Example from "./Example";
Enzyme.configure({adapter: new Adapter()});
describe('Example', () => {
test('searching requests the correct URL', () => {
fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{a: 1}, {b: 2}]),
})
);
let searchButton;
const wrapper = mount(<Example/>);
const input = wrapper.find('input').at(0);
searchButton = wrapper.find('button').at(0);
input.simulate('change', {target: {value: 'Driver Name'}});
searchButton.simulate('click');
expect(searchButton.text()).toBe('Search');
expect(fetch.mock.calls.length).toBe(1);
expect(fetch.mock.calls[0][0]).toBe('/test?name=Driver Name');
});
});
I've tried wrapping act around various parts of the test, and it either still errors, or the name isn't appended to the query string. Can anyone point me in the right direction?
UPDATE:
Working test below for anyone (and probably me!) in the future
describe('Example', () => {
test('searching requests the correct URL', async () => {
fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{a: 1}, {b: 2}]),
})
);
let searchButton: ReactWrapper;
const wrapper = mount(<Example/>);
const input = wrapper.find('input').at(0);
searchButton = wrapper.find('button').at(0);
input.simulate('change', {target: {value: 'Driver Name'}});
await act(async () => {
searchButton.simulate('click');
});
expect(searchButton.text()).toBe('Search');
expect(fetch.mock.calls.length).toBe(1);
expect(fetch.mock.calls[0][0]).toBe('/test?name=Driver Name');
});
});
I'm guessing that it is the clicking of the search button that is generating the act warning.
From react#16.9.0, act was changed to return a promise, meaning that you can avoid these types of warnings when testing async handlers.
Wrapping your search click simulation in an async act, might resolve the warning - but you might have to add a little timeout (not sure how this works with enzyme)
await act(() => {
searchButton.simulate('click');
})
Here are some more resources on the topic that might help you along the way:
secrets of the act(...) api
React’s sync and async act

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();

React testing library: An update inside a test was not wrapped in act(...) & Can't perform a React state update on an unmounted component

In my test, the component receives its props and sets up the component.
This triggers a useEffect to make an http request (which I mock).
The fetched mocked resp data is returned, but the cleanup function inside the useEffect has already been called (hence the component has unmounted), so I get all these errors.
How do I prevent the component from un-mounting so that the state can be updated? I've tried act, no act, nothing causes the component to wait for the fetch to finish.
I should say my warning are just that, warnings, but I don't like all the red, and it indicates something is going wrong.
export const BalanceModule = (props) => {
const [report, setReport] = useState();
useEffect(() => {
fetch('http://.....').then((resp) => {
console.log("data returned!!!")
setReports((report) => {
return {...report, data: resp}
})
})
return () => {
console.log("unmounted!!!")
};
}, [report])
.... trigger update on report here
}
// the test:
test("simplified-version", async () => {
act(() => {
render(
<BalanceModule {...reportConfig}></BalanceModule>
);
});
await screen.findByText("2021-01-20T01:04:38");
expect(screen.getByText("2021-01-20T01:04:38")).toBeTruthy();
});
Try this:
test("simplified-version", async () => {
act(() => {
render(<BalanceModule {...reportConfig}></BalanceModule>);
});
await waitFor(() => {
screen.findByText("2021-01-20T01:04:38");
expect(screen.getByText("2021-01-20T01:04:38")).toBeTruthy();
});
});

Resources