Formik form submission with react-testing library - reactjs

I am looking to fire a submit handler for a LoginForm. However, for some reason, instead of my mock function being called, the actual handler for the component gets fired (calling an external api). How can I ensure that my mock handler gets called instead?
The three components of interest are below (The presentational, container and the test suite)
LoginForm.js
import { Formik, Form, Field } from 'formik';
import { CustomInput } from '..';
const LoginForm = ({ initialValues, handleSubmit, validate }) => {
return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={handleSubmit}
>
{({ isSubmitting, handleSubmit }) => {
return (
<Form onSubmit={handleSubmit}>
<div className="d-flex flex-column justify-content-center align-items-center">
<Field
data-testid="usernameOrEmail"
type="text"
name="identifier"
placeholder="Username/Email"
component={CustomInput}
inputClass="mb-4 mt-2 text-monospace"
/>
<Field
data-testid="login-password"
type="password"
name="password"
placeholder="Password"
component={CustomInput}
inputClass="mb-4 mt-4 text-monospace"
/>
<button
data-testid="login-button"
className="btn btn-primary btn-lg mt-3 text-monospace"
type="submit"
disabled={isSubmitting}
style={{ textTransform: 'uppercase', minWidth: '12rem' }}
>
Submit
</button>
</div>
</Form>
)}}
</Formik>
);
};
export default LoginForm;
LoginPage.js
import React, { useContext } from 'react';
import { loginUser } from '../../services';
import { userContext } from '../../contexts';
import { loginValidator } from '../../helpers';
import { setAuthorizationToken, renderAlert } from '../../utils';
import LoginForm from './login-form';
const INITIAL_VALUES = { identifier: '', password: '' };
const LoginPage = props => {
const { handleUserData, handleAuthStatus } = useContext(userContext);
const handleSubmit = async (values, { setSubmitting }) => {
try {
const result = await loginUser(values);
handleAuthStatus(true);
handleUserData(result.data);
setAuthorizationToken(result.data.token);
props.history.push('/habits');
renderAlert('success', 'Login Successful');
} catch (err) {
renderAlert('error', err.message);
}
setSubmitting(false);
};
return (
<LoginForm
initialValues={INITIAL_VALUES}
validate={values => loginValidator(values)}
handleSubmit={handleSubmit}
/>
);
};
export default LoginPage;
LoginPage.spec.js
import React from 'react';
import { cleanup, getByTestId, fireEvent, wait } from 'react-testing-library';
import { renderWithRouter } from '../../../helpers';
import LoginPage from '../login-page';
afterEach(cleanup);
const handleSubmit = jest.fn();
test('<LoginPage /> renders with blank fields', () => {
const { container } = renderWithRouter(<LoginPage />);
const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
const passwordNode = getByTestId(container, 'login-password');
const submitButtonNode = getByTestId(container, 'login-button');
expect(usernameOrEmailNode.tagName).toBe('INPUT');
expect(passwordNode.tagName).toBe('INPUT');
expect(submitButtonNode.tagName).toBe('BUTTON');
expect(usernameOrEmailNode.getAttribute('value')).toBe('');
expect(passwordNode.getAttribute('value')).toBe('');
});
test('Clicking the submit button after entering values', async () => {
const { container } = renderWithRouter(<LoginPage handleSubmit={handleSubmit} />);
const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
const passwordNode = getByTestId(container, 'login-password');
const submitButtonNode = getByTestId(container, 'login-button');
fireEvent.change(usernameOrEmailNode, { target: { value: fakeUser.username }});
fireEvent.change(passwordNode, { target: { value: fakeUser.password }});
fireEvent.click(submitButtonNode);
await wait(() => {
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
expect(usernameOrEmailNode.tagName).toBe('INPUT');
expect(passwordNode.tagName).toBe('INPUT');
expect(submitButtonNode.tagName).toBe('BUTTON');
expect(usernameOrEmailNode.getAttribute('value')).toBe('');
expect(passwordNode.getAttribute('value')).toBe('');
});```

To answer your question, you will need to first make the handleSubmit constant accessible outside LoginPage.js so that it may be mocked and then tested. For example,
LoginPage.js
export const handleSubmit = async (values, { setSubmitting }) => {
... code to handle submission
})
And in your tests - LoginPage.spec.js
jest.unmock('./login-page');
import LoginPage, otherFunctions from '../login-page'
otherFunctions.handleSubmit = jest.fn();
...
test('Clicking the submit button after entering values', () => {
...
fireEvent.click(submitButtonNode);
expect(handleSubmit).toHaveBeenCalledTimes(1);
})
I hope the above fixes your problem.
But, going by the philosophy of unit testing, the above components
must not be tested the way you are doing it. Instead your test setup
should be like this -
Add a new test file called LoginForm.spec.js that tests your LoginForm component. You would test the following in this -
Check if all input fields have been rendered.
Check if the correct handler is called on submit and with the correct parameters.
The existing test file called LoginPage.spec.js would then only test if the particular form was rendered and then you could also test
what the handleSubmit method does individually.
I believe the above would make your tests more clearer and readable
too, because of the separation of concerns and would also allow you to
test more edge cases.

Related

passing a variable from child component to parent component in Next.js

I have 2 components home and tiny tiny is imported inside home as u can see in the code
I am trying to pass value.toString("html") from tiny.js to home.js
if this is not possible at least help me integrate both tiny and home components as a single object so that I don't have to pass the value as props to a parent component
import React from "react";
import Tiny from "./tiny";
function Home({ data }) {
const [Questions, setQuestions] = useState();
const [deatils1, setdeatils] = useState();
function clickQuestion() {
axios
.post("https://askover.wixten.com/questionpost", {
Name: Questions,
Summary: deatils1,//pass tiny value as summery
})
.then(() => {
window.location.reload();
});
}
function question(e) {
setQuestions(e.target.value);
}
return (
<>
<div>
<div className="container search-box">
<Form>
<Form.Group className="mb-3" controlId="exampleForm.ControlInput1">
<Form.Label>Title</Form.Label>
<Form.Control
type="text"
onChange={question}
placeholder="ask anything?"
/>
</Form.Group>
<Tiny /> //tiny component
</Form>
<Button
type="submit"
disabled={!deatils1 || !Questions}
onClick={clickQuestion}
variant="outline-secondary"
id="button-addon2"
>
ask?
</Button>
</div>
</div>
</>
);
}
tiny.js
import React, { useState, useEffect } from "react";
import dynamic from "next/dynamic";
import PropTypes from "prop-types";
//import the component
const RichTextEditor = dynamic(() => import("react-rte"), { ssr: false });
const MyStatefulEditor = ({ onChange }) => {
const [value, setValue] = useState([]);
console.log(value.toString("html"));
useEffect(() => {
const importModule = async () => {
//import module on the client-side to get `createEmptyValue` instead of a component
const module = await import("react-rte");
console.log(module);
setValue(module.createEmptyValue());
};
importModule();
}, []);
const handleOnChange = (value) => {
setValue(value);
if (onChange) {
onChange(value.toString("html"));
}
};
return <RichTextEditor value={value} onChange={handleOnChange} />;
};
MyStatefulEditor.propTypes = {
onChange: PropTypes.func,
};
export default MyStatefulEditor;
Actually, you already have onChange event in tiny, so you only need to pass another onChange event from home to tiny.
import React from "react";
import Tiny from "./tiny";
function Home({ data }) {
const [Questions, setQuestions] = useState();
const [details, setDetails] = useState();
function clickQuestion() {
axios
.post("https://askover.wixten.com/questionpost", {
Name: Questions,
Summary: details,//pass tiny value as summery
})
.then(() => {
window.location.reload();
});
}
function question(e) {
setQuestions(e.target.value);
}
return (
<>
<div>
<div className="container search-box">
<Form>
<Form.Group className="mb-3" controlId="exampleForm.ControlInput1">
<Form.Label>Title</Form.Label>
<Form.Control
type="text"
onChange={question}
placeholder="ask anything?"
/>
</Form.Group>
<Tiny onChange={(value) => setDetails(value)}/> //tiny component
</Form>
<Button
type="submit"
disabled={!deatils1 || !Questions}
onClick={clickQuestion}
variant="outline-secondary"
id="button-addon2"
>
ask?
</Button>
</div>
</div>
</>
);
}

Unable to test React useState hook using Mocha and Enzyme

I have a login Component with two inputs one is for username and the other is for password. I have onChange function for both these inputs. I have written some test cases which works fine. I need to write few more test cases which involves testing of initial state and state after updating. I have researched a lot but couldn't find suitable example for useState hooks testing with MOCHA and Enzyme.
My last test case is failing.
Mocha is mandatory for me. Any help please?
Login Component
----------------
import React from 'react'
import { Form, FormGroup, TextInput, Button } from 'carbon-components-react'
import Login16 from '#carbon/icons-react/lib/login/16'
import { PropTypes } from 'prop-types'
function LoginComponent(props) {
const { username, password, onUsernameChange, onPasswordChange, onSubmit } = props;
return (
<div className="bx--row login-box">
<div className="bx--col-xs-6 bx--col-sm-6 bx--col-md-6 bx--col-lg-6 login-form">
<Form className="form-box">
<FormGroup legendText="Login">
<div>Sign in to your account</div>
<TextInput
className="login-input"
id="username"
name="username"
value={username}
onChange={onUsernameChange}
labelText=""
placeholder="User Name"
type="text"
/>
<TextInput
className="login-input"
id="password"
name="password"
value={password}
onChange={onPasswordChange}
labelText=""
placeholder="Password"
type="password"
/>
<Button
className=""
id="login-btn"
onClick={onSubmit}
>
<Login16 className="login-icon"/> Login
</Button>
</FormGroup>
</Form>
</div>
<div className="bx--col-xs-6 bx--col-sm-6 bx--col-md-6 bx--col-lg-6 login-image">
<img src="/ibmlogo.png" alt=""/>
</div>
</div>
)
}
LoginComponent.propTypes = {
username: PropTypes.string,
password: PropTypes.string,
onUsernameChange: PropTypes.func,
onPasswordChange: PropTypes.func,
onSubmit: PropTypes.func
}
export default LoginComponent
Login.test.js
-------------
import React from 'react'
import { shallow } from 'enzyme'
import chai, { expect } from 'chai';
import chaiEnzyme from 'chai-enzyme';
import { spy } from 'sinon'
import LoginComponent from '../components/Login';
describe('Login component testing', () => {
const handleChange = spy();
const props = {
username: "",
password: "",
onUsernameChange: handleChange,
onPasswordChange: handleChange,
onSubmit: () => {}
}
const wrapper = shallow(<LoginComponent {...props}/>);
let input;
beforeEach(() => {
input = ""
})
afterEach(() => {
})
it('Should have two inputs', () => {
input = wrapper.find('.login-input');
expect(input).to.have.length(2);
})
it('Should have one button to handle onSubmit', () => {
input = wrapper.find('#login-btn');
expect(input).to.have.length(1);
})
it('Should have an initial state for username and password to be empty string or undefined', () => {
expect(wrapper.find("#username").prop('value')).to.equal('');
expect(wrapper.find("#password").prop('value')).to.equal('');
})
it('Should have props for onUsernameChange, onPasswordChange, and onSubmit', () => {
expect(wrapper.find('#username').props().onChange).to.not.be.an('undefined');
expect(wrapper.find('#password').props().onChange).to.not.be.an('undefined');
expect(wrapper.find('#login-btn').props().onClick).to.not.be.an('undefined');
})
//below test case is failing.
it('Should update state for username and password onChange', () => {
const func = wrapper.find('#username');
func.simulate('change', { target: { value: "username" } })
console.log(wrapper.find('#username').debug())
expect(wrapper.find("#username").prop('value')).to.equal('username');
})
chai.use(chaiEnzyme());
})

Fix this react memory leak

I need to fix a memory leak in my app but Im not sure how to. I have a component that uses a modal and I get the error when I am adding an item. The modal is reusable and I use it in other components as well. This is the main component:
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Card, Select, Form, Button } from 'antd';
import Table from 'components/Table';
import Modal from '../Modal';
import styles from '../index.module.scss';
const { Item } = Form;
const { Option } = Select;
const PersonForm = ({ details, form }) => {
const [modalVisible, setModalVisible] = useState(false);
const [name, setName] = useState(
details?.name ? [...details?.name] : []
);
useEffect(() => {
form.setFieldsValue({
name: name || [],
});
}, [form, details, name]);
const addName = values => {
setName([...name, values]);
setModalVisible(false);
};
const removeName = obj => {
setName([...name.filter(i => i !== obj)]);
};
const cancelModal = () => {
setModalVisible(false);
};
return (
<div>
<Card
title="Names
extra={
<Button type="solid" onClick={() => setModalVisible(true)}>
Add Name
</Button>
}
>
<Table
tableData={name}
dataIndex="name"
removeName={removeName}
/>
</Card>
<Item name="name">
<Modal
title="Add Name"
fieldName="name"
onSubmit={addName}
visible={modalVisible}
closeModal={cancelModal}
/>
</Item>
</div>
);
};
PersonForm.propTypes = {
details: PropTypes.instanceOf(Object),
form: PropTypes.instanceOf(Object),
};
PersonForm.defaultProps = {
form: null,
details: {},
};
export default PersonForm;
And this is the modal component:
import React from 'react';
import PropTypes from 'prop-types';
import { Input, Form } from 'antd';
import Modal from 'components/Modal';
import LocaleItem from 'components/LocaleItem';
const { Item } = Form;
const FormModal = ({ visible, closeModal, onSubmit, fieldName, title }) => {
const [form] = Form.useForm();
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 15 },
};
const addItem = () => {
form
.validateFields()
.then(values => {
onSubmit(values, fieldName);
form.resetFields();
closeModal(fieldName);
})
.catch(() => {});
};
const canceledModal = () => {
form.resetFields();
closeModal(fieldName);
};
return (
<Modal
onSuccess={addItem}
onCancel={canceledModal}
visible={visible}
title={title}
content={
<Form {...layout} form={form}>
<Item
name="dupleName"
label="Name:"
rules={[
{
required: true,
message: 'Name field cannot be empty',
},
]}
>
<Input placeholder="Enter a name" />
</Item>
</Form>
}
/>
);
};
FormModal.propTypes = {
visible: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
FormModal.defaultProps = {};
export default FormModal;
I get a memory leak when I am in the test file when adding items in the modal. Can someone point out why this is happening and how to fix this? Thanks
Remove closeModal and form.resetFields from addItem function.
const addItem = () => {
form
.validateFields()
.then(values => {
onSubmit(values, fieldName); // when this onSubmit resolves it closes the modal, therefor these two lines below will be executed when component is unmounted, causing the memory leak warning
form.resetFields();
closeModal(fieldName);
})
.catch(() => {});
};
// instead maybe just:
const [form] = Form.useForm();
<Modal onOk={form.submit}>
<Form form={form}>
<Form.Item name="foo" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
</Modal>
Also, as far as I know you don't need to call form.validateFields as Ant Design's Form would do that automatically if rules are set in the Form.Item's.

Enzyme Returns More Nodes Than Exist When Using Mount

component.tsx
import React, { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { Form, FormControl, FormGroup, FormLabel } from "react-bootstrap";
import Button from "react-bootstrap/Button";
import Card from "react-bootstrap/Card";
import { useHistory } from "react-router-dom";
import { StorageKeys } from "../ProtectedRoute";
import "./styles.scss";
const Login = () => {
const history = useHistory();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSetEmail = (event: ChangeEvent<HTMLInputElement>) =>
setEmail(event.target.value);
const handleSetPassword = (event: ChangeEvent<HTMLInputElement>) =>
setPassword(event.target.value);
const handleSubmit = (event: FormEvent<HTMLElement>) => {
event.preventDefault();
console.log("email::", email);
console.log("password::", password);
localStorage.setItem(StorageKeys.TOKEN, "TODO: Auth");
history.push("/");
};
useEffect(() => {
localStorage.removeItem(StorageKeys.TOKEN);
}, []);
return (
<div id="login">
<Card id="loginCard">
<Card.Header>Login</Card.Header>
<Card.Body>
<Form onSubmit={handleSubmit}>
<FormGroup>
<FormLabel>Email address</FormLabel>
<FormControl type="email" id="email" placeholder="Enter email"
value={email} onChange={handleSetEmail}
required={true} />
</FormGroup>
<FormGroup>
<FormLabel>Password</FormLabel>
<FormControl type="password" id="password" placeholder="Password"
value={password} onChange={handleSetPassword}
required={true} />
</FormGroup>
<div className={"button-container"}>
<Button id="submit" variant="primary" type="submit">
Submit
</Button>
</div>
</Form>
</Card.Body>
</Card>
</div>
);
};
export default Login;
This works when using shallow to render the component:
login.test.tsx
import { mount, shallow } from "enzyme";
import React from "react";
import Login from "./index";
describe("Login Component", () => {
test("can properly submit form", () => {
jest.spyOn(window.localStorage.__proto__, "removeItem");
const wrapper = shallow(<Login />);
// This works just find, finds only the one #email input.
const emailInput = wrapper.find("#email");
emailInput.simulate("change", { target: { value: testLoginData.email } });
});
});
When using mount this throws the error:
Error: Method “simulate” is meant to be run on 1 node. 2 found instead
import { mount, shallow } from "enzyme";
import React from "react";
import Login from "./index";
describe("Login Component", () => {
test("can properly submit form", () => {
jest.spyOn(window.localStorage.__proto__, "removeItem");
const wrapper = mount(<Login />);
const emailInput = wrapper.find("#email");
// This will now complain about there being too many nodes.
emailInput.simulate("change", { target: { value: testLoginData.email } });
});
});
What gives? I need to use mount for the test that I'm working on, why is it finding multiple elements when there is for sure ONLY ONE.
I can patch it to work using the following, but I shouldn't have to... right?!
emailInput.at(0).simulate("change", { target: { value: testLoginData.email } });
So it's because your <FormControl is the first with this id and <input is second(or vice versa).
There are wide list of approaches:
.at(0) will work, but this way you will never know if you(because of error in the code) renders multiple elements. It might happen if conditions in conditional rendering {someFlag && <.... that suppose to be mutually exclusive are not. So really, it's a bad way.
Mock FormControl to be final element - so <input will not be returned anymore by .find()(honestly never used that, just assume it will work - but still looks messy and need additional boilerplate code for each test file, so not really handful way):
jest.mock('../FormControl.jsx', () => null);
use hostNodes() to filter only native elements(like <span> to be returned):
const emailInput = wrapper.find("#email").hostNodes();
I vote for 3rd option as most reliable and still safe for catching code logic's errors.

Jest How to test react form submission?

Consider the following component:
import React, { Component } from "react";
import ImageUpload from "./ImageUpload/ImageUpload";
import axios from "axios";
class Dashboard extends Component {
state = {
fileName: ""
};
onFormSubmit = e => {
e.preventDefault();
const { group, country } = e.target;
axios.post("/api/dashboard", {
name: group.value,
country: country.value,
image: this.state.fileName
});
//TODO redirect the user
};
onFileNameChange = name => {
this.setState({ fileName: name });
};
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<input type="text" name="group" placeholder="Group Name" />
<input type="text" name="country" placeholder="Country" />
<ImageUpload
fileName={this.state.fileName}
onFileNameChange={this.onFileNameChange}
/>
<button type="submit">Add Group</button>
</form>
</div>
);
}
}
export default Dashboard;
I am trying to fake the submission of a form and test that when onFormSubmit is being called. I make onFormSubmit a spy function. But, it is not being called at all.
import React from "react";
import Dashboard from "components/admin/Dashboard/Dashboard";
import { shallow, render } from "enzyme";
describe("The Dashboard component", () => {
it("should not regress", () => {
const wrapper = render(<Dashboard />);
expect(wrapper).toMatchSnapshot();
});
it("should submit the form and send the group to server", () => {
const wrapper = shallow(<Dashboard />);
const preventDefault = jest.fn();
const event = {
preventDefault,
target: {
group: "Samrat",
country: "Nepal"
}
};
wrapper.instance().onFormSubmit = jest.fn();
wrapper.update();
wrapper.find("form").simulate("submit", event);
expect(wrapper.instance().onFormSubmit).toHaveBeenCalledWith(event);
});
});

Resources