Reputation: 3860
I have a stateful component that attaches a dom event listener on mount. If a user clicks a given element, then another given element will conditionally appear and disappear. I want to write a test for this, but when I do so using enzyme, I am getting an error:
sampleComponent.js:
import React from 'react';
class SampleComponent extends React.Component {
constructor() {
super();
this.state = {
onClick: false,
};
this.handleClick = this.handleClick.bind(this);
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClick);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClick);
}
handleClick(event) {
if (this.divRef && this.divRef.contains(event.target)) {
this.setState(prevState => ({ onClick: !prevState.onClick }));
}
}
render() {
return (
<div
ref={(node) => { this.divRef = node; }}
test-attr="div"
>
{
this.state.onClick && <p test-attr="p">clicked!</p>
}
</div>
);
}
}
export default SampleComponent;
sampleComponent.test.js:
import React from 'react';
import { shallow } from 'enzyme';
import SampleComponent from './sampleComponent';
test('renders component without errors', () => {
const wrapper = shallow(<SampleComponent />);
const div = wrapper.find('[test-attr="div"]');
const p = wrapper.find('[test-attr="p"]');
div.simulate('click');
expect(p.length).toEqual(1);
});
Error:
Error:
expect(received).toEqual(expected)
Expected value to equal:
1
Received:
0
Expected :1
Actual :0
Why is my click simulation not properly updating my component state? Thanks!
Upvotes: 0
Views: 2387
Reputation: 1442
If enzyme only simulates those events which are added on the props as in on*
syntax e.g. onClick
, onChange
and does not simulate the events added using addEventListener
then I think one can do the following in a Javascript way:
const div = wrapper.find('[test-attr="div"]');
const divNode = div.getDOMNode();
divNode.dispatchEvent(new Event('mousedown'))
I've not tested it for the code in question but I've been doing this for similar use cases and hopeful that it should work.
I think this works for the mounted components (Using mount
).
Upvotes: 1
Reputation: 19762
By design, enzyme doesn't support event listeners since they're a Javascript implementation and not a React implementation. So, you'll have to do some Javascript and jest trickery in order to mimic the event listener.
In this case, you really don't need to test the event handler since you're just manipulating state. Bypassing the event listener, you can manually manipulate the onClick
class property and make assertions against how the state and DOM change accordingly -- that would be a more React-centric test. However, even this makes it a bit difficult because onClick
expects a real DOM node. So, an even simpler approach would be to just manipulate the state directly with wrapper.setState({ ... })
and make assertions against the DOM changes.
On a side note, I prefer using className
s over data-attributes
as they're more useful for styling and testing and they don't pollute the DOM with a lot of unnecessary and/or unused properties.
The below example covers all 3 options.
Working example (click Tests
tab -- located to the right of Browser
-- to run all the tests) :
components/ClickHandler/index.js (the component)
import React, { Fragment, Component } from "react";
import ClickBox from "../ClickBox";
class ClickHandler extends Component {
state = {
isVisible: false
};
componentDidMount = () => {
document.addEventListener("mousedown", this.handleClick);
};
componentWillUnmount = () => {
document.removeEventListener("mousedown", this.handleClick);
};
handleClick = ({ target }) => {
this.setState({
isVisible: this.wrapperRef && this.wrapperRef.contains(target)
});
};
render = () => {
const { isVisible } = this.state;
return (
<div className="wrapper" ref={node => (this.wrapperRef = node)}>
<ClickBox>
<p className="instruction">
(click <strong>{isVisible ? "outside" : "inside"}</strong> the box
to <strong>{isVisible ? "hide" : "show"}</strong> the message)
</p>
<h2 className="message">
{isVisible ? (
<Fragment>
Hello <strong>World</strong>!
</Fragment>
) : null}
</h2>
</ClickBox>
</div>
);
};
}
export default ClickHandler;
components/ClickHandler/__tests__/ClickHandlerEvent.test.js (mimic event)
import React, { Fragment } from "react";
import { mount } from "enzyme";
import ClickHandler from "../index";
const initialState = {
isVisible: false
};
// elevating the event listener to the test
const eventListener = {};
document.addEventListener = (evt, cb) => (eventListener[evt] = cb);
describe("Click Handler", () => {
let wrapper;
beforeAll(() => {
wrapper = mount(
<Fragment>
<ClickHandler />
<div className="outside" />
</Fragment>
);
wrapper.setState({ ...initialState });
});
afterAll(() => {
wrapper.unmount();
});
it("renders without errors and the message should be hidden", () => {
expect(wrapper.find("div.wrapper")).toHaveLength(1);
expect(wrapper.find("h2.message").text()).toEqual("");
});
it("displays a message when a click is inside of the box", () => {
// manually triggering the event listener with a node
// inside of "ClickHandler"
eventListener.mousedown({
target: wrapper
.find("ClickHandler")
.getDOMNode()
.getElementsByClassName("instruction")[0]
});
expect(wrapper.find("ClickHandler").state("isVisible")).toBeTruthy();
expect(wrapper.find("h2.message").text()).toEqual("Hello World!");
});
it("hides the message when the click is outside of the box", () => {
// manually triggering the event listener with a node
// outside of "ClickHandler"
eventListener.mousedown({
target: wrapper.find("div.outside").getDOMNode()
});
expect(wrapper.find("ClickHandler").state("isVisible")).toBeFalsy();
expect(wrapper.find("h2.message").text()).toEqual("");
});
});
components/ClickHandler/__tests__/ClickHandlerHandleClick.test.js (mimic handleClick)
import React, { Fragment } from "react";
import { mount } from "enzyme";
import ClickHandler from "../index";
const initialState = {
isVisible: false
};
describe("Click Handler", () => {
let wrapper;
beforeAll(() => {
wrapper = mount(
<Fragment>
<ClickHandler />
<div className="outside" />
</Fragment>
);
wrapper.setState({ ...initialState });
});
afterAll(() => {
wrapper.unmount();
});
it("renders without errors and the message should be hidden", () => {
expect(wrapper.find("div.wrapper")).toHaveLength(1);
expect(wrapper.find("h2.message").text()).toEqual("");
});
it("displays a message when a click is inside of the box", () => {
// manually triggering the handleClick class property with a
// node inside of "ClickHandler"
wrapper
.find("ClickHandler")
.instance()
.handleClick({
target: wrapper
.find("ClickHandler")
.getDOMNode()
.getElementsByClassName("instruction")[0]
});
expect(wrapper.find("ClickHandler").state("isVisible")).toBeTruthy();
expect(wrapper.find("h2.message").text()).toEqual("Hello World!");
});
it("hides the message when the click is outside of the box", () => {
// manually triggering the handleClick class property with a
// node outside of "ClickHandler"
wrapper
.find("ClickHandler")
.instance()
.handleClick({
target: wrapper.find("div.outside").getDOMNode()
});
expect(wrapper.find("ClickHandler").state("isVisible")).toBeFalsy();
expect(wrapper.find("h2.message").text()).toEqual("");
});
});
components/ClickHandler/__tests__/ClickHandler.test.js (manipulate state)
import React from "react";
import { mount } from "enzyme";
import ClickHandler from "../index";
const initialState = {
isVisible: false
};
describe("Click Handler", () => {
let wrapper;
beforeAll(() => {
wrapper = mount(<ClickHandler />);
wrapper.setState({ ...initialState });
});
afterAll(() => {
wrapper.unmount();
});
it("renders without errors and the message should be hidden", () => {
expect(wrapper.find("div.wrapper")).toHaveLength(1);
expect(wrapper.find("h2.message").text()).toEqual("");
});
it("displays a message when a click is inside of the box", () => {
// manually manipulating state
wrapper.setState({ isVisible: true });
expect(wrapper.find("h2.message").text()).toEqual("Hello World!");
});
it("hides the message when the click is outside of the box", () => {
// manually manipulating state
wrapper.setState({ isVisible: false });
expect(wrapper.find("h2.message").text()).toEqual("");
});
});
Upvotes: 1