enzyme: window.addEventListener not triggered by simulated events

The first component I tried to test using this was a mixin for detecting clicks outside a component. In order to do this one needs to listen with window.addEventListener('click'). This handler doesn’t appear to be triggered when using enzyme simulated clicks.

If handling this is out of scope for enzyme, do you have a recommendation on the best way to get this under test?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 21 (4 by maintainers)

Commits related to this issue

Most upvoted comments

Generally any test framework(jest, mocha, etc) can solve your problem natively. Your goal here is effectively to make sure the event is bound, and that when its fired something happens in your component. So you’ll have to do some setup prior to rendering, but it is definitely possible to test this code without using context.

To be clear, @aweary is spot on in saying this is not enzyme supported.

For example in jest this is a sort of code you could use.

const map = {};
Window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
  map[event] = cb;
});

// render component

map.event(...args);

// assert changes

just as a small update and FYI, for document this is working for me, and on a newer version of jest:

const map = {};
    document.addEventListener = jest.fn((event, cb) => {
      map[event] = cb;
    })

This also works without having to mock window.addEventListener, due to all the reasons mentioned above. Test framework: jest + enzyme.

    test('should close when user clicks outside it', () => {
        const outerNode = document.createElement('div');
        document.body.appendChild(outerNode);
        
        const onClose = jest.fn();
        const wrapper = mount(<Toast onClose={ onClose } />, { attachTo: outerNode });
        const toast = wrapper.find(`[data-test-id="toast"]`); // getDOMNode() could also work

        toast.instance().dispatchEvent(new Event('click'));
        expect(onClose).not.toHaveBeenCalled();

        outerNode.dispatchEvent(new Event('click'));
        expect(onClose).toHaveBeenCalled();
    });

A little further explanation: wrapper.find().instance() returns a DOM element (whereas wrapper.instance() would just return the Toast class)–this gives us access to EventTarget.dispatchEvent(), which you can use to dispatch non-synthetic events, rather than mocking out window.addEventListener. And by adding another div to the document.body, then attaching the mounted wrapper, you ensure the real element event will bubble up to the actual window. (Assumption is that you have a click listener on the window.) Note that I also tried attaching directly to the document.body, as in the first comment, but React then throws “Rendering components directly into document.body is discouraged.”

@blainekasten you saved my day 😃

const map = {};
window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
  map[event] = cb;
});

const component = mount(<SomeComponent />);
map.mousemove({ pageX: 100, pageY: 100});

This worked for me, the state of the component is successfully being updated.

Ahh, so mount doesn’t actually attach the rendered fragment to the document, so even if simulated events did bubble up through the concrete DOM, they wouldn’t reach window anyway.

I found a workaround:

  • Attach the mounted component to document.body
  • Use simulant to fire “real” DOM events.

e.g.

  it('only triggers clickOutside handler when clicking outside component', t => {
    const onClickOutside = sinon.spy()
    mount(<Page onClickOutside={onClickOutside} />, { attachTo: document.body })
    simulant.fire(document.body.querySelector('aside'), 'click')
    t.equal(onClickOutside.callCount, 1, 'should fire when clicking menu sibling')
    document.body.innerHTML = ''
    t.end()
  })

However, I’ve just noticed that in this particular example enzyme isn’t actually doing anything haha.

We can just ReactDOM.render into our JSDOM document.body directly:

  it('only triggers clickOutside handler when clicking outside component', t => {
    const onClickOutside = sinon.spy()
    ReactDOM.render(<Page onClickOutside={onClickOutside} />, document.body)
    simulant.fire(document.body.querySelector('aside'), 'click')
    t.equal(onClickOutside.callCount, 1, 'should fire when clicking menu sibling')
    document.body.innerHTML = ''
    t.end()
  })

Curious now as to why not use just always use this method? why enzyme?

@prasadmsvs please file a new issue rather than commenting on a new one; but for this kind of question, the gitter channel linked in the readme is preferred.

@Faline10, that’s awesome! As a note to others, I had to do .dispatchEvent(new Event('click', { bubbles: true})) to make it bubble to the window

Can a core contributor respond to this? I’m having the same issue now where instead of attaching a event on a direct element, I’m attaching it on the document so that I can easily detect a click outside of the said element. But I’m having hard time testing this behavior with enzyme.

@Aweary @lelandrichardson

Inspired by @kellyrmilligan’s solution here’s full implementation I use to detect ESC keydown (also useful for any other event type):

it('calls the dismiss callback on ESC key', () => {
  const KEYBOARD_ESCAPE_CODE = 27;
  const mockDismissCallback = jest.fn();

  // Declaring keydown prop so the linter doesn't compain
  const eventMap = {
    keydown: null,
  };

  document.addEventListener = jest.fn((event, cb) => {
    eventMap[event] = cb;
  });

  // MyModalComponent internally uses
  // document.addEventListener('keydown', this.onModalDialogKeyDown, false);
  // which then via onModalDialogKeyDown binding does some stuff and then calls onDismiss which
  // is really mockDismissCallback
  const modal = shallow(
    <MyModalComponent isOpen={true} onDismiss={mockDismissCallback}>
      Test
    </MyModalComponent>
  );

  eventMap.keydown({ keyCode: KEYBOARD_ESCAPE_CODE });

  expect(mockDismissCallback.mock.calls.length).toEqual(1);
});

@Faline10, that’s awesome! As a note to others, I had to do .dispatchEvent(new Event('click', { bubbles: true})) to make it bubble to the window

ev = new Event('click');

I am getting Event is undefined.

@tleunen I doubt this is a use case we’d support, enzyme is meant to test React components and attaching an event listener to the document with addEventListener means the event is not being handled by React’s synthetic event system. Our simulate method for mount is a thin wrapper around ReactTestUtils.Simulate, which only deals with React’s synthetic event system.

I can’t speak to your specific use case, but I would advise that this is generally an anti-pattern in React and should be handled within React’s event system when possible (such as passing down an onClick prop from a stateful parent and calling it in the leaf component’s onClick handler). You can try workaround like @timoxley but your mileage may vary.