I have the following input:
<input
name="name"
type="text"
data-testid="input"
onChange={(e) => setTypedName(e.target.value)}
value=""
/>
the test:
test.only('Should change the value of the input ', async () => {
makeSut()
const nameInput = sut.getByTestId('input') as HTMLInputElement
fireEvent.change(nameInput, { target: { value: 'value' } })
expect(nameInput.value).toBe('value')
})
My assertion fails, as the change does not take effect, while the value remains to be ""
If I remove value="" from the input, change takes effect.
I have tried using fireEvent.input fireEvent.change, userEvent.type and nothing works.
It seems that when I use a default value the testing library does not accept changes, even though it works on production...
Any hints?
Using:
jest 27.3.1
#testing-library/react 12.1.2
#testing-library/user-event 13.5.0
I'm not sure, but perhaps this is due to React relying more on explicitly setting the value of components through JS rather than through "vanilla" HTML.
Explicitly setting the input value through JS makes your test pass:
import { render, screen } from "#testing-library/react";
import React, { useState } from "react";
import userEvent from "#testing-library/user-event";
const Component = () => {
const [value, setValue] = useState("");
return (
<input
name="name"
type="text"
data-testid="input"
onChange={(e) => setValue(e.target.value)}
value={value}
/>
);
};
test.only("Should change the value of the input ", async () => {
render(<Component />);
const nameInput = screen.getByTestId("input") as HTMLInputElement;
userEvent.type(nameInput, "value");
expect(nameInput.value).toBe("value");
});
PS I slightly modified your test to use render because I'm not sure what makeSut is, and I assume it's some custom function that you created to render your component.
Related
I am trying to test a basic form with Jest and testing library. I keep getting the same error on Jest which I don't understand why: TypeError: Cannot read properties of undefined (reading 'firstName')
Here is the code for the form I am trying to test on the component:
import React, { useState, useReducer } from "react";
import PropTypes from "prop-types";
import Input from "../../components/Input";
import NextButton from "../../components/Button/Next";
import { UserReducer, DefaultUser } from "./user-reducer";
import { stepOneValidate } from "./validation";
const StepOne = ({ step, setStep, user, setUser }) => {
console.log(user);
const [errors, setErrors] = useState({});
// const [user, setUser] = useReducer(UserReducer, DefaultUser);
// handle onchange
const handleUser = ({ target }) => {
setUser({
type: "UPDATE_STEPONE_INFO",
payload: { [target.name]: target.value },
});
};
const handleContinue = (e) => {
e.preventDefault();
const errors = stepOneValidate(user);
setErrors(errors);
if (Object.keys(errors).length > 0) return;
setStep(step + 1);
};
return (
<form onSubmit={handleContinue}>
<h4 className="font-bold text-lg leading-title mb-6">Step {step + 1}</h4>
<Input
type="text"
name="firstName"
data-testid="firstName"
label="First name"
onChange={(e) => handleUser(e)}
error={errors.firstName}
value={user.firstName}
/>
<Input
type="text"
name="lastName"
data-testid="lastName"
label="Last name"
onChange={(e) => handleUser(e)}
error={errors.lastName}
value={user.lastName}
/>
<Input
type="number"
label="Age"
name="age"
data-testid="age"
onChange={(e) => handleUser(e)}
error={errors.age}
value={user.age}
/>
<NextButton data-testid="submit" type="submit">
Next
</NextButton>
</form>
);
};
StepOne.propTypes = {
step: PropTypes.number,
setStep: PropTypes.func,
user: PropTypes.object,
setUser: PropTypes.func,
};
export default StepOne;
So here basically Jest does not like the value value={user.lastName} and keeps erroring out.
Here is the Jest test file:
import { render, screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import StepOne from "../pages/register-user/step-one";
import { expect, test } from "#jest/globals";
describe("<StepOne />", () => {
test("render name and text input", () => {
render(<StepOne />);
const nameInput = screen.getByTestId("firstName");
expect(nameInput).toBeInTheDocument();
expect(nameInput).toHaveAttribute("type", "text");
});
});
I cannot figure out why Jest gives out that error. Once I remove the value={user.firstName} from the page, Jest is able to test the page.
The form needs that value in order to preserve the form data so I cannot remove it eventhough that's what I tried.
I tried setting up again Jest to see if it is a setup issue perhaps but it's not.
I tried other methods with testing library to see if it is the right utility but that did not help.
I can actually test a component button but Jest says, anything with this value={user.firstName) I do not like. I just cannot figure it out please if there is some help with this. I actually have not worked with Jest too much.
I'm not sure if something went wrong when you copied your component code here but I noticed that the state that is managing user is commented out.
Regardless, this is not a "testing" issue. When you reference properties of an object that doesn't exist yet, you will get that error hence the error wording properties of undefined. If you set your user's initial state to an empty object, it should solve the problem but the values you reference will still be undefined, i.e user is defined but user.lastName etc will be undefined.
To get around that, it is good practice to initialize state with default values (preferably from the same type that the state is supposed to store). I'm not what else your reducer supposed to do and whether you actually need it, but if I were to use useState, I would initialize the user state like so:
const [user, setUser] = useState({
// I would normally initialize state that manages numbers with a default value of number type
// but inputs manage all values as strings by default so I made that an empty string as well
age: '',
firstName: '',
lastName: '',
})
Having such simple React (with the react-hook-form library) form
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, formState: { errors } } = useForm();
const firstNameRef1 = useRef(null);
const onSubmit = data => {
console.log("...onSubmit")
};
const { ref, ...rest } = register('firstName', {
required: " is required!",
minLength: {
value: 5,
message: "min length is <5"
}
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<hr/>
<input {...rest} name="firstName" ref={(e) => {
console.log("...ref")
ref(e)
firstNameRef1.current = e // you can still assign to ref
}} />
{errors.firstName && <span>This field is required!</span>}
<button>Submit</button>
</form>
);
}
I'm getting:
...ref
...ref
...onSubmit
...ref
...ref
in the console output after the Submit button click.
My question is why is there so many ...ref re-calls? Should't it just be the one and only ref call at all?
P.S.
When I've removed the formState: { errors } from the useForm destructuring line above the problem disappears - I'm getting only one ...ref in the console output as expected.
It is happening because you are calling ref(e) in your input tag. That's why it is calling it repeatedly. Try removing if from the function and then try again.
<input {...rest} name="firstName" ref={(e) => {
console.log("...ref")
ref(e) // <-- ****Remove this line of code*****
firstNameRef1.current = e // you can still assign to ref
}} />
My question is why is there so many ...ref re-calls?
This is happening because every time you render, you are creating a new function and passing it into the ref. It may have the same text as the previous function, but it's a new function. React sees this, assumes something has changed, and so calls your new function with the element.
Most of the time, getting ref callbacks on every render makes little difference. Ref callbacks tend to be pretty light-weight, just assigning to a variable. So unless it's causing you problems, i'd just leave it. But if you do need to reduce the callbacks, you can use a memoized function:
const example = useCallback((e) => {
console.log("...ref")
ref(e);
firstNameRef1.current = e
}, [])
// ...
<input {...rest} name="firstName" ref={example} />
I am trying to build a functionality where when a user navigates away from the form i.e when component unmounts it should trigger a save i.e post form data to server. This should happen only if there is any change in form data. Can anyone guide me as to why this is happening. I have tried class based approach which works but I do not want to refactor my production code.
import { useCallback, useEffect, useState } from "react";
import React from "react";
import * as _ from "lodash";
import { useFormik } from "formik";
// for now this is hardcoded here..but let's assume
// this server data will be loaded when component mounts
const serverData = {
choice: "yes",
comment: "some existing comment"
};
const availableChoices = ["yes", "no"];
const Form = () => {
const formik = useFormik({ initialValues: { ...serverData } });
const [isFormChanged, setIsFormChanged] = useState(false);
const valuesHaveChanged = React.memo(() => {
console.log("INIT VALUES= ", formik.initialValues);
console.log("FINAL VALUES = ", formik.values);
return !_.isEqual(formik.initialValues, formik.values);
}, [formik.initialValues, formik.values]);
const triggerSave = () => console.log("Save");
useEffect(() => {
// setForm({ ...serverData });
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (setIsFormChanged) {
triggerSave();
}
};
});
return (
<form>
<div className="form-group">
{availableChoices.map((choice) => (
<label key={choice}>
{choice}
<input
id="choice"
value={choice}
className="form-control"
type="radio"
name="choice"
checked={choice === formik.values.choice}
onChange={formik.handleChange}
/>
</label>
))}
</div>
<div className="form-group">
<textarea
rows="5"
cols="30"
id="comment"
name="comment"
value={formik.values.comment}
onChange={formik.handleChange}
className="form-control"
placeholder="some text..."
></textarea>
</div>
</form>
);
};
export default Form;
The first problem i spotted is the dependency array.
useEffect(() => {
// the flag can be set anytime upon a field has changed
// maybe formik has a value like that, read doc
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
if (setIsFormChanged) {
triggerSave();
}
}
// the dependency array is [], can't be missed
}, [])
Currently you are calling this effect and cleanup this effect in every update, ex. if any value changes in this component. But normally you only want to do it once upon dismount.
Even you do the above right, you still need to make sure your code contains no memory leak, because you are trying to do something upon the dismount. So it's better to pass the values:
triggerSave([...formik.values])
And make sure inside triggerSave, you don't accidently call anything about formik or setState.
Try to use useEffect with dependencies
useEffect(() => {
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (!_.isEqual(formik.initialValues, formik.values)) {
triggerSave();
}
};
}, [formik.values]); // won't run on every render but just on formik.values update
Explanation:
useEffect has dependencies as a second argument, if [] is passed - effect is triggered only on mount, if [...] passed, will trigger on the first mount and on any of ... update.
If you don't pass the second agrument, useEffect works as a on-every-render effect.
So I'm using React Hooks with styled components, I have tried to style a form but when I make it into a styled component the form doesn't work ie you type one letter then the form looses focus, you have to click back into the input box, type one letter and it looses focus again etc...
I'm also getting this warning in the dev tools but I don't really understand what I need to do -
index.js:27 The component styled.form with the id of "sc-eCssSg" has been created dynamically.
You may see this warning because you've called styled inside another component.
To resolve this only create new StyledComponents outside of any render method and function component.
at LocationForm (https://3deis.csb.app/src/Components/LocationForm.js:31:39)
at div
at App (https://3deis.csb.app/src/App.js:53:39)
How do I change the below code to do what is needed to make it work ?
import React, { useState } from "react";
import axios from "axios";
import styled from "styled-components";
const LocationForm = (props) => {
const [locationName, setName] = useState("");
const Form = styled.form``;
const handleSubmit = (evt) => {
evt.preventDefault();
axios
.get(
`http://www.mapquestapi.com/geocoding/v1/address?key=z2G40AM2VSDfXx7MQtCqAvmXmoYEX8cV&location=${locationName}&maxResults=1`
)
.then((res) => {
const latitude = res.data.results[0].locations[0].displayLatLng.lat;
const longitude = res.data.results[0].locations[0].displayLatLng.lng;
const city = res.data.results[0].locations[0].adminArea5;
// const submitted = !true;
props.callbackFromParent(
locationName,
// submitted,
latitude,
longitude,
city
);
})
.catch((error) => {
console.log(error);
});
};
return (
<Form onSubmit={handleSubmit}>
<label>
Location:
<input
type="text"
value={locationName}
onChange={(e) => setName(e.target.value)}
/>
</label>
<input type="submit" value="Submit" />
</Form>
);
};
export default LocationForm;
It says exactly what is happening. You are creating const Form = styled.form inside your LocationForm render function. If you move it 4 lines up outsie the function it will stop giving the warning. In general you should never create a styled component inside a render function, because it will recreate the form each render (so each time you input a character) instead of only once upon initialization.
I am quite new to React and how to use hooks. I am aware that the following code doesn't work, but I wrote it to display what I would like to achieve. Basically I want to use useQuery after something changed in an input box, which is not allowed (to use a hook in a hook or event).
So how do I correctly implement this use case with react hooks? I want to load data from GraphQL when the user gives an input.
import React, { useState, useQuery } from "react";
import { myGraphQLQuery } from "../../api/query/myGraphQLQuery";
// functional component
const HooksForm = props => {
// create state property 'name' and initialize it
const [name, setName] = useState("Peanut");
const handleNameChange = e => {
const [loading, error, data] = useQuery(myGraphQLQuery)
};
return (
<div>
<form>
<label>
Name:
<input
type="text"
name="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</div>
);
};
export default HooksForm;
You have to use useLazyQuery (https://www.apollographql.com/docs/react/api/react-hooks/#uselazyquery) if you wan't to control when the request gets fired, like so:
import React, { useState } from "react";
import { useLazyQuery } from "#apollo/client";
import { myGraphQLQuery } from "../../api/query/myGraphQLQuery";
// functional component
const HooksForm = props => {
// create state property 'name' and initialize it
const [name, setName] = useState("Peanut");
const [doRequest, { called, loading, data }] = useLazyQuery(myGraphQLQuery)
const handleNameChange = e => {
setName(e.target.value);
doRequest();
};
return (
<div>
<form>
<label>
Name:
<input
type="text"
name="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</div>
);
};
export default HooksForm;
I think you could call the function inside the useEffect hook, whenever the name changes. You could debounce it so it doesn't get executed at every letter typing, but something like this should work:
handleNameChange = (e) => setName(e.target.value);
useEffect(() => {
const ... = useQuery(...);
}, [name])
So whenever the name changes, you want to fire the query? I think you want useEffect.
const handleNameChange = e => setName(e.target.value);
useEffect(() => {
// I'm assuming you'll also want to pass name as a variable here somehow
const [loading, error, data] = useQuery(myGraphQLQuery);
}, [name]);