reactjstypescriptunit-testingmaterial-uitesting-libraryreact-native

Why are my unit tests failing to assert the TextField values in my ReactPWA?


I have a React PWA and I'm using Material UI form.
"@mui/icons-material": "^6.1.4", "@mui/lab": "^6.0.0-beta.12", "@mui/material": "^6.1.4", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0",

I have a simple test I've tried to alter different ways to get it to pass. The following test fails with the following error:

Expected the element to have form values

- Expected
+ Received

  Object {
-   "drinkingHabits": "Never",
-   "education": "Other",
+   "drinkingHabits": "",
+   "education": "",
    "email": "test@example.com",
-   "gender": "Female",
+   "gender": "",
    "password": "1234567890",
-   "smokingHabits": "Occasionally",
+   "smokingHabits": "",
    "userBio": "this is my bio",
    "username": "username",
  }

I've tried several different ways to make this work and here is my last attempt:

The unit test:

  test("Button was called with form values", async () => {
    render(
      <AlertProvider>
        <MemoryRouter>
          <RegisterForm />
        </MemoryRouter>
      </AlertProvider>
    );

    //Arrange
    const emailElement = screen.getByPlaceholderText("*Email");
    const passwordElement = screen.getByPlaceholderText("*Password");
    const submit = screen.getByTestId("register-button");
    const bio = screen.getByLabelText("User Bio");
    const dob = screen.getByLabelText("*Date of birth");
    const gender = screen.getByLabelText("*Gender");
    const education = screen.getByLabelText("*Education Level");
    const drinking = screen.getByLabelText("*Drinking Frequency");
    const smoking = screen.getByLabelText("*Smoking Frequency");
    const username = screen.getByPlaceholderText("*Username");

    //Act
    userEvent.type(emailElement, "test@example.com");
    userEvent.type(passwordElement, "1234567890");
    userEvent.type(bio, "this is my bio");
    userEvent.type(username, "username");
    userEvent.type(
      dob,
      new Date(new Date().setFullYear(new Date().getFullYear() - 18))
        .toISOString()
        .split("T")[0]
    );

    userEvent.type(gender, "Female");
    userEvent.type(education, "Other");
    userEvent.type(drinking, "Never");
    userEvent.type(smoking, "Occasionally");
    fireEvent.click(submit);

    // Assert
    await waitFor(() => {
      expect(screen.getByTestId("reg-form")).toHaveFormValues({
        email: "test@example.com",
        password: "1234567890",
        username: "username",
        userBio: "this is my bio",
        gender: "Female",
        education: "Other",
        drinkingHabits: "Never",
        smokingHabits: "Occasionally",
      });
    });
  });

Here's the form component

import React, { useContext, useState } from "react";
import { DevTool } from "@hookform/devtools";
import { useForm } from "react-hook-form";
import {
  CONFIRM_PASSWORD_PLACEHOLDER,
  EDUCATION_OPTIONS,
  EMAIL_INVALID,
  EMAIL_PLACEHOLDER,
  EMAIL_REQUIRED,
  FREQUENCY,
  GENDER_OPTIONS,
  PASSWORD_PLACEHOLDER,
  PASSWORD_REQUIRED,
  PASSWORD_VALIDATION,
  USERNAME_INVALID,
  USERNAME_PLACEHOLDER,
  USERNAME_REQUIRED,
} from "../shared-strings/constants";
import { Box, Button, Input, MenuItem, TextField } from "@mui/material";
import { Link } from "react-router-dom";
import { isUserOver18 } from "../utilities/DateChecker";
import { AlertContext } from "../../Context/AlertContext";

