Unit testing onSuccess for a react query call - reactjs

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!

Related

mock api call in a custom hook using react test library

I have written a custom hook and inside it's useEffect function I am calling an API and set the result into state. Here is my custom hook:
export const useGetUsers = (searchParams?: any | undefined, userId?: string) => {
const [users, setUsers] = useState<{
data: readonly any[] | null;
loading: boolean;
}>({
data: [],
loading: true,
});
const parsedSearchParams = {
limit: 100,
...(searchParams || {}),
};
const searchParamStr = `?${makeQueryStringFromObject(parsedSearchParams)}`;
useEffect(() => {
userRequest('users', 'get', null, searchParamStr)
.then(result => {
setUsers({
data: result?.data,
loading: false,
});
})
.catch(() => {
setUsers({
data: null,
loading: false,
});
});
}, [userId, searchParamStr]);
return { users, setUsers };
};
I want my test to get through .then(). but for some reason it does not. here is my test:
test('when the call is a success', async () => {
const spy = jest.spyOn(ES, 'userRequest');
const returnPromise = Promise.resolve({data: ['a']})
ES.userRequest = jest.fn(() => returnPromise);
const { result, waitFor} = renderHook(() => useGetUsers());
await act(() => returnPromise)
await waitFor(() => expect(spy).toHaveBeenCalled())//this fails
});
here is another try and change I made in my test, but no luck:
test('when the call is a success', async () => {
jest.mock('src/..', () => ({
...jest.requireActual('src/..'),
userRequest: jest
.fn()
.mockImplementation(() => new Promise(resolve => resolve({data: ['a']}))),
}));
const { result, waitFor} = renderHook(() => useGetUsers());
await waitFor(() => expect(ES.userRequest).toHaveBeenCalled())
});
P.S. when I mock userRequest, I expect to have the return value as I mocked. but it fails. it goes to .catch instead
I tried to use waitForNextUpdate, but no luck. I would appreciate your help
This works for me:
import { renderHook, waitFor } from '#testing-library/react';
import { useGetUsers } from '../useGetUsers';
import * as utils from '../useGetUsersUtils';
it('should work', async () => {
const mockUserRequest = jest.spyOn(utils, 'userRequest');
renderHook(() => useGetUsers());
await waitFor(() => expect(mockUserRequest).toHaveBeenCalled())
});
I am not sure where is the userRequest placed in your code. As you can see from my import it is in different file then the hook.

Testing custom hooks with Testing Library/React-Hooks

