jsdom: dispatchEvent/addEventListener doesn't seem to work

Jesse Buchanan picture Jesse Buchanan · Apr 22, 2016 · Viewed 17.2k times · Source

Summary:

I am attempting to test a React component that listens to native DOM events in its componentWillMount.

I'm finding that jsdom (@8.4.0) doesn't work as expected when it comes to dispatching events and adding event listeners.

The simplest bit of code I can extract:

window.addEventListener('click', () => {
  throw new Error("success")
})

const event = new Event('click')
document.dispatchEvent(event)

throw new Error('failure')

This throws "failure".


Context:

At risk of the above being an XY problem, I want to provide more context.

Here is an extracted/simplified version of the component I'm trying to test. You can see it working on Webpackbin.

import React from 'react'

export default class Example extends React.Component {
  constructor() {
    super()
    this._onDocumentClick = this._onDocumentClick.bind(this)
  }

  componentWillMount() {
    this.setState({ clicked: false })
    window.addEventListener('click', this._onDocumentClick)
  }

  _onDocumentClick() {
    const clicked = this.state.clicked || false
    this.setState({ clicked: !clicked })
  }


  render() {
    return <p>{JSON.stringify(this.state.clicked)}</p>
  }
}

Here is the test I'm trying to write.

import React from 'react'
import ReactDOM from 'react-dom'
import { mount } from 'enzyme'

import Example from '../src/example'

describe('test', () => {
  it('test', () => {
    const wrapper = mount(<Example />)

    const event = new Event('click')
    document.dispatchEvent(event)

    // at this point, I expect the component to re-render,
    // with updated state.

    expect(wrapper.text()).to.match(/true/)
  })
})

Just for completeness, here is my test_helper.js which initializes jsdom:

import { jsdom } from 'jsdom'
import chai from 'chai'

const doc = jsdom('<!doctype html><html><body></body></html>')
const win = doc.defaultView

global.document = doc
global.window = win

Object.keys(window).forEach((key) => {
  if (!(key in global)) {
    global[key] = window[key]
  }
})

Reproduction case:

I have a repro case here: https://github.com/jbinto/repro-jsdom-events-not-firing

git clone https://github.com/jbinto/repro-jsdom-events-not-firing.git cd repro-jsdom-events-not-firing npm install npm test

Answer

Louis picture Louis · Apr 23, 2016

You're dispatching the event to document so window won't see it because by default it won't bubble up. You need to create the event with bubbles set to true. Example:

var jsdom = require("jsdom");

var document = jsdom.jsdom("");
var window = document.defaultView;

window.addEventListener('click', function (ev) {
  console.log('window click', ev.target.constructor.name,
              ev.currentTarget.constructor.name);
});

document.addEventListener('click', function (ev) {
  console.log('document click', ev.target.constructor.name,
              ev.currentTarget.constructor.name);
});

console.log("not bubbling");

var event = new window.Event("click");
document.dispatchEvent(event);

console.log("bubbling");

event = new window.Event("click", {bubbles: true});
document.dispatchEvent(event);