30

Consider the following input element in a React component:

<input onChange={() => console.log('onChange')} ... />

While testing the React component, I'm emulating user changing the input value:

input.value = newValue;
TestUtils.Simulate.change(input);

This causes 'onChange' to be logged, as expected.

However, when the 'change' event is dispatched directly (I'm using jsdom):

input.value = newValue;
input.dispatchEvent(new Event('change'));

the onChange handler is not called.

Why?

My motivation to use dispatchEvent rather than TestUtils.Simulate is because TestUtils.Simulate doesn't support event bubbling and my component's behavior relies on that. I wonder whether there is a way to test events without TestUtils.Simulate?

Misha Moroshko
  • 166,356
  • 226
  • 505
  • 746

4 Answers4

20

React uses its own events system with SyntheticEvents (prevents browser incompatabilities and gives react more control of events).

Using TestUtils correctly creates such a event which will trigger your onChange listener.

The dispatchEvent function on the other hand will create a "native" browser event. But the event handler you have is managed by react and therefore only reacts (badumts) to reacts SyntheticEvents.

You can read up on react events here: https://facebook.github.io/react/docs/events.html

Scarysize
  • 4,131
  • 25
  • 37
19

One way to do it without ReactTestUtils.Simulate:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://unpkg.com/react-trigger-change/dist/react-trigger-change.js';
document.head.appendChild(script);
input.value = value;
reactTriggerChange(input);

Look at the source of react-trigger-change to just cherry-pick what's needed. Example snippet:

if (nodeName === 'select' ||
    (nodeName === 'input' && type === 'file')) {
    // IE9-IE11, non-IE
    // Dispatch change.
    event = document.createEvent('HTMLEvents');
    event.initEvent('change', true, false);
    node.dispatchEvent(event);
  }
rofrol
  • 14,438
  • 7
  • 79
  • 77
  • 1
    you are my savior! – shlensky Oct 24 '17 at 12:32
  • Not enough info, here. What is `value`? If I want to click a button, should that be added to `const input` or `const input.value`? And so I tried just setting it to `const input`, and got that `reactTriggerChange is undefined`, so I added an Import statement. Then got `Cannot read property 'toLowerCase' of undefined`. Plus I have a ton of linting errors I would have to clean up to use this package :( . – vapcguy Mar 19 '18 at 23:17
  • @vapcguy added more description – rofrol Mar 20 '18 at 04:24
  • @rofrol Thanks-I pulled the last 3 lines out to use them in an event handler I made for a button to click another button. I assign `node`: `const node = document.getElementsByClassName("close");` My target button has `class="close"` & it's the only element on the page with that class. When I click my 1st button that has these lines of code in it, gives me `Uncaught TypeError: Cannot read property '__reactInternalInstance$iedoafjkjst1cmk11d5tgldi' of undefined` - similar error to `TestUtils.Simulate.click(node);` https://stackoverflow.com/questions/36752434/mocking-file-input-in-react-testutils – vapcguy Mar 20 '18 at 20:46
  • 1
    @rofrol I think my problem was in how I was getting `node`. When I assigned an ID and used `const node = document.getElementById("myID")`, I got `TestUtils.Simulate.click(node);` to finally work. I tried again using the code above that used `initEvent` and `dispatchEvent`, and it stopped giving me errors, but it didn't work, either - nothing happened. React version 15.3.1. I dunno. – vapcguy Mar 20 '18 at 21:51
  • React handles events at the document level, and that's the root cause for why your answer works. So you don't have to switch to the deprecated `initEvent` syntax - just adding `{bubbles: true}` as the second parameter of the Event constructor in the original code should work. – James Pearce Nov 24 '21 at 23:28
  • @JamesPearce Thanks. Can you edit the answer to add new approach? – rofrol Nov 25 '21 at 08:51
2

I created a small version of the https://github.com/vitalyq/react-trigger-change only for the input element.

No external dependencies, copypaste and it will work.

Works for React 16.9, checked with Safari (14), Chrome (87), and Firefox (72).

const triggerInputChange = (node: HTMLInputElement, inputValue: string) => {
      const descriptor = Object.getOwnPropertyDescriptor(node, 'value');

      node.value = `${inputValue}#`;
      if (descriptor && descriptor.configurable) {
        delete node.value;
      }
      node.value = inputValue;

      const e = document.createEvent('HTMLEvents');
      e.initEvent('change', true, false);
      node.dispatchEvent(e);

      if (descriptor) {
        Object.defineProperty(node, 'value', descriptor);
      }
};
alexlz
  • 618
  • 1
  • 10
  • 24
  • how about ``, i replaced the `value` with 'checked' but doesn't work – Heyyy Marco Jul 15 '21 at 03:09
  • This answer has a shorter version with less code: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210 – ge333 Aug 25 '22 at 19:40
1

The trick is to add { bubbles: true } to the created event. For example

target.dispatchEvent(new MouseEvent('click', { bubbles: true }))
Ivan Sanz Carasa
  • 1,141
  • 8
  • 16