export const RegisterForm = () => {
  type FormValues = {
    email: string;
    password: string;
    username: string;
    userBio: string;
    dob: Date;
    gender: string;
    education: string;
    drinkingHabits: string;
    smokingHabits: string;
    confirmPassword: string;
  };
  const { showAlert } = useContext(AlertContext);
  const [smoking, setSmoking] = useState("");
  const [drinking, setDrinking] = useState("");
  const [edu, setEdu] = useState("");
  const [gender, setGender] = useState("");

  const onSubmit = (data: FormValues) => {
    if (data.password !== data.confirmPassword) {
      showAlert("Error", "Passwords do not match.");
      return;
    } else if (!isUserOver18(data.dob)) {
      console.log("Submitted", data.dob);
      showAlert("Error", "Users must be 18 or older.");
    } else if (formState.isValid) {
      console.log("Submitted", data);
    }
  };

  const form = useForm<FormValues>();
  const { register, handleSubmit, formState, setValue } = form;
  const { errors } = formState;

  return (
    <div className="authFormContainer">
      <form
        className="registerForm"
        onSubmit={handleSubmit(onSubmit)}
        noValidate
        data-testid="reg-form">
        <Box>
          <Input
            type="text"
            id="username"
            sx={{ width: "100%", maxWidth: 300, mx: "auto" }}
            placeholder={USERNAME_PLACEHOLDER}
            {...register("username", {
              required: {
                value: true,
                message: USERNAME_REQUIRED,
              },
              minLength: {
                value: 4,
                message: USERNAME_INVALID,
              },
            })}
          />
          <p className="error" data-testid="emailErr">
            {errors.username?.message}
          </p>
        </Box>
        <Box>
          <Input
            type="email"
            id="email"
            sx={{ width: "100%", maxWidth: 300, mx: "auto" }}
            placeholder={EMAIL_PLACEHOLDER}
            {...register("email", {
              pattern: {
                value:
                  /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
                message: EMAIL_INVALID,
              },
              required: {
                value: true,
                message: EMAIL_REQUIRED,
              },
            })}
          />
          <p className="error" data-testid="emailErr">
            {errors.email?.message}
          </p>
        </Box>
        <Box>
          <Input
            type="password"
            id="password"
            data-testid="password"
            sx={{ width: "100%", maxWidth: 300, mx: "auto" }}
            placeholder={PASSWORD_PLACEHOLDER}
            {...register("password", {
              required: {
                value: true,
                message: PASSWORD_REQUIRED,
              },
              minLength: {
                value: 10,
                message: PASSWORD_VALIDATION,
              },
            })}
          />
          <p className="error">{errors.password?.message}</p>
        </Box>
        <Box>
          <Input
            id="confirmPassword"
            data-testid="confirm"
            sx={{ width: "100%", maxWidth: 300, mx: "auto" }}
            placeholder={CONFIRM_PASSWORD_PLACEHOLDER}
            {...register("confirmPassword", {
              required: {
                value: true,
                message: PASSWORD_REQUIRED,
              },
              minLength: {
                value: 10,
                message: PASSWORD_VALIDATION,
              },
            })}
          />
          <p className="error">{errors.password?.message}</p>
        </Box>
        <Box mt={2}>
          <TextField
            sx={{ paddingTop: 5 }}
            InputLabelProps={{ style: { color: "white" } }}
            id="userBio"
            label="User Bio"
            multiline
            rows={4}
            placeholder="Tell us about yourself"
            {...register("userBio", {
              maxLength: {
                value: 200,
                message: "Bio should not exceed 200 characters",
              },
            })}
          />
          <p className="error">{errors.userBio?.message}</p>
        </Box>
        <Box>
          <TextField
            InputLabelProps={{ shrink: true }}
            label="*Date of birth"
            type="date"
            id="dob"
            sx={{
              width: "100%",
              maxWidth: 300,
              mx: "auto",
              backgroundColor: "white",
            }}
            {...register("dob", {
              required: {
                value: true,
                message: "Date of birth is required",
              },
            })}
          />
          <p className="error">{errors.dob?.message}</p>
        </Box>
        <Box>
          <TextField
            select
            label="*Gender"
            id="gender"
            value={gender}
            sx={{
              width: "100%",
              maxWidth: 300,
              mx: "auto",
              backgroundColor: "white",
            }}
            {...register("gender", {
              onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
                setGender(e.target.value);
              },
              required: {
                value: true,
                message: "Gender is required",
              },
            })}>
            {GENDER_OPTIONS.map((option, index) => (
              <MenuItem key={index} value={option}>
                {option}
              </MenuItem>
            ))}
          </TextField>
          <p className="error">{errors.gender?.message}</p>
        </Box>
        <Box>
          <TextField
            select
            label="*Education Level"
            id="education"
            value={edu}
            sx={{
              width: "100%",
              maxWidth: 300,
              mx: "auto",
              backgroundColor: "white",
            }}
            {...register("education", {
              onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
                console.log("Education Level selected:", e.target.value);
                setEdu(e.target.value);
              },
              required: {
                value: true,
                message: "Education level is required",
              },
            })}>
            {EDUCATION_OPTIONS.map((option, index) => (
              <MenuItem key={index} value={option}>
                {option}
              </MenuItem>
            ))}
          </TextField>
          <p className="error">{errors.education?.message}</p>
        </Box>
        <Box>
          <TextField
            select
            label="*Drinking Frequency"
            id="drinkingHabits"
            value={drinking}
            sx={{
              width: "100%",
              maxWidth: 300,
              mx: "auto",
              backgroundColor: "white",
            }}
            {...register("drinkingHabits", {
              onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
                setDrinking(e.target.value);
              },
              required: {
                value: true,
                message: "Required",
              },
            })}>
            {FREQUENCY.map((option, index) => (
              <MenuItem key={index} value={option}>
                {option}
              </MenuItem>
            ))}
          </TextField>
          <p className="error">{errors.drinkingHabits?.message}</p>
        </Box>
        <Box>
          <TextField
            select
            label="*Smoking Frequency"
            id="smokingHabits"
            value={smoking}
            sx={{
              width: "100%",
              maxWidth: 300,
              mx: "auto",
              backgroundColor: "white",
            }}
            {...register("smokingHabits", {
              required: {
                value: true,
                message: "Required",
              },
              onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
                setSmoking(e.target.value);
              },
            })}>
            {FREQUENCY.map((option, index) => (
              <MenuItem key={index} value={option}>
                {option}
              </MenuItem>
            ))}
          </TextField>
          <p className="error">{errors.smokingHabits?.message}</p>
        </Box>
        <div>
          <p style={{ color: "white" }}>
            Already have an account? <Link to="/">Sign in</Link>
          </p>
        </div>
        <Button type="submit" data-testid="register-button" variant="contained">
          Register
        </Button>
      </form>
    </div>
  );
};


Solution

  • When you provide this prop to TextField like so:

    data-testid="email"
    

    Mui will add it to one of its containing elements (that it uses for styling purposes) that represents the TextField, which won't have a value property on its HTML element reference as it will likely be some <div>.

    Provide this instead:

    inputProps={{ 'data-testid': 'email' }}
    

    And repeat for the password field. This will put the data-testid attribute on the actual underlying <input> HTML element, which will have the value.

    The code where you log event.target.elements.email.value is not equivalent to the test code, as this gets the values via the form HTML element elements API, which already resolves any containing form field element like <input> automatically (indexing them via it's name attribute), no matter where it is in the child hierarchy.

    You could alternatively use toHaveFormValues from testing-library/jest-dom and move forward with that concept in the actual test. In this solution the data-testid and associated assertion would be on the <form> and none would be needed on the fields themselves.

    Also, I'm not sure if this is related, but you probably also want to check this before submitting since submit redirects to another page, and those HTML references for the fields you hold in the test may no longer exist in the page (stale). But this is guesswork as I'm not fully sure on intentions.