Jest not exiting properly when using RTK-Query useLazyQuery with ReactNative
I'm trying to test some functionality I've written using RTK-Query. Ive created an api using createApi and have exported the useLazyQuery hook. I am calling this hook and then listening for results changes in a useEffect hook. This works as intended in the app. When I try and write a test for this logic using msw, and @testing-library/react-native I am running into errors.
When I run my tests I see the following console output:
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
The --detectOpenHandles flag does not help.
My test looks like this:
const server = setupServer();
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({
navigate: mockedNavigate,
}),
};
});
describe('ForgotPasswordForm', () => {
const storeRef = setupApiStore(forgotPasswordApi);
const testRender = () =>
render(
<Provider store={storeRef.store}>
<ForgotPasswordForm />
</Provider>
);
beforeAll(() => {
jest.spyOn(Alert, 'alert');
server.listen();
});
beforeEach(() => {
storeRef.store.dispatch(forgotPasswordApi.util.resetApiState());
});
afterEach(() => {
jest.resetAllMocks();
server.resetHandlers();
cleanup();
});
afterAll(() => {
server.close();
});
it('should display an error alert if the email is not registered', async () => {
server.use(
rest.get(`${API_ENDPOINT}/ResetPassword`, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'error' }))
)
);
const { getByText, getByPlaceholderText } = testRender();
fireEvent.changeText(
getByPlaceholderText('Registered email address'),
'[email protected]'
);
fireEvent.press(getByText(/Retrieve Password/i));
await waitFor(() =>
expect(Alert.alert).toHaveBeenCalledWith(
'Error',
'An error has occured. Please contact us for help.'
)
);
});
});
My API looks like this:
export const forgotPasswordApi = createApi({
reducerPath: 'forgotPassword',
baseQuery: fetchBaseQuery({
baseUrl: API_ENDPOINT,
}),
endpoints: (builder) => ({
resetPassword: builder.query({
query: (email) => ({
url: '/ResetPassword',
params: { email },
}),
}),
}),
});
export const { useLazyResetPasswordQuery } = forgotPasswordApi;
My component looks like this:
const ForgotPasswordForm = () => {
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
const [email, setEmail] = useState('');
const [showInvalidEmailMessage, setShowInvalidEmailMessage] = useState(false);
const handleEmailChange = (value: string) => {
setEmail(value);
};
const [triggerResetPasswordQuery, results] = useLazyResetPasswordQuery();
useEffect(() => {
if (results.isUninitialized || results.isFetching) return;
if (results?.data?.status === 'error') {
Alert.alert('Error', 'An error has occured. Please contact us for help.');
} else {
Alert.alert('An email has been sent with further instructions.');
}
}, [results]);
const handleForgotPassword = () => {
const isEmailFormatValid = /^\S+@\S+\.\S+$/.test(email);
if (isEmailFormatValid) {
setShowInvalidEmailMessage(false);
triggerResetPasswordQuery(email);
} else {
setShowInvalidEmailMessage(true);
}
};
return (
<>
<Wrapper width="100%" mt={40} mb={20}>
<TextInput
value={email}
placeholder="Registered email address"
handleOnChangeText={handleEmailChange}
accessibilityLabel="forgot-password-email"
/>
</Wrapper>
<Wrapper mb={10} width="100%">
<Button
fullWidth
title="Retrieve Password"
onPress={handleForgotPassword}
/>
</Wrapper>
{results.isLoading && <LoadingOverlay text="Sending Email" />}
</>
);
};
export default ForgotPasswordForm;
Thanks.
Solution 1:
I was facing a similar issue, but in my case, I wasn't using the lazy version of the hook generated by RTK Query. I fixed it by preventing the API from keeping the data when the code is running in the test environment. A code example of my solution:
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'any-base-url',
}),
keepUnusedDataFor: process.env.NODE_ENV !== 'test' ? 60 : 0, // <- here
endpoints: () => {
return {
// your endpoints
};
},
});
Reference to keepUnusedDataFor
: https://redux-toolkit.js.org/rtk-query/api/createApi#keepunuseddatafor