Mocked useHistory is not called in async event handler - reactjs

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

Related

Unit testing onSuccess for a react query call

I have a component that looks like this:
<TestComponent refetch={fn} />
Within TestComponent, I have a save button that fires off a mutation:
const handleSaveClick = () => {
const reqBody: AddHotelRoomDescription = {...}
addHotelRoomDescriptionMutation(reqBody, {
onSuccess: () => {
closeDrawer();
handleRefetch();
}
})
}
Within my test file for this component, I am trying to ensure handleRefetch gets called:
it('successfully submits and calls the onSuccess method', async () => {
const mockId = '1234'
nock(API_URL)
.post(`/admin/${mockId}`)
.reply(200);
const user = userEvent.setup();
const editorEl = screen.getByTestId('editor-input');
await act( async () => { await user.type(editorEl, 'Updating description'); });
expect(await screen.findByText(/Updating description/)).toBeInTheDocument();
const saveButton = screen.getByText('SAVE');
await act( async () => { await user.click(saveButton) });
expect(mockedMutate).toBeCalledTimes(1);
await waitFor(() => {
expect(mockedHandleRefetch).toBeCalledTimes(1); <-- fails here
})
})
I am not sure how to proceed here. I know I can test useQuery calls by doing:
const { result } = renderHook(() => useAddHotelRoomDescription(), { wrapper }
But I think this is for a different situation.
Appreciate the guidance!

React prevent re render

How should I prevent a re render when I click on onClose to close the modal.
It looks like the dispatch function: dispatch(setActiveStep(0) cause some troubles.
export default function ImportOrderModal(props: ImportOrderModalProps) {
const { open, onClose } = props
const {
orderImport: { activeStep }
} = useAppSelector((state: RootState) => state)
const steps = useImportOrderConfig()
const dispatch = useDispatch()
React.useEffect(() => {
getOrdersList()
}, [])
const onCloseModal = () => {
onClose()
// Force a re render because of activeStep value
dispatch(setActiveStep(0))
}
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
return (
<Modal open={open} onClose={onCloseModal}>
<Stepper steps={steps} currentStepNumber={activeStep} />
<FormSteps />
</Modal>
)
}
This block is outside of your useEffect()
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
This will cause rendering troubles.
if you're using an older version of React (<17) that doesn't enforce <React.StrictMode> you can get away with rewriting that as:
useEffect(() => {
getOrderList
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
}, [dispatch]);
if you're using a newer version of React (>18) you will have to cleanup your asynchronous call in the cleanup function of your useEffect().
useEffect(() => {
// This has to be passed down to your fetch/axios call
const controller = new AbortController();
getOrderList(controller)
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
return () => {
// This will abort any ongoing async call
controller.abort();
}
}, [dispatch]);
For this to make sense I will probably have to write an example of the api call for you as well, if you don't mind I'll use axios for the example but it essentially works the same-ish with .fetch.
const getOrderList = async (controller) => {
try {
const { data } = await axios.get("url", { signal: controller.signal });
return data.orders;
} catch (e) {
throw e;
}
}

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

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

Mocking async function jest to test transitive state

I have a component that handles the login part of my app and I would like to test it's loading state.
Basically it does the following:
press a button to login
disable the button
make the request
enable back the button
What is the best way to mock AuthStore.login in this case to be able to test when isLoading is true and when it get back to false ?
I tried mocking with
const loginSpy = jest.spyOn(AuthStore, 'login').mockResolvedValueOnce({ success: true })
but then it returns immediately and I'm not able to test when the button should be disabled.
Here is a sample of the code
const Login = function(){
const [isLoading, setIsLoading] = useState(false)
async function onPressGoogleLogin() {
setIsLoading(true)
const {
success,
error
} = await AuthStore.login()
setIsLoading(false)
}
return ...
}
My test using #testing-library/react-native looks like this.
it.only('testing login', async () => {
const { getByA11yHint } = render(<LoginScreen componentId="id-1" />)
const btn = getByA11yHint('login')
expect(btn).not.toBeDisabled()
await act(async () => {
await fireEvent.press(btn)
})
expect(btn).toBeDisabled()
expect(within(btn).getByTestId('loader')).toBeTruthy()
})
Any ideas ?
Test 1
Mock AuthStore.login() to not return a resolving promise. Mock it to return a pending promise. Inquire about the state of things when the button is pressed.
Test 2
Then, in a separate test, mock AuthStore.login() to return a resolving promise. Inquire about the state of things when the button is pressed.
Test 3
Bonus points: in a separate test, mock AuthStore.login() to return a rejecting promise. Inquire about the state of things when the button is pressed.
So I managed to get it to work in one test (with Jest 27)
import { act, fireEvent, render } from '#testing-library/react-native'
import React, { useCallback, useState } from 'react'
import { Pressable } from 'react-native'
const Utils = {
sleep: (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
}
const Component = function () {
const [refreshing, setRefresh] = useState(false)
const refresh = useCallback(async () => {
setRefresh(true)
await Utils.sleep(5000)
setRefresh(false)
}, [])
return <Pressable testID="btn" onPress={refresh} disabled={refreshing} />
}
const sleepSpy = jest.spyOn(Utils, 'sleep').mockImplementation(() => {
return new Promise((resolve) => {
process.nextTick(resolve)
})
})
describe('Test', () => {
it('test', async () => {
const { getByTestId } = render(<Component />)
const btn = getByTestId('btn')
expect(btn.props.accessibilityState.disabled).toBeFalsy()
let f = fireEvent.press(btn)
expect(sleepSpy).toBeCalledTimes(1)
expect(btn.props.accessibilityState.disabled).toBeTruthy()
await act(async () => {
await f
})
expect(btn.props.accessibilityState.disabled).toBeFalsy()
})
})

Test intermediary state in async handler with React and Enzyme

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

Resources