Search code examples
reactjstypescriptformikreact-testing-libraryts-jest

How can I test useFormik when the form is submitting


As you can see below, the Form component is using useFormik hook. The component satisfies all of my needs but I'm struggled when testing comes into play, specially when the form is submitted.

Form.tsx

import {
  TextField,
  Button,
  Box,
  Typography,
  useTheme,
  Snackbar
} from '@material-ui/core'
import { useState } from 'react'
import { useFormik } from 'formik'
import { object, string, SchemaOf, ref as yupRef } from 'yup'
import axios from 'axios'
import { useRouter } from 'next/router'
import { useFocus } from '@hooks/useFocus'
import { FormLink } from '@components/common/FormLink'

export interface FormTypes {
  username: string
  email: string
  password: string
  confirmPassword: string
}

export const validationSchema: SchemaOf<FormTypes> = object({
  username: string()
    .min(2, 'Username should be of minimum 2 characters')
    .max(25, 'Username should be of maximum 25 characters')
    .required('Name is required'),
  email: string().email('Enter a valid email').required('Email is required'),
  password: string()
    .min(8, 'Password should be of minimum 8 characters')
    .required('Password is required'),
  confirmPassword: string().oneOf(
    [yupRef('password'), null],
    'Passwords must match'
  )
})

export const Form = () => {
  const theme = useTheme()
  const ref = useFocus()
  const [open, setOpen] = useState(false)
  const [errorMessage, setErrorMessage] = useState('')
  const router = useRouter()

  const formik = useFormik<FormTypes>({
    initialValues: {
      username: '',
      email: '',
      password: '',
      confirmPassword: ''
    },
    validationSchema,
    onSubmit: async (values, { setSubmitting }) => {
      setSubmitting(true)

      const res = await axios.post('/api/register', values)
      if (res.data.success) {
        router.push('dashboard')
      } else {
        setOpen(true)
        setErrorMessage(res.data.message)
      }
      setSubmitting(false)
    }
  })

  const handleClose = () => {
    setOpen(false)
  }

  return (
    <Box
      width="55%"
      p={theme.spacing(6, 8)}
      borderRadius={16}
      bgcolor={theme.palette.grey[200]}
      boxShadow={theme.shadows[15]}
      display="grid"
    >
      <Box clone alignSelf="center" style={{ marginBottom: theme.spacing(2) }}>
        <Typography component="h3" variant="h5" color="primary">
          Sign up
        </Typography>
      </Box>
      <Box clone display="grid" gridGap={theme.spacing(1)}>
        <form onSubmit={formik.handleSubmit}>
          <TextField
            id="username"
            label="Username"
            name="username"
            inputRef={ref}
            value={formik.values.username}
            onChange={formik.handleChange}
            error={formik.touched.username && Boolean(formik.errors.username)}
            helperText={formik.touched.username && formik.errors.username}
          />
          <TextField
            id="email"
            label="Email"
            name="email"
            value={formik.values.email}
            onChange={formik.handleChange}
            error={formik.touched.email && Boolean(formik.errors.email)}
            helperText={formik.touched.email && formik.errors.email}
          />
          <TextField
            id="password"
            label="Password"
            name="password"
            type="password"
            value={formik.values.password}
            onChange={formik.handleChange}
            error={formik.touched.password && Boolean(formik.errors.password)}
            helperText={formik.touched.password && formik.errors.password}
          />
          <TextField
            id="confirmPassword"
            label="Confirm Password"
            name="confirmPassword"
            type="password"
            value={formik.values.confirmPassword}
            onChange={formik.handleChange}
            error={
              formik.touched.confirmPassword &&
              Boolean(formik.errors.confirmPassword)
            }
            helperText={
              formik.touched.confirmPassword && formik.errors.confirmPassword
            }
          />
          <Box
            clone
            justifySelf="start"
            alignSelf="center"
            style={{
              borderRadius: 24,
              padding: theme.spacing(1.5, 5),
              marginTop: theme.spacing(2)
            }}
          >
            <Button
              type="submit"
              variant="contained"
              color="primary"
              disabled={formik.isSubmitting}
            >
              {formik.isSubmitting ? 'Loading...' : 'sign up'}
            </Button>
          </Box>
        </form>
      </Box>
      <Typography style={{ marginTop: theme.spacing(2) }} variant="body1">
        Already registered? <FormLink href="/login">Login</FormLink>
      </Typography>
      <Snackbar
        open={open}
        autoHideDuration={3000}
        message={errorMessage}
        onClose={handleClose}
      />
    </Box>
  )
}


Form.test.tsx

import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Form } from './Form'

describe('Signup form', () => {
  it('should submit the signup form', async () => {
    render(<Form />)

    userEvent.type(screen.getByLabelText(/username/i), 'John')
    userEvent.type(screen.getByLabelText(/email/i), '[email protected]')
    userEvent.type(screen.getByLabelText(/^password$/i), 'Dee123456')
    userEvent.type(screen.getByLabelText(/^confirm password$/i), 'Dee123456')

    await waitFor(() =>
      /* ?????? */
      expect('').toHaveBeenCalledWith({
        username: 'John',
        email: '[email protected]',
        password: 'Dee123456',
        confirmPassword: 'Dee123456'
      })
    )
  })
})

I'm a beginner using testing and it's hard to say but I need to test when the form is submitted and check the data. I found solution using props like

const handleSubmit = jest.fn()

I cannot apply that code because I'm not using props in the Form component.


Solution

  • The main goal of React Testing Library is to allow you to write tests which resemble the way your software is used by real users. In this case a user would not see a handleSubmit function, but they do see "Loading..." text after clicking submit. So, let's test like a real user and check for that:

    describe('Signup form', () => {
      it('should submit the signup form', async () => {
        render(<Form />)
    
        userEvent.type(screen.getByLabelText(/username/i), 'John')
        userEvent.type(screen.getByLabelText(/email/i), '[email protected]')
        userEvent.type(screen.getByLabelText(/^password$/i), 'Dee123456')
        userEvent.type(screen.getByLabelText(/^confirm password$/i), 'Dee123456')
    
        // Click "sign up" button, like a real user would.
        screen.getByRole('button', { name: 'sign up' }).click();
    
        // Check to see if the button text changes, like a real user would.
        await waitFor(() =>
          expect(screen.getByRole('button', { name: 'Loading...' })).toBeInTheDocument();
        )
      })
    })
    

    You'll notice I used getByRole here. This is the number one best way to find elements as it encourages you to write more accessible code, and it's often how real users find your components anyway (button, input, checkbox etc.) Have a look at this order of priority to give you a better idea on which query to use. Also make sure to read this article by Kent C Dodds, the creator of React Testing Library.