Using jest to send text to an input field and update state - javascript

Working on a technical challenge again. I want to set up unit tests (don't have to, but I would like to).
Problem is when I try to use userEvent or fireEvent to send some text to an input field, nothing happens (my expect toEqual test fails. I noticed that when I commented out enough of my code to figure out what was going on, I got the error: TypeError: setUsername is not a function when I use userEvent to type some text into the input field.
I assumed this meant I would need to create a mock state (setUsername updates the username state). However, this didn't resolve the issue.
The component I am testing is a child of a parent component Index . So I also tried wrapping this child component under the parent component (the username state is passed down to the child from Index). This didn't resolve the issue.
Here's my code:
const one = <index>
<One/>
</index>
describe("Step One", () => {
it("Can input text", async () => {
render(one);
const mockSetState = jest.fn();
const component ={
setState: mockSetState,
state: {
username: "test"
}
};
const username = screen.getByTestId("username");
const email = screen.getByTestId("email");
await userEvent.type(username, "Emmanuel");
userEvent.type(email, "emmanuelsibanda21#gmail.com")
expect(mockSetState).toEqual({"username": "emmanuelsibanda21#gmail.com"});
// expect(email.value).toEqual("emmanuelsibanda21#gmail.com")
});
});
This is the code in Index:
const [username, setUsername] = useState('')
const [email, setEmail] = useState(null);
return (
...
{
steps === 1 ?
<One
username={username}
setUsername={setUsername}
steps={steps}
setSteps={setSteps}
setSelectedCountry={setSelectedCountry}
selectedCountry={selectedCountry}
setSelectedState={setSelectedState}
selectedState={selectedState}
setSelectedCity={setSelectedCity}
selectedCity={selectedCity}
email={email}
setEmail={setEmail}
profilePic={profilePic}
setProfilePic={setProfilePic}
/> ...
}
Here's the code from One:
export default function One ({username, setUsername, steps, setSteps, setEmail, email, profilePic, setProfilePic}) {
const url = 'http://localhost:3000/api/getPic';
function changeHandler(e) {
setUsername(e.target.value)
}
function emailChange(e){
setEmail(e.target.value)
}
...
return (
<>
...
<div className={styles.description}>
<Input
data-testid="username"
onChange={changeHandler}
placeholder="What is your full name"
value={username}
/>
<Input
data-testid="email"
onChange={emailChange}
placeholder="Please enter a valid email"
value={email}
/>
</div>
{
username && isEmailValid(email) === true?
<Button data-testid="next" onClick={ () => nextStep() }>Next</Button>
: null
}
</main>
</>
)
};
Any ideas

Related

Putting redux state into local state with useEffect() doesn't change the local state completeley

I know that setState is asynchronous, but I have a weird problem with it.
My shortened code:
// My imports including my own component for an input switch (Switchbox)
function EditDoc(props) {
const [name, setName] = useState("");
const [privateDoc, setPrivateDoc] = useState(true);
const {
editDoc,
createDoc,
data: { docs },
} = props;
useEffect(() => {
let id = props.match.params.docId;
if (id in docs) {
setPrivateDoc(docs[id].privateDoc);
setName(docs[id].name);
}
}, []);
function handleSubmit(event) {
if (id in docs) {
editDoc(name, privateDoc);
} else {
createDoc(name, privateDoc);
}
}
return (
<React.Fragment>
<form onSubmit={handleSubmit}>
<label>Name</label>
<input
autoFocus="autoFocus"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Switchbox
title="Public document"
checked={!privateDoc}
function={() => setPrivateDoc(!privateDoc)}
/>
<button
type="reset"
onClick={props.history.goBack}
/>
<button
type="submit"
/>
</div>
</form>
</React.Fragment>
);
}
CreateLearningUnit.propTypes = {
editDoc: PropTypes.func.isRequired,
createDoc: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
data: state.data,
});
const mapActionsToProps = {
editDoc,
createDoc,
};
export default connect(mapStateToProps, mapActionsToProps)(EditDoc);
The Switchbox is always showing true after the first rendering, which is set in const [private, setPrivate] = useState(true);. My Switchbox element is not the problem, it works fine in other cases, and in this case it also works fine after toggling the switch once, but it seems like useState() is overwriting the things set in useEffect().
Now the weird thing: With name for example, everything works great, name gets set and shows up as the value in the input. SetPrivate() in useEffect is doing something too, but private is only displayed correctly after changing it one time. And yes, I know, that I use the negative of private, it shows true in both cases: docs[id].private true and false.
Could you please help me with this problem? 😃
I already tried using local variables and using useRef() with a constant but that doesn't work either.
Edit: I changed private to privateDoc, in my original code it is a longer name 😁

React prevent child update

I have a simple for with some fields in it, the fields being child components to that form. Each field validates its own value, and if it changes it should report back to the parent, which causes the field to re-render and lose focus. I want a behavior in which the child components do not update. Here's my code:
Parent (form):
function Form() {
const [validFields, setValidFields] = useState({});
const validateField = (field, isValid) => {
setValidFields(prevValidFields => ({ ...prevValidFields, [field]: isValid }))
}
const handleSubmit = (event) => {
event.preventDefault();
//will do something if all fields are valid
return false;
}
return (
<div>
<Title />
<StyledForm onSubmit={handleSubmit}>
<InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />
<Button type="submit" content="Enviar" maxWidth="none" />
</StyledForm>
</div>
);
}
export default Form;
Child (field):
function InputField(props) {
const [isValid, setValid] = useState(true);
const [content, setContent] = useState("");
const StyledInput = isValid ? Input : ErrorInput;
const validate = (event) => {
setContent(event.target.value);
setValid(stringValidator.validateField(event.target.value, props.isValidCondition))
props.reportState(props.name, isValid);
}
return (
<Field>
<Label htmlFor={props.name}>{props.name + ":"}</Label>
<StyledInput
key={"form-input-field"}
value={content}
name={props.name}
onChange={validate}>
</StyledInput>
</Field>
);
}
export default InputField;
By setting a key for my child element I was able to prevent it to lose focus when content changed. I guess I want to implement the shouldComponentUpdate as stated in React documentation, and I tried to implement it by doing the following:
Attempt 1: surround child with React.memo
const InputField = React.memo((props) {
//didn't change component content
})
export { InputField };
Attempt 2: intanciate child with useMemo on parent
const fooField = useMemo(<InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />, [fooRegex]);
return (
<div>
<Title />
<StyledForm onSubmit={handleSubmit}>
{fooField}
<Button type="submit" content="Enviar" maxWidth="none" />
</StyledForm>
</div>
);
Both didn't work. How can I make it so that when the child component isValid state changes, it doesn't re-render?
The problem is not that the component is re-rendering, it is that the component is unmounting given by this line:
const StyledInput = isValid ? Input : ErrorInput;
When react unmounts a component, react-dom will destroy the subtree for that component which is why the input is losing focus.
The correct fix is to always render the same component. What that means to you is based on how your code is structured, but I would hazard a guess that the code would end up looking a bit more like this:
function InputField(props) {
const [isValid, setValid] = useState(true);
const [content, setContent] = useState("");
const validate = (event) => {
setContent(event.target.value);
setValid(stringValidator.validateField(event.target.value, props.isValidCondition))
props.reportState(props.name, isValid);
}
return (
<Field>
<Label htmlFor={props.name}>{props.name + ":"}</Label>
<Input
valid={isValid} <-- always render an Input, and do the work of displaying error messages/styling based on the `valid` prop passed to it
value={content}
name={props.name}
onChange={validate}>
</Input>
</Field>
);
}
The canonical solution to avoiding rerendering with function components is React.useMemo:
const InputField = React.memo(function (props) {
// as above
})
However, because validateField is one of the props passed to the child component, you need to make sure it doesn't change between parent renders. Use useCallback to do that:
const validateField = useCallback((field, isValid) => {
setValidFields(prevValidFields => ({ ...prevValidFields, [field]: isValid }))
}, []);
Your useMemo solution should also work, but you need to wrap the computation in a function (see the documentation):
const fooField = useMemo(() => <InputField name="fooField" reportState={validateField} isValidCondition={fooRegex} />, [fooRegex]);

Assigned input value is undefined since onChange is never triggered

I need a solution for following problem ...
When a user registers succesfully, he gets redirected to the login page with email input field filled out and focus on password field. (email is send over paramaters)
Now since onChange event was not triggered (because email was not typed) it's value returns undefined and therefore login fails.
Is there a way to get around this?
You can found my code below
const email = queryString.parse(location.search).email; // returns email correctly
The following is a component so each attribute is set as name={props.name} value={props.value} etc
const handleChange = (name, value) => {
setData((prev) => ({ ...prev, [name]: value }));
};
<InputField name="email" type="email" onChange={handleChange} value={email ? email : ""}
So when everything is typed manually, data gets updated as expected and everything works fine, but when redirected from register to login with the field filled out through email variable, then email in data will be undefined.
Is there a way to trigger onChange when email is placed as a value inside the input field?
Thanks in advance!
The problem is handleChange is not getting the parameters of name and value
You can try something like: (Assuming name is a variable defined)
<InputField name="email" type="email" onChange={(e, {value}) => handleChange(name, value)} value={email ? email : ""}
I make a simple code, but I don't konw what actually you need, you can check on :
Or
import React, { useState } from "react";
const Sample = (props) => {
// const query = window.location.search;
const query = window.location;
const [value, setValue] = useState(query);
const handleValueOnChange = (e) => {
setValue(e.target.value);
};
console.log(query);
return (
<div>
<input onChange={handleValueOnChange} value={value} />
</div>
);
};
export default Sample;
Hope to help you .

using ...prev in setState prohibits userEvent.type to type into input when react testing

I want to test my Log In Component, which consists of two input fields, whose values are determined by a single react state that is an object with two parameters. However, when I try the test only the first letter appears in the value of the selected input and not the rest of the word. I determined my use of ...prev when updating the state to be the issue. If I only use a single input field with one state it works fine!
Here is my component:
import {useState} from 'react';
export function Login () {
//Login Credentials
const [loginCredentials, setLoginCredentials] = useState({ name: '' });
const handleChange = ({target}) => {
setLoginCredentials({[target.name]: target.value});
}
return (
<div className="login-container">
<h1>Log In</h1>
<div className="login-label-input">
<label htmlFor="name">Account Name
<input
type="name"
id="name"
name="name"
onChange={handleChange}
value={loginCredentials.name}
/>
</label>
<label htmlFor="name">Password
<input
type="password"
id="password"
name="password"
onChange={handleChange}
value={loginCredentials.password}
/>
</label>
</div>
State name: {loginCredentials.name}. State password: {loginCredentials.password}.
</div>
)
}
This works but if I include the password state:
export function Login () {
//Login Credentials
const [loginCredentials, setLoginCredentials] = useState({ name: '', password: '' });
const handleChange = ({target}) => {
setLoginCredentials((prev) => ({...prev, [target.name]: target.value}));
}
...
it does not pass the test. I does not throw an error but simply only adds the first letter of the string I am testing with:
test("log in with empty name input returns error message", async () => {
render(
<Login />
);
const nameField = screen.getByLabelText(/account name/i);
userEvent.type(nameField, 'test');
await waitFor(() => expect(nameField).toHaveValue('test'));
});
with the error:
expect(element).toHaveValue(test)
Expected the element to have value:
test
Received:
t
Is using ...prev bad or is this is a bug or what is going on?
It seems like you have to assign the new value to a different variable, I am not sure why this is necessary for the test but not in the app itself.
const handleChange = ({target}) => {
const newValue = target.value
setLoginCredentials((prev) => ({ ...prev, [target.name]: newValue }));
}

React Hooks - setState takes two clicks before working

I have an array of tags that take an input and update when the user presses enter. For some reason, the user needs to press enter twice before anything happens.
const [ tags, setTags ] = useState([]);
const addTag = (inputEvent) => {
if (inputEvent.key === 'Enter') {
setTags([ ...tags, inputEvent.target.value ]);
inputEvent.target.value = '';
}
};
return(
<input
type="text"
placeholder="Press enter"
onKeyUp={(inputEvent) => addTag(inputEvent)}
/>
)
Need more context to be sure. But i would guess this is a classic stale state problem. Instead of setTags([ ...tags, inputEvent.target.value ]), try use the callback function signature:
setTags(tags => [ ...tags, inputEvent.target.value ])
Use the ref to access the current tag from your input field.
Use form so that it is easier to add the tags and listen to Enter submit.
On submit, update your tags, and reset your form using the ref.
import React from "react";
export default function App() {
const [tags, setTags] = React.useState([]);
const inputRef = React.useRef(null);
const formRef = React.useRef(null);
const addTag = (e) => {
e.preventDefault();
setTags([...tags, inputRef.current.value]);
formRef.current.reset();
};
return (
<div>
<p>{tags.join(",")}</p>
<form onSubmit={addTag} ref={formRef}>
<input type="text" ref={inputRef} placeholder="Press enter" />
</form>
</div>
);
}
Note :- It is advisable in React to use ref to access DOM elements and manipulating them.
You could check a working example here -> https://codesandbox.io/s/wispy-microservice-3j455?file=/src/App.js:0-469

Categories

Resources