How to test functions in component with enzyme, jest, recompose, react - reactjs

Ok, so I'm a little stumped on how to test my component's functionality using enzyme/jest. Still learning how to test my components - I can write simple tests but now I need to make them more complex.
I'd like to know the best way to test that my component's functions are being called properly and that they update state props as they're supposed to. What I'm finding is tricky is that my functions and state all live in my component's props.
If I need to use a spy I'd like to preferably know how to with Jest, but if a dep like Sinon or Jasmine is better suited for the job I'm open to it (just let me know why so I can better understand).
As an example, I have a UserDetails component
const UserDetails = ({
userInfo,
onUserInfoUpdate,
className,
error,
title,
primaryBtnTitle,
submit,
secondaryBtnTitle,
secondaryBtnFunc,
...props
}) => (
<div className={className}>
<div className="user-details-body">
<Section title="User details">
<TextInput
className="firstName"
caption="First Name"
value={userInfo.first}
onChange={onUserInfoUpdate('first')}
name="first-name"
min="1"
max="30"
autoComplete="first-name"
/>
<TextInput
className="lastName"
caption="Last Name"
value={userInfo.last}
onChange={onUserInfoUpdate('last')}
name="last-name"
min="1"
max="30"
autoComplete="last-name"
/>
</Section>
</div>
<div className="errorBar">
{error && <Alert type="danger">{error}</Alert>}
</div>
<ActionBar>
<ButtonGroup>
<Button type="secondary" onClick={secondaryBtnFunc}>
{secondaryBtnTitle}
</Button>
<Button type="primary" onClick={submit}>
{primaryBtnTitle}
</Button>
</ButtonGroup>
</ActionBar>
</div>
TextInput consists of:
<label className={className}>
{Boolean(caption) && <Caption>{caption}</Caption>}
<div className="innerContainer">
<input value={value} onChange={updateValue} type={type} {...rest} />
</div>
</label>
Here is example code of my index.js file that composes my withState and withHandlers to my Component:
import UserDetails from './UserDetails'
import { withState, withHandlers, compose } from 'recompose'
export default compose(
withState('error', 'updateError', ''),
withState('userInfo', 'updateUserInfo', {
first: '',
last: '',
}),
withHandlers({
onUserInfoUpdate: ({ userInfo, updateUserInfo }) => key => e => {
e.preventDefault()
updateCardInfo({
...cardInfo,
[key]: e.target.value,
})
},
submit: ({ userInfo, submitUserInfo }) => key => e => {
e.preventDefault()
submitUserInfo(userInfo)
//submitUserInfo is a graphQL mutation
})
}
})
)
So far, my test file looks like this:
import React from 'react'
import { mount } from 'enzyme'
import UserDetails from './'
import BareUserDetails from './UserDetails'
describe('UserDetails handlers', () => {
let tree, bareTree
beforeEach(() => {
tree = mount(
<ThemeProvider theme={theme}>
<UserDetails />
</ThemeProvider>
)
bareTree = tree.find(BareUserDetails)
})
it('finds BareUserDetails props', () => {
console.log(bareTree.props())
console.log(bareTree.props().userInfo)
console.log(bareTree.find('label.firstName').find('input').props())
})
})
the console logs return me the right information in regards what I expect to see when I call on them:
//console.log(bareTree.props())
{ error: '',
updateError: [Function],
userInfo: { first: '', last: '' },
updateUserInfo: [Function],
onUserInfoUpdate: [Function] }
//console.log(bareTree.props().userInfo)
{ first: '', last: '' }
//console.log(bareTree.find('label.firstName').find('input).props()
{ value: '',
onChange: [Function],
type: 'text',
name: 'first-name',
min: '1',
max: '30',
autoComplete: 'first-name' }
Now the question is how I can use them, and the best way. Do I even use my functions or do I just check to see that onChange was called?
UPDATE (sort of)
I've tried this and I get the following:
it('Input element updates userInfo with name onChange in FirstName input', () => {
const firstNameInput = bareTree.find('label.firstName').find('input)
ccNameInput.simulate('change', {target: {value: 'John'}})
expect(ccNameInput.prop('onChange')).toHaveBeenCalled()
})
In my terminal I get:
expect(jest.fn())[.not].toHaveBeenCalled()
jest.fn() value must be a mock function or spy.
Received:
function: [Function anonymous]
If I try and create a spyOn with Jest, however, I get an error that it cannot read the function of 'undefined'.
I've tried spy = jest.spyOn(UserDetails.prototypes, 'onUpdateUserInfo') and spy = jest.spyOn(BareUserDetails.prototypes, 'onUpdateUserInfo') and they both throw the error.

I believe you should probably test the dumb component (UserDetails) and HOC separately. For the dumb component you want to render the component with shallow and inject the props. To mock the onUserInfoUpdate, you need to do const onUserInfoUpdate = jest.fn();
You want something along the lines of ....
import React from 'react'
import { shallow } from 'enzyme'
import UserDetails from './UserDetails'
const onUserInfoUpdate = jest.fn(); // spy
const props = {
onUserInfoUpdate,
// list all your other props and assign them mock values
};
describe('UserDetails', () => {
let tree;
beforeAll(() => {
tree = shallow(<UserDetails {...props} />)
});
it('should invoke the onUserInfoUpdate method', () => {
const firstNameInput = tree.find('label.firstName').find('input');
firstNameInput.simulate('change', { target: { value: 'John' } });
expect(onUserInfoUpdate).toHaveBeenCalledWith('first');
});
});

Related

Test the methods inside a functional component using react

I have the following component and I am trying to test these two methods onHandleClick and onHandleSave for checking the visible states but I am not sure how to achieve this. Can anyone help me with this?
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export const MyComponent = (props) => {
const {
visible,
setVisible,
products,
setProducts,
} = props;
const onHandleSave = (e, product) => {
e.stopPropagation();
setProducts(product)
setVisible(visible);
};
const onHandleClick = (e, product) => {
e.stopPropagation();
setProducts(product);
setVisible(!visible);
};
const onToggle = () => {
setVisible(!visible);
};
const mapProducts = () => {
if (products === undefined) {
return [];
}
return products.sort().map((product, key) => (
<Modal
className="product-option"
id="product"
key={key}
onClick={(e) => onHandleClick(e, product)}
>
{product.text}
<div className="set-default">
<span role="button" id="save-selection" tabIndex="0" onClick={(e) => onHandleSave(e, product)} aria-hidden="true" className="hovered-icon">
<Tooltip className="tooltip-content">
<small>Save</small>
</Tooltip>
</span>
</div>
</Modal>
));
};
return (
<div className="wrapper">
<button
className="products"
onClick={onToggle}
id="products"
>
{selectedItem.text}
</ button>
{visible && (
<Modal>
<div className="product-content">{mapProducts()}</div>
</Modal>
)}
</div>
);
};
export default MyComponent;
So far, I have tried to write the test like this:
import React from 'react';
import { shallow, shallowWithIntl } from 'enzyme';
import { MyComponent } from '.';
describe('<Product />', () => {
let props;
let wrapper;
beforeEach(() => {
props = {
visible: true,
setVisible: jest.fn(() => 'setVisible'),
onToggle: jest.fn(() => 'onToggleCurrency'),
onHandleClick: jest.fn(() => 'onHandleClick'),
onHandleSave: jest.fn(() => 'onHandleSave'),
};
wrapper = shallow(<MyComponent {...props} />);
});
it('check for visibility states', () => {
wrapper = shallow(<MyComponent {...props} />);
wrapper.instance().onHandleSave = jest.fn();
wrapper.instance().forceUpdate();
expect(wrapper.instance().onHandleSave).toBeCalledWith({ visible: true });
});
But no luck as the test fails. Any helps would be highly appreciated. Thanks in advance
Accessing the internal functions this way will not work, because you don't have access to the scope inside the function. These consts are technically private!
You could do it otherwise and make them accessible, but I wouldn't recommend you to do that. I'd recommend you to test from a perspective which is as close to the user's experience:
The user clicked the save -> Unit test needs to assert that the callback to setVisible and setProducts was called correctly. By accessing the mocks on props.setVisible (for example), you will be able to assert that this was called.
For further reading: https://www.stevethedev.com/blog/programming/public-private-and-protected-scope-javascript
So what you would be looking for actually is a way to click the button. Using Enzyme, you'll find plenty of resources in the net on how to do that.
Since you asked, you could make these functions accessible by setting them as class methods, adding them to the prototype, or by setting them as attributes of the component function-object:
const myFunction = () => {}
myFunction.foo = 'bar'
// you can now access myFunction.foo, as in JavaScript even a function
// is an object
To my experience you should refrain from doing so, but since you asked, here's an answer.

Passing props in a child component using jest mount

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]);

ReactWrapper can only wrap valid elements (material-ui - enzyme)

I'm writing a test for a component to test one of its functions, but I am getting an error: ShallowWrapper can only wrap valid elements
The component file is as follows - TextInput.js:
/* eslint-disable react/require-default-props */
import React from 'react'
import PropTypes from 'prop-types'
import { InputLabel, TextField } from '#material-ui/core'
const TextInput = ({
name, label, onChange, placeholder, value, error, optional = false, isDisable = false, t,
}) => (
<>
{label ? (
<InputLabel htmlFor={name} className="default-style_label">
{label}
{' '}
{optional && <span className="optional">{`(${t('application.optional')})`}</span>}
</InputLabel>
) : ''}
<TextField
type="text"
name={name}
placeholder={placeholder}
value={value}
// isRequired={isRequired}
disabled={isDisable}
onChange={onChange}
error={error && true}
helperText={error}
variant="outlined"
className="default-style_input"
/>
</>
)
TextInput.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
value: PropTypes.string,
error: PropTypes.string,
optional: PropTypes.bool,
isDisable: PropTypes.bool,
t: PropTypes.func,
}
export default TextInput
The test file
/* eslint-disable no-undef */
import React from 'react'
import { shallow, mount } from 'enzyme'
import TextInput from '../TextInput'
function createTestProps(props) {
return {
// common props
name: 'test',
label: 'foo',
value: 'bar',
onChange: jest.fn(),
// allow to override common props
...props,
}
}
describe('rendering', () => {
describe('<TextInput>', () => {
let wrapper
let instance
beforeEach(() => {
const props = createTestProps()
wrapper = mount(shallow(<TextInput {...props} />)).get(0)
instance = wrapper.instance()
})
afterEach(() => {
jest.clearAllMocks()
})
it('should be rendered', () => {
const content = wrapper.find('input').at(1)
console.debug(content.debug())
console.log(instance)
expect(content.value).toBe('bar')
})
})
})
The problem is that my tests fail when remove mount from
wrapper = mount(shallow(<TextInput {...props} />)).get(0)
with a Compared values have no visual difference
Any ideas why this is happening would be much appreciated!
You should use either mount or shallow to render your component based on your use case.
use mount if you want to render your component where all children would be rendered to the last leaf node.
use shallow if you need a level deep rendering of your component.
Note: in most cases, you should use shallow rendering as mount takes a long time.
You should use shallow or mount, not both of them.
wrapper = mount(<TextInput {...props} />);
const content = wrapper.find('input');
expect(content.value).toBe('bar');
It's something different when you're using #material-ui.
You've to use #material-ui's Built-in API(s). Such as createMount, createShallow, createRender in order to use enzyme's shallow, mount & render.
These APIs are built on top of enzyme, so you can't use enzyme directly for testing #material-ui.
Here's my test file
/* eslint-disable no-undef */
import React from 'react'
import { createMount } from '#material-ui/core/test-utils'
import TextField from '#material-ui/core/TextField'
import TextInput from '../TextInput'
function createTestProps(props) {
return {
// common props
name: 'test',
label: 'foo',
value: 'bar',
onChange: jest.fn(),
// allow to override common props
...props,
}
}
describe('rendering', () => {
describe('<TextInput>', () => {
let mount
let props
beforeEach(() => {
mount = createMount()
props = createTestProps()
})
afterEach(() => {
mount.cleanUp()
})
it('renders a <TextField/> component with expected props', () => {
const wrapper = mount(<TextInput {...props} />)
expect(wrapper.props().name).toEqual('test')
expect(wrapper.props().onChange).toBeDefined()
})
it('should trigger onChange on <TextField/> on key press', () => {
const wrapper = mount(<TextInput {...props} />)
wrapper.find('input').simulate('change')
expect(props.onChange).toHaveBeenCalled()
})
})
})
I found the solution here
Material UI + Enzyme testing component

