2

I have a component that uses contentEditable as an input method. The part from the component that is of interest is:

<div className="enter-edit-mode" onClick={view.enterEditMode}>
    <div className="user-input" ref="content" contentEditable onInput={view.textChanged}></div>
</div>

The component works fine - it gets into the textChanged method on user input. The method looks like this:

textChanged: function (e) {
    var view      = this,
        textValue = e.target.innerHTML;

    view.setState({
        enteringText: textValue.length,
        temporaryValue: textValue
    });
}

The problem I'm facing appears when I try to test the input behavior. The setup is done with enzyme, chai, sinon. I'm rendering the component using a simple renderComponent function, using enzyme's mount method.

beforeEach(function () {
    view = renderComponent(card);
    viewReact = view.get(0);
});

it('should enter text changed method on input', function () {
    let spy = sinon.spy(viewReact, 'textChanged');
    view.find('.user-input').simulate('input');
    expect(spy).to.have.been.called;
    spy.restore();
});

It outputs expected textChanged to have been called at least once, but it was never called. The weird part is, however, if I put a console.log inside the component's method, it gets there.

What I've tried to make it work

  • use sinon.stub instead of spy, as I though that maybe something in my method doesn't work properly
  • call it with view.find('.user-input').simulate('input', {target: {value: "lorem"}) or .simulate('input', {key: 'a'})

If instead of simulating the input I do a viewReact.textChanged(), it obviously works.

I'm guessing that it's the input method on contentEditable that's causing this. Any suggestions? How can I properly enter text in the onInput method? (even if it gets in the textChanged method, the text is empty)

Raul Rene
  • 10,014
  • 9
  • 53
  • 75
  • Hmmm, is this solution would resolve your issue? An another SO [link](http://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable) – Michael Rasoahaingo Jun 02 '16 at 15:08
  • @MichaelRasoahaingo: I've looked into it, and the first problem is that using that means some refactoring of my component and usages. I've given it a try, and I still get the same outcome. I'm also trying right now to directly simulate the `change` event on the `` component and still the same, it enters my method but the test crashes. – Raul Rene Jun 02 '16 at 15:29
  • Strange are you sure you have found the ('.user-input') ? – Michael Rasoahaingo Jun 02 '16 at 15:48
  • @MichaelRasoahaingo Yes. I've tried doing a typo with the className and it crashes saying that the element was not found – Raul Rene Jun 02 '16 at 15:51
  • @MichaelRasoahaingo I have though before about using `onChange` instead of `onInput` , because I use it on textareas and it works fine and I've managed to also test it. If nothing else shows up here I will do a refactor and try to use onChange – Raul Rene Jun 02 '16 at 15:53

2 Answers2

1

I could reproduce your issue trying to test the following component (which looks similar to yours):

const MyComponent = React.createClass({
  textChanged(e) { console.log('text changed') },
  render() {
    return (
      <div className="enter-edit-mode">
        <div className="user-input" ref="content" contentEditable onInput={ this.textChanged }></div>
      </div>
    );
  }
});

I also managed to get the test working, in a somewhat convoluted way:

it('should enter text changed method on input', () => {
  let view = mount(<MyComponent/>);
  let spy  = sinon.spy(view.instance(), 'textChanged');
  view     = view.mount();

  view.find('.user-input').simulate('input');
  expect(spy).to.be.called;
  spy.restore();
});

The view.mount() to re-mount the component seems to do the trick here.

I'm wasn't familiar with Enzyme at all (although I like it :), but it looks like it's adding various layers around components, in which case it's easy for Sinon spies to get "lost".

One possible caveat: I did all of this testing in Node using jsdom, and not in a browser.

robertklep
  • 198,204
  • 35
  • 394
  • 381
  • Wow! Thanks, the `view.mount()` thing really worked. I still can't properly explain it, though. I found a [somewhat similar issue here](http://stackoverflow.com/a/9012788/1300817) and I tried moving the spy on the `Card.prototype` before I render the component (s.t. I don't have to use the `mount()` trick) but that didn't work either. I'll leave it like this as it works properly, but it would be nice to properly understand what's happening. In `beforeEach` I'm already mounting the component. Not sure why mounting it again does the trick – Raul Rene Jun 02 '16 at 20:41
  • Yeah I can't really explain either (mostly due to lack of knowledge of the specifics of what Enzyme actually does). I also tried spying on the prototype, but because of all the wrapping that's taking place the spy never got called. Spying on the instance made sense, but it wasn't until the re-mount that it actually worked. – robertklep Jun 02 '16 at 20:47
  • Well, I'm still spying on the `viewReact`, which is kinda a sub-part of the rendered view. I thought that the problem was from there. But on the other hand, similar tests work with the `click` or `change` simulation on other components without the need of re-mounting. That's what I found weird. I guess it's something about how the `input` mechanism gets triggered – Raul Rene Jun 02 '16 at 20:58
  • 1
    I just found that `view.setState({})` instead of `view.mount()` also works. – robertklep Jun 02 '16 at 21:09
0

I made a little test, I don't have sinon but I found a use case:

class Home extends Component {

  state = {
    status: 'default'
  }

  textChanged = () => {
    this.setState({
      status: 'changed'
    })
  }

  render () {
    return (
        <div className="enter-edit-mode" >
          <div className="user-input" ref="content" contentEditable onInput={this.textChanged}>Hello</div>
          <span>{this.state.status}</span>
        </div>
    )
  }
}

And the test

describe('component', () => {
    it('should say changed', () => {
      const component = shallow(<Home />);

      expect(component.find('span').text()).toEqual('default');

      component.find('.user-input').simulate('input', {key: 'a'})

      expect(component.find('span').text()).toEqual('changed');
    });
});

It passed all is expected

Michael Rasoahaingo
  • 1,069
  • 6
  • 11
  • This does indeed work, but this also worked for me. I wasn't looking for testing what I end up with in the html, but rather to test that the function was called. As I previously said, I know it was getting called because the functionality worked fine and it printed the `console.logs`, it's just that I wasn't able to make the damn test work properly that got me annoyed enough to post a question – Raul Rene Jun 02 '16 at 20:45
  • In all example, and in the enzyme, they give a spy function as prop of the component :( – Michael Rasoahaingo Jun 02 '16 at 21:13