I have more of a conceptual question regarding testing-library/react-hooks.
I have the following test:
describe('something else', () => {
const mock = jest.fn().mockResolvedValue({ data: [['NL', 'Netherlands'], ['CU', 'Cuba']], status: 200 });
axios.get = mock;
it('calls the api once', async () => {
const setItemMock = jest.fn();
const getItemMock = jest.fn();
global.sessionStorage = jest.fn();
global.sessionStorage.setItem = setItemMock;
global.sessionStorage.getItem = getItemMock;
const { waitFor } = renderHook(() => useCountries());
await waitFor(() => expect(setItemMock).toHaveBeenCalledTimes(0));
});
});
Which test the following custom hook:
import { useEffect, useState } from 'react';
import axios from '../../shared/utils/axiosDefault';
import { locale } from '../../../config/locales/locale';
type TUseCountriesReturnProps = {
countries: [string, string][];
loading: boolean;
error: string;
}
export default function useCountries(): TUseCountriesReturnProps {
const [countries, setCountries] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const sessionStorageCountriesKey = `countries-${locale}`;
useEffect(() => {
const countriesFromStorage = sessionStorage.getItem(sessionStorageCountriesKey);
const getCountries = async () => {
try {
const response = await axios.get('/api/v3/countries', {
params: {
locale,
},
});
console.log(response);
if (response.status === 200) {
setCountries(response.data);
sessionStorage.setItem(sessionStorageCountriesKey, JSON.stringify(response.data));
} else {
console.error(response);
setError(`Error loading countries, ${response.status}`);
}
} catch (error) {
console.error(error);
setError('Failed to load countries');
}
};
if (!countriesFromStorage) {
getCountries();
} else {
setCountries(JSON.parse(countriesFromStorage));
}
setLoading(false);
}, []);
return {
countries,
loading,
error,
};
}
If I change the toHaveBeenCalledTimes(1) to toHaveBeenCalledTimes(0), all of a sudden I get a Warning: An update to TestComponent inside a test was not wrapped in act(...). on
29 | if (response.status === 200) {
> 30 | setCountries(response.data);
And if I do any number higher than 1, it times out. Even if I extend the timeout time to 30 seconds, it just times out. What is happening here. I just don't understand. And all of that makes me wonder if it is even actually running the test correctly.
Alright, I think I figured it out. For some reason the wait for does not work in this situation. I am now doing it as follows and that works in all scenarios:
describe('useCountries', () => {
describe('when initialising without anything in the sessionStorage', () => {
beforeEach(() => {
axios.get.mockResolvedValue({ data: [['NL', 'Netherlands'], ['CU', 'Cuba']], status: 200 });
global.sessionStorage = jest.fn();
global.sessionStorage.getItem = jest.fn();
});
afterEach(() => {
jest.clearAllMocks();
});
it('calls session storage set item once', async () => {
const setItemMock = jest.fn();
global.sessionStorage.setItem = setItemMock;
const { waitForNextUpdate } = renderHook(() => useCountries());
await waitForNextUpdate();
expect(setItemMock).toHaveBeenCalledTimes(1);
});
});
});
So it seems that testing library wants you to just wait for the first update that happens and not until it stops doing things. As soon as it waits for the final results, other updates trigger and those are somehow messing up something internally.. I wish I could be more explicit about why, but as least waitForNextUpdate seems to have fixed my issue.

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

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

How can I run a function when internet connection is established?

The app is a quiz, and if user finishes the round he may send the points in firebase. If user is not connected to internet, I save the points in device memory, so when connection is established the points are send in firebase.
The best would be to let this happen automatically and show a message...
I'm trying to do this in App.js in a useEffect, but it checks only if I refresh the app. I tried withNavigationFocus and useFocusEffect but error: the App.js is unable to get access to navigation....
I could also move the code to WelcomeScreen.js and show a button if connection is established to add the points, but it's not that user friendly.
Any ideas would be appreciated.
Thanks!
useEffect(() => {
const getPoints = async () => {
let points = await AsyncStorage.getItem("savedPoints");
if (!!points) {
const getEmail = async () => {
const userData = await AsyncStorage.getItem("userData");
if (userData) {
const transformedData = JSON.parse(userData);
const { userEmail } = transformedData;
return userEmail;
}
};
const email = await getEmail();
// Give it some time to get the token and userId,
// because saveData needs them.
setTimeout(
async () => await dispatch(dataActions.saveData(email, +points)),
3000
);
await AsyncStorage.removeItem("savedPoints");
}
};
NetInfo.fetch().then(state => {
if (state.isConnected) {
console.log("isConnected");
getPoints();
}
});
}, []);
The solution
WelcomeScreen.js
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
console.log("useEffect welcome");
const unsub = NetInfo.addEventListener(state => {
setIsConnected(state.isConnected);
});
return () => unsub();
}, []);
const getPoints = async () => {
console.log("getPoints welcome");
let points = await AsyncStorage.getItem("savedPoints");
if (!!points) {
const getEmail = async () => {
const userData = await AsyncStorage.getItem("userData");
if (userData) {
// parse converts a string to an object or array
const transformedData = JSON.parse(userData);
const { userEmail } = transformedData;
return userEmail;
}
};
const email = await getEmail();
// Give it some time to get the token and userId,
// because saveData needs them.
setTimeout(
async () => await dispatch(dataActions.saveData(email, +points)),
3000
);
await AsyncStorage.removeItem("savedPoints");
}
};
if (isConnected) getPoints();
You can set up a listener to listen for an internet connection. Don't use any logic in app.js, use it in a separate screen component.
constructor(props) {
super(props);
this.state = {
isConnected: false
};
}
componentDidMount() {
this.listenForInternetConnection = NetInfo.addEventListener(state => {
// your logic is here or setState
this.setState({
isConnected: state.isConnected
});
});
}
componentWillUnmount() {
this.listenForInternetConnection();
}
You can use JS EventListeners
window.addEventListener('load', () => {
navigator.onLine ? showStatus(true) : showStatus(false);
window.addEventListener('online', () => {
showStatus(true);
});
window.addEventListener('offline', () => {
showStatus(false);
});
});

Resources