Jest TypeError: e.preventDefault is not a function

I'm trying to test the onSubmit method, however im getting this error.
TypeError: e.preventDefault is not a function
I'm referencing this blog tutorial
https://medium.com/#aghh1504/6-testing-react-components-using-jest-and-enzyme-b85db96fa1e3
i referenced another question but no answer
e.preventDefault is not a function - Jest React
App.test.tsx
describe('Should test onSubmit method',() => {
it('should test onSubmit method', ()=>{
const component = shallow(<App/>)
const preventDefault = jest.fn();
const items = ['Learn react', 'rest', 'go out'];
component.setState({
currentTask:"test-task",
tasks:[...items, 'test-task']
})
component.find('form').simulate('submit', preventDefault);
expect(preventDefault).toBeCalled();
})
})
App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
// we need an interface to be able to use the state properties, if not error.
// the same rule applies when using props
// so here we call Istate
interface IState {
currentTask: string;
tasks: Array<string>;
}
export default class App extends React.Component<{}, IState>{
constructor(props: {}){
super(props);
this.state = {
currentTask: "",
tasks:[]
}
}
// when using e.preventDefault in typescript, or any paramater, it has to be followed
// by the following any, array, string, etc. in this case we use any
handleSubmit(e: any){
e.preventDefault();
this.setState({
currentTask: "",
tasks: [...this.state.tasks, this.state.currentTask]
}, () => {
console.log(this.state.tasks)
})
}
onChange = (e: any) => {
e.preventDefault();
this.setState({
currentTask: e.target.value
})
}
render(){
return (
<div className="App">
<h1 className="sampleh1">React Typescript Todo</h1>
<form onSubmit={(e) => this.handleSubmit(e)}>
<input value={this.state.currentTask} type="text" placeholder="enter a todo"
onChange={this.onChange}/>
<button type="submit"> Add Todo</button>
</form>
</div>
);
}
}
Try passing mockEvent object as second argument.
component.find('form').simulate('submit', { preventDefault });
If you using Jest
form.simulate(`submit`, {preventDefault: jest.fn()});
or just an empty function if you don't
form.simulate(`submit`, {preventDefault: () => {}});
The second argument to simulate is the mock event that gets passed to the handler when you call onSubmit, so it has to be in the form of the event object that handler expects:
component.find('form').simulate('submit', { preventDefault });
Likewise, if you're going to test your onChange function, you'll need to pass a mock event with a target/value as the 2nd argument to simulate:
component.find('input').simulate('change', { target: { value: 'todo' } });

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
});
});

Resources