Jimmy
Jimmy

Reputation: 3860

Testing click listener with Enzyme

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

Answers (2)

Ritesh Jagga
Ritesh Jagga

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

Matt Carlotta
Matt Carlotta

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 classNames 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.

  • ClickHandlerEvent.test.js (mimic event)
  • ClickHandlerHandleClick.test.js (mimic handleClick)
  • ClickHandler.test.js (manipulate state)

Working example (click Tests tab -- located to the right of Browser -- to run all the tests) :

Edit Click Listener Testing


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;

Option 1

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

Option 2

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

Option 3

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

Related Questions