I am using Jest and Enzyme to test a React component. I am trying to test my form validation rules when submitting a form. The tests need to cover all possible cases of this function
const handleSubmit = event => {
event.preventDefault();
const { createPassword, confirmPassword } = event.target.elements;
if (createPassword.value !== confirmPassword.value) {
setPassValidationError("*Passwords must match!");
} else if (createPassword.value.length < 8) {
setPassValidationError("*Passwords must be at least 8 characters long!");
} else if (createPassword.value.search(/[A-Z]/) < 0) {
setPassValidationError(
"*Passwords must contain at least one uppercase letter!"
);
} else if (createPassword.value.search(/[!##$%^&*]/) < 0) {
setPassValidationError(
"*Passwords must contain at least one special character!"
);
} else {
props.updatePassword({
uid: props.uid,
token: props.token,
new_password: createPassword.value
});
event.target.reset();
}
};
This function is pretty straight forward createPassword and confirmPassword are the values for 2 different input fields. When the form is submitted and this function gets called I am testing the password on different criteria. If the password is not strong enough, the setPassValidationError hook is called and updates a state variable.
I am currently trying to test the function with a password shorter than 8 characters.
it("passwords must be 8 char long", () => {
const wrapper = mount(<NoAuthPasswordChange />);
const passInput = wrapper.find("#create-password");
const confirmPass = wrapper.find("#confirm-password");
passInput.simulate("change", { target: { value: "QQQQQQ" } });
confirmPass.simulate("change", { target: { value: "QQQQQQ" } });
const submitButton = wrapper.find("#submit-button");
submitButton.simulate("click");
expect(wrapper.find("#password-validation-error").text()).toContain(
"*Passwords must be at least 8 characters long!"
);
});
Jest is telling me that #password-validation-error cannot be found (expected 1 node found 0). Now this particular part of the code is only rendered if passValidationError has data.
{passValidationError ? (
<h2
className={styles.passwordError}
id="password-validation-error"
>
{passValidationError}
</h2>
) : null}
I'm not sure if I just have a simple bug in my test or if something more advanced needs to be done in order to use Jest and have a function call a hook update.
Edit: I am beginning to wonder if the event parameter required by the handleSubmit function is problematic due to the function being called by Jest.
This can be cause by not updating the component itself. Have you tried to force your wrapper to be re-rendered:
https://airbnb.io/enzyme/docs/api/ShallowWrapper/update.html
https://airbnb.io/enzyme/docs/api/ReactWrapper/update.html
I have found a solution to my issue. The test needs to call the form submission on the form element itself and not via a button click. So instead of submitButton.simulate("click") I need to simulate a submit on my form element. I am unsure why this solution works and the posted code does not.
Related
In my React app, I've built a function that accepts a string full of regular text and any number of URLs. It then converts these into a <span> in React with every URL inside of an <a href tag. The code works really well but I can't seem to write a Jest test for it.
Here's what I've tried so far:
expect(convertHyperlinks('http://stackoverflow.com'))
.toStrictEqual(<span><a href='http://stackoverflow.com' target='_blank'>stackoverflow.com</a></span>);
And:
expect(convertHyperlinks('http://stackoverflow.com'))
.toMatchInlineSnapshot(<span><a href='http://stackoverflow.com' target='_blank'>stackoverflow.com</a></span>);
In the former case I'm getting the "serializes to the same string" message.
In the latter case, it's showing me this:
Expected properties: <span>stackoverflow.com</span>
Received value: <span>stackoverflow.com</span>
Might anyone know how to build a passing test for this?
Robert
Update: Here's the code for the function in question:
export const convertHyperlinks = (text: string): React.Node => {
// Find all http instances
const regex = /http\S*/g;
const hyperlinkInstances = text.match(regex);
if (!hyperlinkInstances) {
return <span>{text}</span>;
}
// Break up `text` into its logical chunks of strings and hyperlinks
let items = [];
let idx1 = 0;
let idx2 = -1;
hyperlinkInstances.forEach((hyperlink) => {
idx2 = text.indexOf(hyperlink, idx1);
if (idx2 === idx1) {
items.push(hyperlink);
idx1 += hyperlink.length;
} else {
items.push(text.substring(idx1, idx2));
items.push(hyperlink);
idx1 = idx2 + hyperlink.length;
}
});
if (idx1 < text.length) {
items.push(text.substring(idx1, text.length));
}
return (
<span>
{items.map((item) => {
if (item.includes('http://')) {
const plainLink = item.replace('http://', '');
return (
<a href={item.toLowerCase()} target='_blank' key={plainLink}>
{plainLink}
</a>
);
} else {
return item;
}
})}
</span>
);
};
You are returning a ReactNode from the method, which is an object. But you are trying to assert as just a string. It would'nt work.
This is what you may be getting back from the method,
And so, you must assert against the object you got, and not the way you are doing it right now,
const result = convertHyperlinks('http://stackoverflow.com')
expect(result.props[0].key).equals('stackoverflow.com');
// similar kind of assertions.
Additionally, I would suggest you go the component route and just render the component in the test method and assert for presence of elements as opposed to diving into react objects.
A representation of the same is as follows,
Here is your component,
const ConvertToHyperlinks = ({text}: {text: string}) => {
// your logic and then returning DOM elements.
return <></>;
}
Then you use it anywhere as,
<div>
<ConvertToHyperlinks text={'https://www.test.com/'} />
</div>
In your unit test you can then,
const renderedComponent = render(<ConvertToHyperlinks text={''https://www.anytyhing.com}/>);
expect(renderdComponent.getByText('anytyhing.com')).ToBeInTheDocument();
Here I am using some Rect Testing Library method but the idea is same even if you use enzyme etc.
So I have two modals that I am using one of them was already implemented and behaves as expected however when I've added the other modal depending on the condition of if there is any true value when mapping over the array the way it works right now both modals show when there is a true value. I think this is because there are multiple false values returned from my .includes() function before the true appears. I think a good solution for this would be to make an array of all the values returned when I run .includes() on the entries then I can check that array for any true values but I cant seem to get the values into an array. When I try and push them into an array they just all push into their own separate arrays. This may be the wrong approach if it is can you explain what a better approach would be:
const checkPending = () => {
if(entries){
entries.map(descriptions => {
const desc = descriptions.description
//check if there are any pending tests
const check = desc.includes("pending")
//if the check returns true show the pending modal if it doesnt set the other modal to true
if(check === true){
setShowModal(false)
setShowPendingM(true)
}else{
setShowModal(true)
}
})
}
}
return(
<Button
onClick={() => checkPending()}
className={`${styles.headerButton} mr-2`}
>
Add File
<Plus />
</Button>
)
setShowModal & setShowPendingM are both passed from a parent component as props. They are both initialized as false. The most straightforward question I can pose is is there any way to say if there are any true values returned from .includes then do something even if there are false values present
I think this is how your checkingPending method should look like.
const checkPending = () => {
if(entries){
let pending = false;
entries.forEach((descriptions) => {
const desc = descriptions.description
if(desc.includes('pending')){
pending = true;
}
});
if(pending) {
setShowModal(false);
setShowPendingM(true);
} else {
setShowModal(true);
setShowPendingM(false);
}
}
}
Let me know if you have any additional questions.
Is it possible to do something like:
const data={
star: "<h1>STAR</h1>",
moon: "<h3>moon</h3>"
}
const App = () => {
return(
<div>{data.start}</div>
);
}
what i get is the actual string of <h1>STAR</h1> not just STAR
I don't think you can. You can return an html string and possibly get it to display, but JSX isn't a string, it gets compiled into javscript code that creates those elements. it works when your app is built, I don't think you can use dynamic strings with it at run-time. You could do something like this:
const getData = (which) => {
if (which === 'star') {
return (<h1>STAR</h1>);
}
if (which === 'moon') {
return (<h3>moon</h3>);
}
return null; // nothing will display
}
const App = () => {
return (
<div>{getData('star')}</div>
);
};
Strings can be converted to JSX with third-party libraries such as h2x or react-render-html. It may be unsafe to do this with user input because of possible vulnerabilities and security problems that may exist libraries that parse DOM.
It's impossible to use components this way because component names aren't associated with functions that implement them during conversion.
I have a form that contains several questions. Some of the questions contains a group of subquestions.
The logic to render sub questions is written inside componentDidUpdate method.
componentDidUpdate = (prevProps, prevState, snapshot) => {
if (prevProps !== this.props) {
let questions = this.props.moduleDetails.questions,
sgq = {};
Object.keys(questions).map((qId) => {
sgq[qId] = (this.state.groupedQuestions[qId]) ? this.state.groupedQuestions[qId] : [];
let answerId = this.props.formValues[qId],
questionObj = questions[qId],
groupedQuestions = [];
if(questionObj.has_grouped_questions == 1 && answerId != null && (this.state.groupedQuestions != null)) {
groupedQuestions = questions[qId].question_group[answerId];
let loopCount = this.getLoopCount(groupedQuestions);
for(let i=0; i<loopCount; i++) {
sgq[qId].push(groupedQuestions);
}
}
});
this.setState({groupedQuestions: sgq});
}
}
The problem is that on every key stroke of text field, handleChange method is invoked which will ultimately invoke componentDidUpdate method. So the same question groups gets rendered on every key stroke.
I need a way to detect if the method componentDidUpdate was invoked due to the key press(handleChange) event so that i can write logic as follows.
if(!handleChangeEvent) {
Logic to render question group
}
Any idea on how to integrate this will be appreciated.
I assume your textfield is a controlled component, meaning that its value exists in the state. If this is the case, you could compare the previous value of your textfield to the new one. If the value is different, you know the user entered something. If they are equal however, the user did something else at which point you want your snippet to actually execute.
Basically:
componentDidUpdate = (prevProps) => {
// if value of textfield didn't change:
if (prevProps.textfieldValue === this.props.textfieldValue) {
// your code here
}
}
Another approach is to use componentDidReceiveProps(). There you can compare the props to the previous ones, similarly to the above, and execute your code accordingly. Which method is most suitable depends on how your app works.
So I have a little bit of form validation going on and I am running into an issue. When I first load the web app up and try adding a value and submitting with my button it doesn't allow me and gives me the error I want to see. However, when I add a value setState occurs and then my value is pushed to UI and I try to add another blank value it works and my conditional logic of checking for an empty string before doesn't not go through what am I doing wrong?
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
}, () => {
document.getElementById('test').value = '';
})
console.log(this.state.id);
}
}
You are checking this.state.input but no where in that code are you setting the input value on the state.
Try adding this where it makes sense in your application:
this.setState({ input: 'some value' });
Also, I recommend you use the state to define the application UI. So instead of using document.getElementById('error') or document.getElementById('test').value, have the UI reflect what you have in your state.
See here for more info: https://reactjs.org/docs/forms.html
Instead of manipulating the DOM directly:
document.getElementById('test').value = '';
you'll want to use React:
this.setState({ input: '' });
A good ground rule for React is to not manipulate the DOM directly through calls like element.value = value or element.style.color = 'red'. This is what React (& setState) is for. Read more about this on reactjs.org.
Before you look for the solution of your issue, I noticed that you are directly updating the DOM
Examples
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
document.getElementById('test').value = '';
Unless you have special use case or dealing with external plugins this isn't recommended, when dealing with React you should update using the virtual DOM. https://www.codecademy.com/articles/react-virtual-dom
Pseudo code sample
constructor(props) {
this.state = {
// retain previous states in here removed for example simplicity
errorString: ''
}
}
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
this.setState({
errorString: 'Please enter something first'
});
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
input: '',
});
}
}
// notice the "error" and "test" id this could be omitted I just added this for your reference since you mentioned those in your example.
render() {
return (
<div>
{(this.state.errorString !== '') ? <div id="error" style={{color: 'red'}}>{this.state.errorString}</div> : null}
<input id="test" value={this.state.input} />
</div>
}
Every time you invoke setState React will call render with the updated state this is the summary of what is happening but there are lot of things going behind setState including the involvement of Virtual DOM.