I built a form with formik + material-ui in React app.
and want to test input onChange event with Jest, enzyme and sinon.
I used setTimeout() since Formik's handlers are asynchronous and enzyme's change event is synchronous.
problem
testing 'if the value is displayed on input change' fails.
const input = wrapper.find('#username');
input.simulate('change', { target: { name: 'username', value: 'y' }
setTimeout(() => {
expect(mockChange.calledOnce).toBe(true); // test is passed.
expect(input.props().value).toEqual('y'); // Expected value to equal: "y" , Received: ""
done();
}, 1000);
loginContainer
...
render() {
const values = { username: '', password: '' };
return (
<React.Fragment>
<Formik
initialValues={values}
render={props => <Form {...props} />}
onSubmit={this.handleUserLogin}
validationSchema={loginValidation}
/>
</React.Fragment>
);
}
...
loginForm
import React from 'react';
import TextField from '#material-ui/core/TextField';
...
const styles = theme => ({ });
const Form = props => {
const {
values: { username, password },
errors,
touched,
handleChange,
handleSubmit,
isSubmitting,
handleBlur,
classes,
} = props;
return (
<div className="login-container" data-test="loginformComponent">
<form onSubmit={handleSubmit} className="flex flex-column-m items-center">
<TextField
id="username"
value={username || ''}
onChange={handleChange}
...
/>
<TextField
id="password"
value={password || ''}
onChange={handleChange}
...
/>
...
</form>
</div>
);
};
export const Unwrapped = Form;
export default withStyles(styles)(Form);
loginForm.test
import React, { shallow, sinon } from '../../__tests__/setupTests';
import { Unwrapped as UnwrappedLoginForm } from './loginForm';
const mockBlur = jest.fn();
const mockChange = sinon.spy();
const mockSubmit = sinon.spy();
const setUp = (props = {}) => {
const component = shallow(<UnwrappedLoginForm {...props} />);
return component;
};
describe('Login Form Component', () => {
let wrapper;
beforeEach(() => {
const props = {
values: { username: '', password: '' },
errors: { username: false, password: false },
touched: { username: false, password: false },
handleChange: mockChange,
handleSubmit: mockSubmit,
isSubmitting: false,
handleBlur: mockBlur,
classes: {
textField: '',
},
};
wrapper = setUp(props);
});
describe('When the input value is inserted', () => {
it('renders new username value', done => {
const input = wrapper.find('#username');
input.simulate('change', { target: { name: 'username', value: 'y' } });
setTimeout(() => {
wrapper.update();
expect(mockChange.calledOnce).toEqual(true);
done();
}, 1000);
});
});
});
Try the following:
const waitForNextTick = process.nextTick
waitForNextTick(() => {
expect(mockChange.calledOnce).toBe(true); // test is passed.
expect(input.props().value).toEqual('y'); // Expected value to equal: "y" , Received: ""
done();
});
From the code you have provided, I don't think the value of the input is going to be updated.
You are triggering the change event on TextField component. What this triggers is the onChange callback for the input, which in turn executes the handleChange prop of your UnwrappedLoginForm component. But this does not change the input value per se.
Related
I have a Formik signup form that is validated with yup. When validation schema isn't fulfilled i want to show error component. Everything works in browser, but when i do my tests, even though there should be errors already jest and enzyme behaves like there was no error. Can someone tell me what am I doing wrong? It's strange because when I put console.log(errors.email) in Formik's return function i see that there is 'It does not seem to be a valid email address.' error in my test console. From the other hand when i put erros.email in Formik's return function, in test it looks like it doesn't exist.
// SignUp.tsx
const SignUp: React.FC = () => {
const { register, errorMessage } = React.useContext(AuthContext);
const initialValues = {
email: '',
password: '',
passwordRepeat: '',
};
const hasErrors = (errors: FormikErrors<typeof initialValues>) => {
return Object.keys(errors).length !== 0 || errorMessage;
};
const allFieldsWereTouched = (touched: FormikTouched<typeof initialValues>) => {
return Array.from(Object.values(touched)).filter((wasTouched) => wasTouched).length === Object.keys(initialValues).length;
};
const signUpValidationSchema = Yup.object().shape({
email: Yup.string().email('It does not seem to be a valid email address.'),
password: Yup.string().min(5, 'Password must have at least 5 characters.').max(15, 'Password must not have more than 15 characters.'),
passwordRepeat: Yup.string().oneOf([Yup.ref('password')], 'Both passwords must be equal.'),
});
const submitForm = (values: typeof initialValues) => {
register(values.email, values.password);
};
return (
<AuthWrapper>
<h3 className={classes.authModalTitle} data-test='signup-modal-title'>
Sign Up
</h3>
<Formik initialValues={initialValues} onSubmit={submitForm} validationSchema={signUpValidationSchema}>
{({ errors, touched }) => {
return (
<Form>
<FormikTextInput type='email' name='email' description='E-mail address' as={Input} />
<FormikTextInput type='password' name='password' description='Password' as={Input} />
<FormikTextInput type='password' name='passwordRepeat' description='Password repeat' as={Input} />
{errors.email && <Error message={errors.email || errors.password || errors.passwordRepeat || errorMessage} />}
<Button className={classes.buttonAdditional} type='submit' dataTest='signup-button'>
Open account
</Button>
</Form>
);
}}
</Formik>
<FormInfo question='Already have an account?' answer='Sign in' path='SignIn' />
</AuthWrapper>
);
};
export default SignUp;
// SignUp.test.tsx
const setup = () => {
return mount(
<MemoryRouter>
<AuthContextProvider>
<SignUp />
</AuthContextProvider>
</MemoryRouter>
);
};
describe('<SignUp />', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = setup();
});
afterEach(() => {
wrapper.unmount();
});
describe('showing errors', () => {
const simulateTyping = async (name: string, value: string | number) => {
const formField = formikFindByInputName(wrapper, name);
formField.simulate('change', { target: { name, value } });
formField.simulate('blur');
};
const checkErrorExistance = (wrapper: ReactWrapper, condition: boolean) => {
const error = findByTestAttr(wrapper, 'error');
expect(error.exists()).toBe(condition);
};
it('does not show error when email input value is wrong but not all inputs were touched', () => {
simulateTyping('email', 'test');
checkErrorExistance(wrapper, true);
});
});
});
I had the same issue and had to run validateForm myself, so something like:
<Formik>{({..., validateForm}) =>
<Button onClick={async () => {const newErrors = await validateForm?.()}} />
Then check the newErrors.
I am trying to test a controlled form component with Enzyme-Jest. I have simulated a 'change' event which should call my handleFormChange method which is passed down as a prop from parent. When I check the props for the ImproveListing component, they do not update/change after I simulate this event. Thanks
describe('<ImproveListing /> Form', () => {
let improveListing;
let app;
let instance;
beforeEach(() => {
app = mount(<Attraction />);
instance = app.instance();
improveListing = mount(<ImproveListing />);
});
afterEach(() => {
app.unmount();
improveListing.unmount();
});
test('it should update form data with state', () => {
improveListing.setProps({ clicked: true });
const input = improveListing.find('input').first();
input.simulate('change', { target: { value: 'test', name: 'description' } });
const changedInput = improveListing.find('input').first();
expect(changedInput.props().value).toEqual('test');
});
this is the console output after running the test
<Overview /> › <ImproveListing /> Form › HandleFormChange › it should update form data with state
expect(received).toEqual(expected) // deep equality
Expected: "test"
Received: ""
82 | // improveListing.setProps({ form: { description: 'Test' } });
83 | const changedInput = improveListing.find('input').first();
> 84 | expect(changedInput.props().value).toEqual('test');
| ^
85 | });
86 | });
87 | });
//ImproveListing.js
import React from 'react';
import PropTypes from 'prop-types';
const ImproveListing = ({ clicked, form, handleFormChange, handleClick }) => (
<div className="improveListing">
{clicked ? (
<form className="improve" onSubmit={() => {}}>
<input name="description" placeholder="description" type="text" value={form.description} onChange={handleFormChange} />
</form>
) : <div onClick={handleClick}>Improve This Listing</div>}
</div>
);
//Overview.js (parent to ImproveListing)
const Overview = ({ overview, form, handleFormChange, clicked, handleClick }) => (
<div className="overview">
<ImproveListing
form={form}
handleFormChange={handleFormChange}
clicked={clicked}
handleClick={handleClick}
/>
</div>
);
//attraction.js (parent to overview)
export default class Attraction extends React.Component {
constructor(props) {
super(props);
this.state = {
current: null,
likeHover: false,
form: {
description: '',
isOpen: false,
suggestedDuration: 0,
address: '',
},
clickImproved: false,
};
this.updateHeartHover = this.updateHeartHover.bind(this);
this.handleFormChange = this.handleFormChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleFormChange(e) {
const { form } = this.state;
this.setState({
form: {
...form,
[e.target.name]: e.target.value,
},
});
}
render() {
const {
current, likeHover, form, clickImproved,
} = this.state;
return (
<>
{current ? (
<div className="attraction">
<Overview
overview={current.overview}
form={form}
clicked={clickImproved}
handleClick={this.handleClick}
handleFormChange={this.handleFormChange}
/>
</>
);
}
}
This test passes.
test('it should call handleFormChange when there is a change', () => {
improveListing.setProps({ clicked: true });
const spy = jest.spyOn(instance, 'handleFormChange');
improveListing.setProps({ handleFormChange: instance.handleFormChange });
const input = improveListing.find('input').first();
input.simulate('change');
expect(spy).toHaveBeenCalledTimes(1);
});
I am trying to build a small app to test my jest and enzyme knowledge. However, I have run into an issue.
I have the following Homepage.js:
const Homepage = props => {
const [input, setInput] = React.useState({
type: 'input',
config: {
required: true,
placeholder: 'Type a GitHub username'
},
value: ''
});
const onChangeInput = event => {
setInput(prevState => {
return {
...prevState,
value: event.target.value
};
});
};
return (
<section className={styles.Homepage}>
<form className={styles.SearchBox} onSubmit={(event) => props.getUser(event, input.value)} data-test="component-searchBox">
<h4>Find a GitHub user</h4>
<Input {...input} action={onChangeInput}/>
{props.error ? <p>{`Error: ${props.error}`}</p> : null}
<Button>Submit</Button>
</form>
</section>
);
};
I want to test that the input field fires a function when I type in it. The Input.js component is the following:
const input = props => {
switch (props.type) {
case 'input':
return <input className={styles.Input} {...props.config} value={props.value} onChange={props.action} data-test="component-input"/>
default: return null;
};
};
You can find my test below:
const mountSetup = () => {
const mockGetUser = jest.fn()
return mount(
<Homepage type='input' getUser={mockGetUser}>
<Input type='input'/>
</Homepage>
);
};
test('State updates with input box upon change', () => {
const mockSetInput = jest.fn();
React.useState = jest.fn(() => ["", mockSetInput]);
const wrapper = mountSetup();
const inputBox = findByTestAttr(wrapper, 'component-input');
inputBox.simulate("change");
expect(mockSetInput).toHaveBeenCalled();
});
The problem here is that in the Input.js the switch is always returning null even though I am passing the props type='input'. As such I get the error Method “simulate” is meant to be run on 1 node. 0 found instead.
Can someone help me with this?
Thanks
You need not pass the Input inside the HomePage. Homepage has input child and it renders
<Homepage type='input' getUser={mockGetUser}>
</Homepage>
You can try passing the data in the react Usestate mock.
React.useState = jest.fn(() => [{
type: 'input'
}, mockSetInput]);
This is not a duplicate question because non of them worked for me and they were doing something different as well.
I have a component of email and password with form validation and now I want to do unit testing, and i want to simulate change on input but i am not able to do it. I have tried both shallow and mount i didn't try render but it wouldn't work either i read it in some documentations. I read somewhere that functions are needed to be binded or something but i am new to testing so i don't know much. This question might be super easy but i have been working on it for hours and i need to get it done. So please guide me and also suggest me if what i am doing is optimal or not.
sign-in.test.jsx
import React from "react";
import { shallow } from "enzyme";
import { BrowserRouter } from "react-router-dom";
import { findByTestAttr } from "../../Utils/testUtils.js";
import SignIn from "./sign-in.component";
const INITIAL_STATE = {
email: "",
password: "",
rememberme: false,
emailErr: "",
passwordErr: "",
buttonErr: "",
progressBar: 0,
disable: true,
};
const setUp = (props = {}) => {
const component = shallow(<SignIn />); // Have also done like this: mount(<BrowserRouter><SignIn /><BrowserRouter />)
return component;
};
//Issue is with the last test case but feel free to suggest me about other tests as well
describe("Sign in component should render ", () => {
let component;
beforeEach(() => {
component = setUp();
});
test("Email input field", () => {
const wrapper = findByTestAttr(component, "email");
expect(wrapper.length).toBe(1);
});
test("It should render password input field", () => {
const wrapper = findByTestAttr(component, "password");
expect(wrapper.length).toBe(1);
});
test("It should render a remember me checkbox", () => {
const wrapper = findByTestAttr(component, "remember-me");
expect(wrapper.length).toBe(1);
});
test("It should render a forget password link", () => {
const wrapper = findByTestAttr(component, "forget-password");
expect(wrapper.length).toBe(1);
});
test("It should render a sign in button", () => {
const wrapper = findByTestAttr(component, "sign-in");
expect(wrapper.length).toBe(1);
});
test("It should render a google sign in button", () => {
const wrapper = findByTestAttr(component, "google-sign-in");
expect(wrapper.length).toBe(1);
});
test("It should render a facebook sign in button", () => {
const wrapper = findByTestAttr(component, "facebook-sign-in");
expect(wrapper.length).toBe(1);
});
});
describe("Form validation with different values", () => {
let component;
beforeEach(() => {
component = setUp();
});
test("Form values", () => {
const input = component.find(".email").at(0);
input.simulate("change", { target: { value: "check" } });
console.log(component.state.email); //undefined
});
});
sign-in.component.jsx
import React from "react";
import { Link } from "react-router-dom";
import CustomButton from "../../components/custom-button/custom-button.component";
import CompanyLogo from "../../components/company-logo-header/company-logo-header.component";
import InputField from "../../components/input-field/input-field.component";
import InputError from "../../components/input-field-error/input-field-error.component";
import { mailFormat, passwordFormat } from "../../Utils/regularExpressions";
import "./sign-in.styles.scss";
const INITIAL_STATE = {
email: "",
password: "",
rememberme: false,
emailErr: "",
passwordErr: "",
buttonErr: "",
progressBar: 0,
disable: true,
};
class SignIn extends React.Component {
constructor() {
super();
this.state = INITIAL_STATE;
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value,
});
const isValid = this.validate();
if (isValid) {
this.setState({ disable: false });
} else {
this.setState({ disable: true });
}
};
validate = () => {
let emailErr = "";
let passwordErr = "";
if (!this.state.email.match(mailFormat)) {
emailErr = "Please enter a valid email format";
}
if (!this.state.password.match(passwordFormat)) {
passwordErr = "Password should be eight digit with letters and numbers";
}
if (emailErr || passwordErr) {
this.setState({
emailErr,
passwordErr,
});
return false;
}
this.setState({
emailErr,
passwordErr,
});
return true;
};
handleSubmit = async (event) => {
event.preventDefault();
if (this.state.disable) {
await this.setState({
buttonErr: "Please enter the email and password",
});
return;
}
const isValid = this.validate();
if (isValid) {
console.log(isValid);
this.setState(INITIAL_STATE);
}
};
render() {
const { disable } = this.state;
return (
<div className="sign-in">
<CompanyLogo />
<form noValidate className="sign-in-form" onSubmit={this.handleSubmit}>
<InputField
name="email"
type="email"
placeholder="Email Address"
data-test="email"
className="email"
onChange={this.handleChange}
value={this.state.email}
/>
<InputError>{this.state.emailErr}</InputError>
<InputField
name="password"
type="password"
placeholder="Password"
data-test="password"
onChange={this.handleChange}
value={this.state.password}
/>
<InputError>{this.state.passwordErr}</InputError>
<div className="remember-forget">
<span className="remember-me">
<input
type="checkbox"
name="rememberme"
value="Remember Me"
data-test="remember-me"
/>
<label htmlFor="rememberme" className="rememberme">
{" "}
Remember me
</label>
</span>
<Link
to="password-reset"
className="forgot-password"
data-test="forget-password"
>
Forgot password?
</Link>
</div>
<div className="center-buttons">
<CustomButton
data-test="sign-in"
type="submit"
SignIn
disabled={disable}
>
Sign In
</CustomButton>
<InputError>{this.state.buttonErr}</InputError>
</div>
<div className="separator">Or</div>
<div className="center-buttons">
<CustomButton data-test="facebook-sign-in" type="button" Options>
Sign in with facebook
</CustomButton>
<CustomButton data-test="google-sign-in" type="button" Options>
Sign in with google
</CustomButton>
</div>
</form>
</div>
);
}
}
export default SignIn;
I have an application with 2 inputs; name and password. Also, I have a save button that should change the state, using the values from the inputs, in the parent component.
But now, if I insert just one value in one input, I lose the state in the parent component. For example, if I type just name, and click save button, in the parent component I lose the password value, but I don't know why.
How to avoid this using my code?
import React, { useEffect } from "react";
import "./styles.css";
const Test = ({ user, setUser }) => {
const [u, setU] = React.useState("");
const [p, setP] = React.useState("");
function name(e) {
const a = e.target.value;
setU(a);
}
function password(e) {
const a = e.target.value;
setP(a);
}
function save() {
console.log(u);
setUser({ ...user, name: u, password: p });
}
return (
<div>
<input onChange={name} />
<input onChange={password} />
<button onClick={save}>save</button>
</div>
);
};
export default function App() {
const [user, setUser] = React.useState({
name: "",
password: ""
});
useEffect(() => {
setUser({ name: "john", password: "Doe" });
}, []);
return (
<div className="App">
<p>{user.name}</p>
<p>{user.password}</p>
<Test user={user} setUser={setUser} />
</div>
);
}
code link: https://stackblitz.com/edit/react-ytowzg
This should fix your issue:
function save() {
console.log(u);
if(u === '') {
setUser({ ...user, password: p });
} else if (p === '') {
setUser({ ...user, name: u });
} else {
setUser({ ...user, name: u, password: p });
}
}
So, now the state is conditionally updated based on the values of the input fields. The issue with your code is that you're always overwriting the state regardless of the input values.
i have a better proposition,instead of using a separate state variable for name ,password and percentage use a single state variable object
Test.js
import React, { useState } from "react";
import { InputNumber } from "antd";
import "antd/dist/antd.css";
const Test = ({ user, setUser }) => {
const [state, setState] = useState({
name: "",
password: "",
percentage: ""
});
function onChange(e, name) {
setState({
...state,
...(name === undefined
? { [e.target.name]: e.target.value }
: { [name]: e })
});
console.log(state);
}
function save() {
setUser({
...user,
...(state.name !== "" && { name: state.name }),
...(state.password !== "" && { password: state.password }),
...(state.percentage !== "" && { percentage: state.percentage })
});
}
return (
<div>
<input name='name' onChange={onChange} />
<input name='password' onChange={onChange} />
<InputNumber
defaultValue={100}
min={0}
max={100}
formatter={value => `${value}%`}
parser={value => value.replace("%", "")}
onChange={e => onChange(e, "percentage")}
/>
<button onClick={save}>save</button>
</div>
);
};
export default Test;
Updated CodeSandbox here