Unit test form submission with data using react testing library - reactjs

I have a react component with a form. I want to unit test (using jest and RTL) if form gets submitted with correct data. Here are my component and unit test method:
Component:
class AddDeviceModal extends Component {
handleOnSave(event) {
const { deviceName } = event.target;
const formData = {
deviceName: deviceName.value,
};
this.props.onSave(formData);
}
render() {
return (
<Form onSubmit={this.handleOnSave}>
<Form.Label>Device Name</Form.Label>
<Form.Control name="deviceName" placeholder="Device Name" required />
<Button type="submit">Save Device</Button>
</Form>
);
}
}
Unit Test:
it("Test form submit and validation", () => {
const handleSave = jest.fn();
const props = {
onSave: handleSave,
};
render(<AddDeviceModal {...props} />);
const deviceNameInput = screen.getByPlaceholderText(/device name/i);
fireEvent.change(deviceNameInput, { target: { value: "AP VII C2230" } });
fireEvent.click(getByText(/save device/i));
});
However, in handleOnSave(), I get error as deviceName is undefined. For some reason, it is not able to get the textbox value from event.target. Am I doing something wrong in above code? Needed help in fixing this issue.

The problem you have it with trying to access the input directly from event.target. You should access it from event.target.elements instead: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements.
function handleOnSave(event) {
event.preventDefault();
const { deviceName } = event.target.elements;
const formData = {
deviceName: deviceName.value
};
// this will log the correct formData even in tests now
console.log(formData);
this.props.onSave(formData);
}
And here is your test:
it("Test form submit and validation", () => {
const { getByPlaceholderText, getByText } = render(<App />);
const deviceNameInput = getByPlaceholderText(/device name/i);
fireEvent.change(deviceNameInput, { target: { value: "AP VII C2230" } });
fireEvent.click(getByText(/Save Device/i));
});
I created a codesandbox where you can see this in action: https://codesandbox.io/s/form-submit-react-testing-library-45pt8?file=/src/App.js

Related

Not being able to simulate change on input and call onChange function using both shallow and mount. Enzyme, jestjs, react

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;

asynchronous function test onChange event fails in enzyme and jest

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.

React Testing Library fireEvent.change not working with fireEvent.submit

tldr; fireEvent.change works, but on form submit, new value not found in submit handler.
Say we have a simple form:
// MyForm.tsx
class MyForm extends React.Component {
state = { data: '123' }
handleChange = (e: any) => {
// This never gets fired in the test, why?
console.log('HandleChange Fired', e.target.value)
this.setState({ data: e.target.value })
}
handleSubmit = () => {
console.log('************* HandleSubmit Fired **************', this.state)
}
render() {
return (
<form name="address" onSubmit={this.handleSubmit}>
<input name="title" value={this.state.data} onChange={this.handleChange} />
<button type="submit">Submit</button>
</form>
)
}
}
and a test to assert the form submission values are accurate:
// MyForm.spec.tsx
import React from 'react'
import { expect } from 'chai'
import { fireEvent, render, wait } from 'react-testing-library'
import { JSDOM } from 'jsdom'
import MyForm from './MyForm'
.
const dom = new JSDOM('<!doctype html><html><body><div id="root"><div></body></html>')
global.document = dom.window.document
global.window = dom.window
global.navigator = dom.window.navigator
.
describe.only('MyForm works!', () => {
it('Should change the value of the field', async () => {
const { container, debug } = render(<MyForm />)
const field: any = container.querySelector('input[name="title"]')
.
// Change the value
fireEvent.change(field, { target: { value: 'Hello World!' } })
expect(field.value).to.equal('Hello World!') // true
.
console.log('Field value: ', field.value) // prints 'Hello World!'
debug(field) // html is printed out in console, shows old value unexpectedly.
.
// Submit the form
const form: any = container.querySelector('form[name="address"]')
const onSubmit = fireEvent.submit(form)
expect(onSubmit).to.equal(true) // true, but form submit value is still the old one
})
})
Here are the test results:
Here are my versions:
"jsdom": "^11.5.1",
"mocha": "^5.1.1",
"react": "^16.8.4",
"react-testing-library": "^6.0.0"
"dom-testing-library": "3.17.1"
How do I get the form handleSubmit value to reflect the new input value after the onChange?

Cannot read property 'value' when simulating a form submit

