2

I am building an app with React. I am hiding a file input element (<input type="file"/>) "behind" a react-bootstrap Button to be able to control styling. So, when the button is clicked I turn around and fire a synthetic click event on the text input element, as shown below.

class OpenFileButton extends React.Component {
  ...
  clickHandler() {
    this.refs['input'].click();
  }

  render() {
    return (
      <ButtonGroup>
        <div>
          <input type="file" onChange={this.props.someCallback}
            ref="input" style={{display: 'none'}}/>
          <Button onClick={this.clickHandler}>Open File</Button>
        </div>
      </ButtonGroup>
    );
  }
}

I want to be able to test this with Jest/Enzyme. However, while I can simulate a click event on the button, I haven't figured out how to detect a synthetic click event on the file input element.

I have tried using Jest/Enzyme to mock the click method on the input element.

const component = mount(<OpenFileButton/>);
const fileInput = component.find('input');
const button    = component.find('Button');
fileInput.click = jest.fn();
button.simulate('click');
expect(fileInput.click).toHaveBeenCalled();

However, mocking the click method this way does not work. I also can't add an onClick attribute, i.e. fileInput.props().onClick = jest.fn() does not work.

This question is about detecting synthetic click events in the code itself, not in the test code, and so is not relevant.

So, how can I detect a (synthetic) click event on a DOM element using Jest/Enzyme?

Community
  • 1
  • 1
Andrew Willems
  • 11,880
  • 10
  • 53
  • 70

2 Answers2

3

<input /> or this.refs.input is an instance of HTMLInputElement.

Then you can test if HTMLInputElement.prototype.click is called .

Using you will have :

import sinon from 'sinon';
import {mount} from 'enzyme';

const clickInputSpy = sinon.spy(HTMLInputElement.prototype, 'click')
const component = mount(<OpenFileButton/>);

const button    = component.find('Button');

button.simulate('click');
expect(clickInputSpy.called).toBeTruthy();
clickInputSpy.restore();

const clickInputSpy = sinon.spy(HTMLInputElement.prototype, 'click');


console.log(
 'Is <input /> clicked ? ', clickInputSpy.called 
);
document.querySelector('input').click();


console.log(
 'Is <input /> clicked ? ', clickInputSpy.called 
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>


<script src="https://cdnjs.cloudflare.com/ajax/libs/sinon.js/1.15.4/sinon.min.js"></script>

<input />
Abdennour TOUMI
  • 87,526
  • 38
  • 249
  • 254
  • That is brilliant! Thank you. Works perfectly. Just to directly answer my question, in order to do the same with Jasmine/Jest instead of Sinon, do the following: (A) Replace your `sinon.spy(...` with `spyOn(...`. (B) Change the `expect` statement to `expect(clickInputSpy).toHaveBeenCalled()` (or something similar). (C) If you need to restore the spy within the same `it` or `describe` block within which the spy was defined, use `clickInputSpy.and.callThrough()` as suggested [here](http://stackoverflow.com/a/23723064/5218951). – Andrew Willems Mar 04 '17 at 22:40
  • However, there is a problem, though not necessarily completely insurmountable. The spy spies on _any/all_ input element(s), not the specific one that I am interested in. In many cases there probably won't be multiple different `input` elements that need to be differentiated, so that might be OK. However, it _does_ make the test somewhat non-optimal. It also makes this strategy completely unworkable if you ever want to "transfer" a click between two elements of the same type, e.g. clicking on one button which then programmatically clicks another button. – Andrew Willems Mar 04 '17 at 23:46
  • So I don't know if it is possible to spy on the instance directly . If so, use `spy(component.ref('input'), 'click')` instead of `spy(HTMLInputElement.prototype, 'click')`. ..It might work or `spy(component.find('input').get(0), 'click')` – Abdennour TOUMI Mar 05 '17 at 02:45
  • Thanks for the ideas. I had already used your last suggestion, i.e. `spy(...find...get..., 'click')`, in the alternative answer that I posted and it seems to be working. In any case, thanks again for originally sending me off in the right direction with your initial answer. – Andrew Willems Mar 05 '17 at 12:27
2

The solution here involves spying on the click method of the particular file input element that I'm interested in. I can thus check to see if this file-input-element-click-spy was called after a click is simulated on the button element, as follows:

const openFileButtonWrapper = mount(<OpenFileButton/>);
const buttonWrapper = openFileButtonWrapper.find(Button);
const fileInputWrapper = openFileButtonWrapper.find('input [type="file"]');
const fileInput = fileInputWrapper.get(0);
const clickInputSpy = spyOn(fileInput, 'click');
buttonWrapper.simulate('click');
expect(clickInputSpy).toHaveBeenCalled();

The answer by @AbdennourTOUMI used Sinon's spy method which reminded me that Jest uses some Jasmine functionality, including its spyOn method, that isn't obvious from the Jest documentation. So, even though that other answer ended up spying on _all_ input elements, which is not ideal, it did send me in the right direction, so thank you, Adbennour.

Andrew Willems
  • 11,880
  • 10
  • 53
  • 70