reactjsgraphqlreact-testing-libraryformik

React Testing Library doesn't see click on submit button


I am struggled with writing tests to my simple form:

import { useFormik } from 'formik';
import * as yup from 'yup';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import dayjs from 'dayjs';
import Stack from '@mui/material/Stack';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker';

const validationSchema = yup.object({
  firstName: yup
    .string('Enter your first name')
    .required('First name is required'),
  lastName: yup
    .string('Enter your last name')
    .required('Last Name is required'),
  email: yup
    .string('Enter your email')
    .email('Enter a valid email')
    .required('Email is required'),
  date: yup.string('Enter your date').required('Date is required'),
  // .min(Date.now(), 'Start Date must be later than today'),
});

export const AddUserForm = ({ addUser }) => {
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
      date: dayjs(),
    },
    validationSchema: validationSchema,
    onSubmit: (values) => {
      addUser({ variables: values });
    },
  });

  return (
    <div data-testid="addUserForm">
      <form onSubmit={formik.handleSubmit}>
        <Stack spacing={3}>
          <TextField
            fullWidth
            id="firstName"
            name="firstName"
            label="First Name"
            value={formik.values.firstName}
            onChange={formik.handleChange}
            error={formik.touched.firstName && Boolean(formik.errors.firstName)}
            helperText={formik.touched.firstName && formik.errors.firstName}
          />
          <TextField
            fullWidth
            id="lastName"
            name="lastName"
            label="Last Name"
            value={formik.values.lastName}
            onChange={formik.handleChange}
            error={formik.touched.lastName && Boolean(formik.errors.lastName)}
            helperText={formik.touched.lastName && formik.errors.lastName}
          />
          <TextField
            fullWidth
            id="email"
            name="email"
            label="Email"
            value={formik.values.email}
            onChange={formik.handleChange}
            error={formik.touched.email && Boolean(formik.errors.email)}
            helperText={formik.touched.email && formik.errors.email}
          />{' '}
          <LocalizationProvider dateAdapter={AdapterDayjs}>
            <DesktopDatePicker
              disablePast
              label="Pick Your date"
              inputFormat="MM/DD/YYYY"
              value={formik.values.date}
              onChange={(val) => {
                formik.setFieldValue('date', val);
              }}
              renderInput={(params) => (
                <TextField
                  error={formik.touched.date && Boolean(formik.errors.date)}
                  helperText={formik.touched.date && formik.errors.date}
                  {...params}
                />
              )}
            />
          </LocalizationProvider>
          <Button color="primary" variant="contained" fullWidth type="submit">
            Submit
          </Button>
        </Stack>
      </form>
    </div>
  );
};

and tests:

    const onSubmit = jest.fn((e) => e.preventDefault());
    const addUser = jest.fn();
    render(
      <MockedProvider mocks={[addUserMock]} addTypename={false}>
        <AddUserForm addUser={addUser} />
      </MockedProvider>
    );
    const firstNameInput = screen.getByRole('textbox', { name: /first name/i });
    const lastNameInput = screen.getByRole('textbox', { name: /last name/i });
    const emailInput = screen.getByRole('textbox', { name: /email/i });
    const dateInput = screen.getByRole('textbox', { name: /date/i });
    const submitBtn = screen.getByRole('button', { name: /submit/i });

    userEvent.type(firstNameInput, 'Cregan');
    userEvent.type(lastNameInput, 'Stark');
    userEvent.type(emailInput, 'Cregan@Stark.north');
    userEvent.type(dateInput, '2024-05-24');
    userEvent.click(submitBtn);
  
    await waitFor(() =>
      expect(onSubmit).toHaveBeenCalledWith({
        firstName: 'Cregan',
        lastName: 'Stark',
        email: 'Cregan@Stark.north',
        date: '20/24/0524',
      })
    );
  });
  test('test email validation', async () => {
    const addUser = jest.fn();
    render(<AddUserForm addUser={addUser} />);

    const emailInput = screen.getByRole('textbox', { name: /email/i });
    const submitBtn = screen.getByRole('button', { name: /submit/i });
    const emailErrorMsg = screen.getByText(/enter a valid email/i);

    userEvent.type(emailInput, 'Winter is Coming');
    userEvent.click(submitBtn);
    await waitFor(() => {
      expect(emailErrorMsg).toBeInTheDocument();
    });
  });```

After i launch tests i have failure with comments: 1 test -


    Expected: {"date": "20/24/0524", "email": "Cregan@Stark.north", "firstName": "Cregan", "lastName": "Stark"}

    Number of calls: 0

2 test:

 TestingLibraryElementError: Unable to find an element with the text: /enter a valid email/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

it looks like React Testing library doesnt recognize click to submit button. I tried to wrapped component with MockProvider, i tried to use fireEvent or userEvent and the result was still the same.

The code are on my GH repository: https://github.com/PrzemekZygmanowski/bh-fe/blob/main/src/components/AddUserForm.jsx https://github.com/PrzemekZygmanowski/bh-fe/blob/main/src/containers/UserForm.jsx https://github.com/PrzemekZygmanowski/bh-fe/blob/main/src/components/AddUserForm.spec.js If You can help me i will be greatful :)


Solution

  • On the first test you are not wiring the mocked onSubmit into the actual AddUserForm. Submit seems to be the addUser prop but this is actually connected to a different mock.

    You probably meant:

        const onSubmit = jest.fn((e) => e.preventDefault());
        render(
          <MockedProvider mocks={[addUserMock]} addTypename={false}>
            <AddUserForm addUser={onSubmit} />
          </MockedProvider>
        );
    

    On the second you are getting the screen.getByText(/enter a valid email/i); and storing it in a local var before the wait. This reference is stale. When waitFor polls, it just keeps getting the same old stale reference. You need to bring it in scope:

       await waitFor(() => {
          const emailErrorMsg = screen.getByText(/enter a valid email/i);
          expect(emailErrorMsg).toBeInTheDocument();
        });
    

    It's not necessary anyway to use waitFor there. Just use findBy which will wait internally.

    const emailErrorMsg = await screen.findByText(/enter a valid email/i);
    expect(emailErrorMsg).toBeInTheDocument();