JEST test passes, act not correctly used?

160 Views Asked by At

I do some unit testing, here is my code:

import React from 'react';
import { render, fireEvent, act, waitFor } from '@testing-library/react-native';
import { LoginScreen } from '../login.screen';

describe('Login screen', () => {
  //Testing Login button
  it('should go to project screen on login', async () => {
    const navigationMock = { navigate: jest.fn() };
    const { getByTestId } = render(<LoginScreen navigation={navigationMock} />);

    // Find the login inputs and enter test credentials
    const emailInput = getByTestId('email-input');
    const passwordInput = getByTestId('password-input');

    await act(async () => {
      fireEvent.changeText(emailInput, '[email protected]');
      fireEvent.changeText(passwordInput, 'password');
    });

    // Find the login button and trigger a press event
    const loginButton = getByTestId('login-button');

    await act(async () => {
      fireEvent.press(loginButton);
      await waitFor(() => expect(navigationMock.navigate).toHaveBeenCalledWith('Project'));
    });
  });
});

The JEST test passes, but I get the following error:

ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down. From app/screens/login/tests/login.screen.test.tsx.

  at Timeout._onTimeout (node_modules/react-native/jest/setup.js:417:26)

Honestly, I'm new to coding, so I've used some AI tools like ChatGPT to get some help, but that didn't work.

Here is the login.screen.tsx code:

import React from 'react';
import { SafeAreaView, View, TouchableOpacity, Text } from 'react-native';
import { Button, Card, TextInput } from 'react-native-paper';
// other imports 
import { loginStyle } from './login.style';
import { Image } from 'react-native';
import { Formik } from 'formik';
import * as yup from 'yup';
// other imports

// define interface
interface LoginScreenProps {
  navigation: any;
  // other code
}

const validationSchema = yup.object().shape({
  email: yup.string().email('That is an invalid email!').required('Please enter your email address!'),
  password: yup.string().required('Please enter your password!'),
});

export const LoginScreen = (props: LoginScreenProps) => {
  //console.log(props);
  // other code

  const login = () => props.navigation.navigate('Project');
  // other code

  // Define the type for your form values
  interface LoginFormValues {
    email: string;
    password: string;
  }

  // Handle form values
  const handleFormSubmit = (values: LoginFormValues) => {
    //console.log(values); // Handle form submission logic here
    login();
  };

  return (
    <SafeAreaView style={loginStyle.content}>
      <View style={loginStyle.view}>
        <Image source={require('../images/logo/logo.png')} style={loginStyle.logo} resizeMode="contain" />
        <Card>
          <Card.Title
            title="ABC"
            subtitle="DEF"
            titleStyle={loginStyle.cardTitle}
            subtitleStyle={loginStyle.cardSubtitle}
          />
          <Card.Content>
            <Formik
              initialValues={{ email: '', password: '' }}
              validationSchema={validationSchema}
              onSubmit={handleFormSubmit}
            >
              {({ handleChange, handleBlur, handleSubmit, values, errors }) => (
                <>
                  {/* other code */}

                  <Button
                    onPress={() => handleSubmit()}
                    style={loginStyle.cardButton}
                    mode="contained"
                    testID="login-button"
                  >
                    Login
                  </Button>
                </>
              )}
            </Formik>

            {/* other code */}
          </Card.Content>
        </Card>

        {/* other code */}
      </View>
    </SafeAreaView>
  );
};

// other code
export default LoginScreen;

And here is the example code for the solution for testing my Register button:

//...other code
// Testing Register button
  it('should go to register screen when register', async () => {
    jest.useFakeTimers();
    const navigationMock = { navigate: jest.fn() };
    const { getByTestId } = render(<LoginScreen navigation={navigationMock} />, {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });
    const registerButton = getByTestId('register-button');
    await act(async () => {
      fireEvent.press(registerButton);
    });
    act(() => {
      jest.runAllTimers();
    });
      expect(navigationMock.navigate).toHaveBeenCalledWith('Register');
      jest.useRealTimers();
    });
// other code...
1

There are 1 best solutions below

0
Mister_CK On

I've seen that error a couple of times, it generally has to do with applying your asynchronous test code improperly. It is definetly an issue in your testCode and not in your component. I think the issue is that you use a nested await waitFor() inside you await act(). It should all work without having to apply jest.useFakeTimers(), and I would avoid using that, because it might lead to problems down the road. Also, don't put multiple fireEvents in 1 async act().

I think this should work:

it('should go to project screen on login', async () => {
    const navigationMock = { navigate: jest.fn() };
    const { getByTestId } = render(<LoginScreen navigation={navigationMock} />);

    // Find the login inputs and enter test credentials
    const emailInput = getByTestId('email-input');
    const passwordInput = getByTestId('password-input');

    fireEvent.changeText(emailInput, '[email protected]');
    fireEvent.changeText(passwordInput, 'password');

    // Find the login button and trigger a press event
    const loginButton = getByTestId('login-button');
    fireEvent.press(loginButton);
      
    await waitFor(() => 
      expect(navigationMock.navigate).toHaveBeenCalledWith('Project'));
    });