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>
);
};
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.