Reputation: 1180
I am trying to write some tests for a React Native component built using Formik. It's a simple form asking for username and password, and I want to use a validation schema built using Yup.
When I use the emulator and test the form manually, the form behaves as expected, error messages show up only when the input values are invalid.
However, when I try to write some automated tests with @testing-library/react-native
, the behavior is not what I am expecting. The error messages show up in tests even if the provided values are valid. Below are the code:
// App.test.js
import React from 'react';
import { render, act, fireEvent } from '@testing-library/react-native';
import App from '../App';
it('does not show error messages when input values are valid', async () => {
const {
findByPlaceholderText,
getByPlaceholderText,
getByText,
queryAllByText,
} = render(<App />);
const usernameInput = await findByPlaceholderText('Username');
const passwordInput = getByPlaceholderText('Password');
const submitButton = getByText('Submit');
await act(async () => {
fireEvent.changeText(usernameInput, 'testUser');
fireEvent.changeText(passwordInput, 'password');
fireEvent.press(submitButton);
});
expect(queryAllByText('This field is required')).toHaveLength(0);
});
// App.js
import React from 'react';
import { TextInput, Button, Text, View } from 'react-native';
import { Formik } from 'formik';
import * as Yup from 'yup';
const Schema = Yup.object().shape({
username: Yup.string().required('This field is required'),
password: Yup.string().required('This field is required'),
});
export default function App() {
return (
<View>
<Formik
initialValues={{ username: '', password: '' }}
validationSchema={Schema}
onSubmit={(values) => console.log(values)}>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
touched,
validateForm,
}) => {
return (
<>
<View>
<TextInput
onChangeText={handleChange('username')}
onBlur={handleBlur('username')}
value={values.username}
placeholder="Username"
/>
{errors.username && touched.username && (
<Text>{errors.username}</Text>
)}
</View>
<View>
<TextInput
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
value={values.password}
placeholder="Password"
/>
{errors.password && touched.password && (
<Text>{errors.password}</Text>
)}
</View>
<View>
<Button
onPress={handleSubmit}
// If I explicitly call validateForm(), the test will pass
// onPress={async () => {
// await validateForm();
// handleSubmit();
// }}
title="Submit"
/>
</View>
</>
);
}}
</Formik>
</View>
);
}
I am not sure whether I am writing the test correctly. I think Formik will automatically validate the form when the handleSubmit
function is called.
Within the App.js
, if I explicitly call the validateForm
, the test will pass. However, it's not feeling right to change the implementation of the onPress
handler just to cater for the test. Maybe I am missing some fundamental concepts around this issue. Any insights would be helpful, thank you.
Package versions:
"@testing-library/react-native": "^7.1.0",
"formik": "^2.2.6",
"react": "16.13.1",
"react-native": "0.63.4",
"yup": "^0.32.8"
Upvotes: 2
Views: 3095
Reputation: 1180
Finally got the time to revisit this issue. Although I am not 100% sure what's going on under the hood yet, I think the result I found might benefit others so I will share it here.
This problem is intertwined with two sub-problems. This first one is related to the Promise
used in React Native
's module, the second one is related to the asynchronous nature of validation with Formik
Below is the code within App.test.js
after modification, while keeping the App.js
is unchanged,
// App.test.js
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import App from '../App';
it('does not show error messages when input values are valid', async () => {
const { getByPlaceholderText, getByText, queryAllByText } = render(<App />);
const usernameInput = getByPlaceholderText('Username');
const passwordInput = getByPlaceholderText('Password');
const submitButton = getByText('Submit');
await waitFor(() => {
fireEvent.changeText(usernameInput, 'testUser');
});
await waitFor(() => {
fireEvent.changeText(passwordInput, 'password');
});
fireEvent.press(submitButton);
await waitFor(() => {
expect(queryAllByText('This field is required')).toHaveLength(0);
});
});
Normally, we do not need to use act
to wrap the fireEvent
as the fireEvent
is already wrapped with act
by default thanks to the testing-library
. However, since Formik
performs validation asynchronously after the text value is changed, and the validation function is not managed by React
's callstack, so we need to manually wrap the fireEvent
call with an act
, or another convenient method: waitFor
. In short, we need to wrap the fireEvent.changeText
with a waitFor
due to its asynchronicity.
However, changing the code to the above format does not solve all the problems. Although the test is passing, you will encounter warnings related to act
. This is an known issue related to Promise
as React Native's Jest preset overrides native Promise. (https://github.com/facebook/react-native/issues/29303)
If you comment out the line
global.Promise = jest.requireActual('promise');
in node_modules/react-native/jest/setup.js
at around line 20, this issue will be solved. But directly modifying the files in node_modules
is NOT recommended. A workaround would be setting up a jest preset to restore the native Promise like here (https://github.com/sbalay/without_await/commit/64a76486f31bdc41f5c240d28263285683755938)
Upvotes: 10