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