I am trying to do a complete istanbul coverage test with jest. At this moment I have a component almost all tested but there is a handleSubmit function where I make a dispatch receiving form event data and when I run the test it tells me
TypeError: Cannot read property 'value' of undefined
10 | payload: {
11 | name: name.value,
> 12 | item: item.value,
| ^
13 | status: status.value }
14 | })
15 | }
I am loading a mockstore, mounted all the component, its all tested but the submit still fails. My test function is as simple as:
it('testing submit', () => {
const form = component.find(`[data-test="submit"]`).first()
form.simulate('submit')
... and continues expecting some existences, but there aren't problems there
I already tried this: enzyme simulate submit form, Cannot read property 'value' of undefined
And tried to parse the event values in the simulate action...
The complete module code is...
class Filters extends Component {
handleSubmit = event => {
event.preventDefault()
const {name, items, status} = event.target;
this.props.dispatch({
type: 'SEARCH_PLAYER',
payload: {
name: name.value,
item: item.value,
status: status.value }
})
}
render() {
return(
<div>
<form onSubmit={this.handleSubmit} data-test="submit">
<div className="form-group col-md-12 text-center"> ...
Another really crazy thing is that my test recognize the "event.target.name.value" and not the items and status. In fact if i delete items and status from the dispatch the test runs successfully.
Looks like you are using item on line 12, but extracting items from the event.target.
The way you chose to handle values is a bit strange. Instead, handle values through state like so: Controlled Components
Then you can test that this.props.dispatch() was called with the correct values.
Side note: Avoid using data attributes when unnecessary, as they'll start to clog up your DOM with superfluous attributes. You have plenty of options to find by element, element.className, className, ...and so on.
Working example: https://codesandbox.io/s/5j4474rkk (you can run the test defined below by clicking on the Tests tab at the bottom left of the screen.
components/Form/Form.js
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class Form extends Component {
state = {
test: ""
};
static propTypes = {
dispatch: PropTypes.func.isRequired
};
handleChange = ({ target: { name, value } }) => {
this.setState({ [name]: value });
};
handleSubmit = e => {
e.preventDefault();
this.props.dispatch({
type: "SEARCH_PLAYER",
payload: {
test: this.state.test
}
});
};
render = () => (
<form onSubmit={this.handleSubmit} className="form-container">
<h1>Form Testing</h1>
<input
className="uk-input input"
type="text"
name="test"
placeholder="Type something..."
onChange={this.handleChange}
value={this.state.test}
/>
<button type="submit" className="uk-button uk-button-primary submit">
Submit
</button>
</form>
);
}
components/Form/__tests__/Form.js (shallowWrap and checkProps are custom functions that can be found in test/utils/index.js)
import React from "react";
import { shallowWrap, checkProps } from "../../../test/utils";
import Form from "../Form";
const dispatch = jest.fn();
const initialProps = {
dispatch
};
const initialState = {
test: ""
};
const wrapper = shallowWrap(<Form {...initialProps} />, initialState);
describe("Form", () => {
it("renders without errors", () => {
const formComponent = wrapper.find(".form-container");
expect(formComponent).toHaveLength(1);
});
it("does not throw PropType warnings", () => {
checkProps(Form, initialProps);
});
it("submits correct values to dispatch", () => {
const name = "test";
const value = "Hello World!";
const finalValues = {
type: "SEARCH_PLAYER",
payload: {
[name]: value
}
};
wrapper.find("input").simulate("change", { target: { name, value } }); // simulates an input onChange event with supplied: name (event.target.name) and value (event.target.value)
wrapper
.find(".form-container")
.simulate("submit", { preventDefault: () => null }); // simulates a form submission that has a mocked preventDefault (event.preventDefault()) to avoid errors about it being undefined upon form submission
expect(dispatch).toBeCalledWith(finalValues); // expect dispatch to be called with the values defined above
});
});

Testing onChange function in Jest

I'm relatively new to Jest and testing in general. I have a component with an input element:
import * as React from "react";
export interface inputProps{
placeholder: string;
className: string;
value: string;
onSearch: (depID: string) => void;
}
onSearch(event: any){
event.preventDefault();
//the actual onclick event is in another Component
this.props.onSearch(event.target.value.trim());
}
export class InputBox extends React.Component<inputProps, searchState> {
render() {
return (
<input
onChange={this.onSearch} //need to test this
className={this.props.className}
type="text"
value={this.props.value}
placeholder={this.props.placeholder} />
);
}
}
I want a test that checks that input element's onChange is a function that takes in the input element's value attribute as the parameter. This is how far I have gotten so far:
//test to see the input element's onchange
//returns a function that takes its value as a param
it("onChange param is the same value as the input value", () => {
const mockFn = jest.fn();
const input = enzyme.shallow(<InputBox
value="TestVal"
placeholder=""
className=""
onSearch={mockFn}/>);
input.find('input').simulate('change', { preventDefault() {} });
expect(mockFn.mock.calls).toBe("TestVal");
});
I am going off of the first solution here Simulate a button click in Jest
And: https://facebook.github.io/jest/docs/en/mock-functions.html
Edit: Running the above throws the following error:
TypeError: Cannot read property 'value' of undefined
Syntax on your code snippet I think should be:
import React from 'react';
export default class InputBox extends React.Component {
onSearch(event) {
event.preventDefault();
this.props.onSearch(event.target.value.trim());
}
render () { return (<input onChange={this.onSearch.bind(this)} />); }
}
The test is failing because, as same you define the preventDefault function on the event object, you also must define other properties used on the onSearch function.
it('should call onChange prop', () => {
const onSearchMock = jest.fn();
const event = {
preventDefault() {},
target: { value: 'the-value' }
};
const component = enzyme.shallow(<InputBox onSearch={onSearchMock} />);
component.find('input').simulate('change', event);
expect(onSearchMock).toBeCalledWith('the-value');
});
Previous test code needs to define the event shape because you are using shallow rendering. If you want instead to test that the actual input value is being used on your onSearch function you need to try a full render with enzyme.mount:
it('should call onChange prop with input value', () => {
const onSearchMock = jest.fn();
const component = enzyme.mount(<InputBox onSearch={onSearchMock} value="custom value" />);
component.find('input').simulate('change');
expect(onSearchMock).toBeCalledWith('custom value');
});
For those testing using TypeScript (and borrowing from the answers above), you'll need to perform a type coercion (as React.ChangeEvent<HTMLInputElement>) to ensure that the linter can view the signature as being compatible:
React file
export class InputBox extends React.Component<inputProps, searchState> {
onSearch(event: React.ChangeEvent<HTMLInputElement>){
event.preventDefault();
//the actual onclick event is in another Component
this.props.onSearch(event.target.value.trim());
}
render() {
return (
<input
onChange={this.onSearch} //need to test this
className={this.props.className}
type="text"
value={this.props.value}
placeholder={this.props.placeholder} />
);
}
}
Test file
it('should call onChange prop', () => {
const onSearchMock = jest.fn();
const event = {
target: { value: 'the-value' }
} as React.ChangeEvent<HTMLInputElement>;
const component = enzyme.shallow(<InputBox onSearch={onSearchMock} />);
component.find('input').simulate('change', event);
expect(onSearchMock).toBeCalledWith('the-value');
});
or alternatively
it('should call onChange prop', () => {
const onSearchMock = jest.fn();
const event = {
target: { value: 'the-value' }
} as React.ChangeEvent<HTMLInputElement>;
const component = enzyme.mount<InputBox>(<InputBox onSearch={onSearchMock} />);
const instance = component.instance();
instance.onSearch(event);
expect(onSearchMock).toBeCalledWith('the-value');
});
I figured out the solution.
So, instead of passing in the value inside InputBox, we have to pass it inside the second param of simulate as shown below. Then we simply check for equality against the first arg of the first call to the mockFn. Also, we can get rid of the event.preventDefault();
it("onChange param is the same value as the input element's value property", () => {
const mockFn = jest.fn();
const input = enzyme.shallow(<InputBox
value=""
placeholder=""
className=""
onSearch={mockFn}/>);
input.find('input').simulate('change', {target: {value: 'matched'} });
expect(mockFn.mock.calls[0][0]).toBe('matched');
});
How about this one? I simulate the change event using enzyme and perform a snapshot test.
Component
import React, { FunctionComponent, useState } from 'react';
const Index: FunctionComponent = () => {
const [val, setVal] = useState('');
const onInputChange = e => {
e.preventDefault();
setVal(e.target.value);
};
return (
<input type='text' onChange={onInputChange} value={val} />
);
};
export default Index;
Unit Test
describe('Index with enzyme', () => {
it('Should set value to state when input is changed', () => {
const container = shallow(<Index />);
const input = container.find('input');
input.simulate('change', { preventDefault: jest.fn, target: { value: "foo" } });
expect(container).toMatchSnapshot();
});
});
Snapshot
exports[`Index with enzyme Should set value to state when input is changed 1`] = `
<input
onChange={[Function]}
type="text"
value="foo"
/>
`;
I struggled with this for hours. Plus since I had multiple select fields on one page. What I found is that Textfield solution works differently from Select.test given on docs.
On the code I defined SelectProps with id. (You can also go with data-testid)
I could only trigger dropdown by clicking this field.
<TextField
select
variant = "outlined"
value = { input.value || Number(0) }
onChange = { value => input.onChange(value) }
error = { Boolean(meta.touched && meta.error) }
open = { open }
SelectProps = {
{
id: `${input.name}-select`,
MenuProps: {
anchorOrigin: {
vertical: "bottom",
horizontal: "left"
},
transformOrigin: {
vertical: "top",
horizontal: "left"
},
getContentAnchorEl: null
}
}
}
{ ...props} >
//yourOptions Goes here
</TextField>
And in my test.
const pickUpAddress = document.getElementById("address-select");
UserEvent.click(pickUpAddress);
UserEvent.click(screen.getByTestId("address-select-option-0"));
Worked like a charm afterwards. Hope this helps.
If you're writing lwc (salesforce) jest tests you can simulate this by selecting the input and dispatching an event.
const changeEvent = new CustomEvent('change', {
detail: {
'value': 'bad name'
}
});
element.shadowRoot.querySelector('lightning-input').dispatchEvent(changeEvent);

Resources