I have the following React component:
class Form extends React.Component {
constructor(props) {
super(props);
this.state = this._createEmptyTodo();
}
render() {
this.i18n = this.context;
return (
<div className="form">
<form onSubmit={this._handleSubmit.bind(this)}>
<input
placeholder={this.i18n.placeholders.addTitle}
type="text"
value={this.state.title}
onChange={this._handleTitleChange.bind(this)}></input>
<textarea
placeholder={this.i18n.placeholders.addDescription}
value={this.state.description}
onChange={this._handleDescriptionChange.bind(this)}></textarea>
<button>{this.i18n.buttons.submit}</button>
</form>
</div>
);
}
_handleTitleChange(e) {
this.setState({
title: e.target.value
});
}
_handleDescriptionChange(e) {
this.setState({
description: e.target.value
});
}
_handleSubmit(e) {
e.preventDefault();
var todo = {
date: new Date().getTime(),
title: this.state.title.trim(),
description: this.state.description.trim(),
done: false
};
if (!todo.title) {
alert(this.i18n.errors.title);
return;
}
if (!todo.description) {
alert(this.i18n.errors.description);
return;
}
this.props.showSpinner();
this.props.actions.addTodo(todo);
this.setState(this._createEmptyTodo());
}
_createEmptyTodo() {
return {
"pkey": null,
"title": "",
"description": ""
};
}
}
And the related test:
const i18nContext = React.createContext();
Form.contextType = i18nContext;
describe('The <Form> component', () => {
var wrapper;
var showSpinner;
var actions = {}
beforeEach(() => {
showSpinner = jest.fn();
actions.addTodo = jest.fn();
wrapper = mount(<i18nContext.Provider value={i18n["en"]}>
<Form
showModalPanel={showSpinner}
actions={actions} />
</i18nContext.Provider>);
});
test("validate its input", () => {
window.alert = jest.fn();
wrapper.find("button").simulate("click");
expect(window.alert.mock.calls.length).toBe(1);//<<< this FAILS!
});
});
This form, when the button gets clicked, it simply alerts a message using alert.
Now when I run the test I get this:
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 0
Which is a failure because the mock does not get called apparently. But I promise you that the form component does alert a message when clicking on its button.
I suspect that, for some reasons, the mocked window.alert does not get used by the Form component when the click is performed programmatically using enzyme.
Anyone?
In Jest configuration with JSDOM global.window === global, so it can be mocked on window.
It's preferable to mock it like
jest.spyOn(window, 'alert').mockImplementation(() => {});
because window.alert = jest.fn() contaminates other tests in this suite.
The problem with blackbox testing is that troubleshooting is harder, also relying on the behaviour that expected from real DOM may cause problems because Enzyme doesn't necessary support this behaviour. It's unknown whether the actual problem, handleSubmit was called or not, that alert mock wasn't called is just an evidence that something went wrong.
In this case click event on a button won't cause submit event on parent form because Enzyme doesn't support that by design.
A proper unit-testing strategy is to set up spies or mocks for all units except tested one, which is submit event handler. It usually involves shallow instead of mount.
It likely should be:
jest.spyOn(window, 'alert').mockImplementation(() => {});
const formWrapper = wrapper.find(Form).dive();
jest.spyOn(formWrapper.instance(), '_handleSubmit');
formWrapper.find("form").simulate("submit");
expect(formWrapper.instance()._handleSubmit).toBeCalled();
expect(window.alert).toBeCalledWith(...);
State should be changed directly with formWrapper.setState instead of DOM events simulation.
A more isolated unit test would be to assert that form was provided with expected onSubmit prop and call formWrapper.instance()._handleSubmit(...) directly.
Instead of window, you can use global.
global.alert = jest.fn();
This is because browsers use the window name, while nodejs use the global name.
Related
I have a function called onFormSubmit in parent component. I pass this function to child component. Upon form submit in child component, onFormSubmit function inside child component is called to pass a value back to parent component. Then this onFormSubmit function does some kind of checking and based on that, updates the state in parent component.
I want to mock/stub out this ajax/api call. How can I achieve this? Or how do I write my code in such a way that this scenario is testable.
My Parent component looks like this:
class App extends React.Component {
state: { message: "" }; //I want to test the state of message
onFormSubmit = async (form) => {
if (form.primaryEmail !== "") {
const response = await axios.post("api/", form);
this.setState(
{
message: (response.status === 200) ? "Your email has been updated." : ""
});
} else {
this.setState({ message: "No Update" });
}
}
render() {
return (
<div>
<Child onSubmit={this.onFormSubmit} />
<h4>{this.state.message}</h4>
</div>
);
}
}
My Child Component looks like this:
class Child extends React.Component {
state = {
primaryEmail: "",
};
onPrimaryEmailChange = e => {
this.setState({ primaryEmail: e.target.value });
}
onFormSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state); //passing the value back to parent component
}
render() {
return (
<form onSubmit={this.onFormSubmit}>
<h3>Email Address</h3>
<div>
<input type="email" value={this.state.primaryEmail} onChange={this.onPrimaryEmailChange} />
</div>
<div>
<button type="submit">Submit</button>
</div>
</form >
);
}
}
My test looks like this:
test("When valid form is submitted, it should show a success message", () => {
const wrapper = mount(<App />);
wrapper.find("input").at(0).simulate("change", {
target: {
value: "a#b.c",
}
});
wrapper.find('form').simulate('submit');
expect(wrapper.state('message')).toEqual('Your email has been updated.');
});
I get this error:
Expected value to equal:
"Your email has been updated."
Received:
""
As it happens, I ran into a similar situation earlier this week. Here's how I solved it. There may be better solutions, but this worked in my situation.
Disclaimer: I'm writing this from memory directly into the StackOverflow answer field, so it might not be 100% accurate.
First, you should mock Axios so you can have control over the API's output for your tests. You should never actually perform an HTTP request from a test case, because you're not testing your API -- you're testing how your component responds to a particular API response. My project uses create-react-app, which configures Jest to load mocks from the __mocks__ folder in the root of the project.
__mocks__/axios.js:
export default {
// since you are only testing "axios.post()", I'm only mocking "post"
post: jest.fn()
}
Then in your parent component's test, you can specify a mock implementation for the post function that returns a 200 response (that's the case you're testing).
__tests__/App.test.jsx:
// in Jest context, this should load from __mocks__/axios.js
import axios from "axios";
test("When valid form is submitted, it should show a success message", () => {
// Axios itself returns a Promise, so the mock should as well
axios.post.mockImplementationOnce(
(url, formData) => Promise.resolve({
status: 200
})
);
const wrapper = mount(<App />);
// Optionally, test the default state to be an empty string
expect(wrapper.state()).toHaveProperty("message");
expect(wrapper.state().message).toBe("");
wrapper.find("input").at(0).simulate("change", {
target: {
value: "a#b.c",
}
});
wrapper.find("form").simulate("submit");
// Optionally, test if Axios was called
expect(axios.post).toHaveBeenCalled();
// More optionally, test if it was called with the correct email address
expect(axios.post).toHaveBeenCalledWith(
expect.any(),
expect.objectContaining({ primaryEmail: "a#b.c" })
);
// Note that even though Axios may have been called, the asynchronous
// Promise may not have completed yet which means the state will not
// have been updated yet. To be safe, let's use setImmediate(), which
// should wait for any pending Promises to complete first
setImmediate(async () => {
// Now we can test that the state has changed
expect(wrapper.state().message).toBe("Your email has been updated.");
});
});
I have a component that loads another component, sending it an anonymous function as a prop:
export class Header extends Component {
constructor(props) {
super(props)
this.state = { activeTab: TAB_NAMES.NEEDS_REVIEW }
}
filterByNeedsReview() {
const { filterByNeedsReviewFn } = this.props
this.setState({ activeTab: TAB_NAMES.NEEDS_REVIEW })
filterByNeedsReviewFn()
}
...
render() {
return (
<Container>
...
...
<FilterTab
active={this.state.activeTab === TAB_NAMES.NEEDS_REVIEW}
name={TAB_NAMES.NEEDS_REVIEW}
count={40}
onClick={() => this.filterByNeedsReview()}
/>
...
...
</Container>
)
}
}
I have this failing test:
it('renders a filter tab with the right props for needs review', () => {
const filterByNeedsReviewFn = jest.fn()
expect(
shallowRender({ filterByNeedsReviewFn })
.find(FilterTab)
.findWhere(node =>
_.isMatch(node.props(), {
active: true,
name: 'Needs Review',
count: 40,
onClick: filterByNeedsReviewFn, //<-------------- THIS DOESN'T WORK
})
)
).toHaveLength(1)
})
How would I test that onClick is the right thing?
I believe you don't need to check how internal event handlers look like. You might be interested in different things: if triggering event handler changes component as you expect(.toMatchSnapshot() is much better here instead of testing structure manually with .toHaveLength) and if callback you've passed through props is called when it should to(.toHaveBeenCalled). What if component is changed some day not to just call .props.filterByNeedsReviewFn() but also do some stuff like calling anything else? should your test fail just because there is named method passed somewhere inside? I believe it is not.
So I see your test to be
it('renders a filter tab with expected props after clicking', () => {
const comp = shallowRender({});
comp.find(FilterTab).simulate('click');
expect(comp).toMatchSnapshot();
});
it('calls callback passed after clicking on filter tab', () => {
const filterByNeedsReviewFn = jest.fn()
const comp = shallowRender({ filterByNeedsReviewFn });
comp.find(FilterTab).simulate('click');
// let's ensure callback has been called without any unexpected arguments
expect(filterByNeedsReviewFn ).toHaveBeenCalledWith();
});
I don't think you actually needed this code but I wanted to illustrate how clear such approach could be. Your component have API: props callback it calls and render output. So we can skip testing internals without any pitfalls
I am trying to test one of the methods in my react component. It is being called after a button click so I have the simulation in place with enzyme
it('clone should call handleCloneClick when clicked', () => {
const cloneButton = wrapper.find('#clone-btn');
cloneButton.simulate('click');
});
My component method is here:
_handleCloneClick(event) {
event.preventDefault();
event.stopPropagation();
this.props.handleClone(this.props.user.id);
}
The _handleCloneClick is being called when the user clicks on the button thats in the simulation, how can I go about testing that its been called successfully?
There are two options, either you should spy on _handleCloneClick of component's prototype, before you render the component:
export default class cloneButton extends Component {
constructor(...args) {
super(...args);
this. _handleCloneClick = this. _handleCloneClick.bind(this);
}
_handleCloneClick() {
event.preventDefault();
event.stopPropagation();
this.props.handleClone(this.props.user.id);
}
render() {
return (<button onClick={this. _handleCloneClick}>Clone</button>);
}
}
And in your test:
it('clone should call handleCloneClick when clicked', () => {
sinon.spy(cloneButton.prototype, '_handleCloneClick');
const wrapper = mount(<cloneButton/>);
wrapper.find('#clone-btn').simulate('click');
expect(spy).toHaveBeenCalled() //adept assertion to the tool you use
});
Or, you can try to set a spy after rendering the component and invoke wrapper.update() afterwards:
it('clone should call handleCloneClick when clicked', () => {
const wrapper = mount(<cloneButton/>);
sinon.spy(wrapper.instance(), "_handleCloneClick");
wrapper.update();
wrapper.find('#clone-btn').simulate('click');
expect(spy).toHaveBeenCalled() //adept assertion to the tool you use
});
I tried to use enzyme to simulate change event on a checkbox, and use chai-enzyme to assert if it's been checked.
This is my Hello react component:
import React from 'react';
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {
checked: false
}
}
render() {
const {checked} = this.state;
return <div>
<input type="checkbox" defaultChecked={checked} onChange={this._toggle.bind(this)}/>
{
checked ? "checked" : "not checked"
}
</div>
}
_toggle() {
const {onToggle} = this.props;
this.setState({checked: !this.state.checked});
onToggle();
}
}
export default Hello;
And my test:
import React from "react";
import Hello from "../src/hello.jsx";
import chai from "chai";
import {mount} from "enzyme";
import chaiEnzyme from "chai-enzyme";
import jsdomGlobal from "jsdom-global";
import spies from 'chai-spies';
function myAwesomeDebug(wrapper) {
let html = wrapper.html();
console.log(html);
return html
}
jsdomGlobal();
chai.should();
chai.use(spies);
chai.use(chaiEnzyme(myAwesomeDebug));
describe('<Hello />', () => {
it('checks the checkbox', () => {
const onToggle = chai.spy();
const wrapper = mount(<Hello onToggle={onToggle}/>);
var checkbox = wrapper.find('input');
checkbox.should.not.be.checked();
checkbox.simulate('change', {target: {checked: true}});
onToggle.should.have.been.called.once();
console.log(checkbox.get(0).checked);
checkbox.should.be.checked();
});
});
When I run this test, the checkbox.get(0).checked is false, and the assertion checkbox.should.be.checked() reports error:
AssertionError: expected the node in <Hello /> to be checked <input type="checkbox" checked="checked">
You can see the message is quite strange since there is already checked="checked" in the output.
I'm not sure where is wrong, since it involves too many things.
You can also see a demo project here: https://github.com/js-demos/react-enzyme-simulate-checkbox-events-demo, notice these lines
I think some of the details of my explanation might be a bit wrong, but my understanding is:
When you do
var checkbox = wrapper.find('input');
It saves a reference to that Enzyme node in checkbox, but there are times that when the Enzyme tree gets updated, but checkbox does not. I don't know if this is because the reference in the tree changes and therefore the checkbox is now a reference to a node in an old version of the tree.
Making checkbox a function seems to make it work for me, because now the value of checkbox() is always taken from the most up to date tree.
var checkbox = () => wrapper.find('input');
checkbox().should.not.be.checked();
checkbox().simulate('change', {target: {checked: true}});
///...
It is not bug, but "it works as designed".
Enzyme underlying uses the react test utils to interact with react, especially with the simulate api.
Simulate doesn't actually update the dom, it merely triggers react event handlers attached to the component, possibly with the additional parameters you pass in.
According to the answer I got here (https://github.com/facebook/react/issues/4950 ) this is because updating the dom would require React to reimplement a lot of the browsers functionality, probably still resulting in unforeseen behaviours, so they decided to simply rely on the browser to do the update.
The only way to actually test this is to manually update the dom yourself and then call the simulate api.
Below solution best worked for me:
it('should check checkbox handleClick event on Child component under Parent', () => {
const handleClick = jest.fn();
const wrapper = mount(
<Parent onChange={handleClick} {...dependencies}/>,); // dependencies, if any
checked = false;
wrapper.setProps({ checked: false });
const viewChildren = wrapper.find(Children);
const checkbox = viewChildren.find('input[type="checkbox"]').first(); // If you've multiple checkbox nodes and want to select first
checkbox.simulate('change', { target: { checked: true } });
expect(handleClick).toHaveBeenCalled();
});
Hope this helps.
This is what worked for me:
wrapper.find(CCToggle)
.find('input[type="checkbox"]')
.simulate('change', { target: { checked: true } })
CCToggle is my component.
I have a component that uses contentEditable as an input method. The part from the component that is of interest is:
<div className="enter-edit-mode" onClick={view.enterEditMode}>
<div className="user-input" ref="content" contentEditable onInput={view.textChanged}></div>
</div>
The component works fine - it gets into the textChanged method on user input. The method looks like this:
textChanged: function (e) {
var view = this,
textValue = e.target.innerHTML;
view.setState({
enteringText: textValue.length,
temporaryValue: textValue
});
}
The problem I'm facing appears when I try to test the input behavior. The setup is done with enzyme, chai, sinon. I'm rendering the component using a simple renderComponent function, using enzyme's mount method.
beforeEach(function () {
view = renderComponent(card);
viewReact = view.get(0);
});
it('should enter text changed method on input', function () {
let spy = sinon.spy(viewReact, 'textChanged');
view.find('.user-input').simulate('input');
expect(spy).to.have.been.called;
spy.restore();
});
It outputs expected textChanged to have been called at least once, but it was never called. The weird part is, however, if I put a console.log inside the component's method, it gets there.
What I've tried to make it work
use sinon.stub instead of spy, as I though that maybe something in my method doesn't work properly
call it with view.find('.user-input').simulate('input', {target: {value: "lorem"}) or .simulate('input', {key: 'a'})
If instead of simulating the input I do a viewReact.textChanged(), it obviously works.
I'm guessing that it's the input method on contentEditable that's causing this. Any suggestions? How can I properly enter text in the onInput method? (even if it gets in the textChanged method, the text is empty)
I could reproduce your issue trying to test the following component (which looks similar to yours):
const MyComponent = React.createClass({
textChanged(e) { console.log('text changed') },
render() {
return (
<div className="enter-edit-mode">
<div className="user-input" ref="content" contentEditable onInput={ this.textChanged }></div>
</div>
);
}
});
I also managed to get the test working, in a somewhat convoluted way:
it('should enter text changed method on input', () => {
let view = mount(<MyComponent/>);
let spy = sinon.spy(view.instance(), 'textChanged');
view = view.mount();
view.find('.user-input').simulate('input');
expect(spy).to.be.called;
spy.restore();
});
The view.mount() to re-mount the component seems to do the trick here.
I'm wasn't familiar with Enzyme at all (although I like it :), but it looks like it's adding various layers around components, in which case it's easy for Sinon spies to get "lost".
One possible caveat: I did all of this testing in Node using jsdom, and not in a browser.
I made a little test, I don't have sinon but I found a use case:
class Home extends Component {
state = {
status: 'default'
}
textChanged = () => {
this.setState({
status: 'changed'
})
}
render () {
return (
<div className="enter-edit-mode" >
<div className="user-input" ref="content" contentEditable onInput={this.textChanged}>Hello</div>
<span>{this.state.status}</span>
</div>
)
}
}
And the test
describe('component', () => {
it('should say changed', () => {
const component = shallow(<Home />);
expect(component.find('span').text()).toEqual('default');
component.find('.user-input').simulate('input', {key: 'a'})
expect(component.find('span').text()).toEqual('changed');
});
});
It passed all is expected