Trigger a sub-component event with Enzyme

I was struggling to get sub-component interactions nailed down and figured out a few ways to propagate your sub-component interactions with Enzyme.

In my scenario, I had a component that relied on some simply sub-components. The sub-components essentially extend default HTML controls with a bit of sugar. They also expose standard callbacks as props.

My parent component then hooks in to sub-components change events and performs whatever action I need them to.

Sound a bit confusing? Well here, take a look and I'll explain further.

import React from 'react';  
import FormGroup from '../shared/form-group.jsx');  
import TextInput from '../shared/text-input.jsx');

export default ({ onFilter }) => (  
  <div className='list-item-filters'>
    <FormGroup>
      <TextInput
        placeholder='Search for items...'
        onChange={ e => onFilter('query', e.target.value)} />
    </FormGroup>
    {/* more of the same go here */
  </div>
);

That is the the parent component. It leverages the <TextInput/> component. This component simply takes a callback prop that gets called when the TextInput sub-component fires its onChange event. In short, a text box change event should trigger the onFilter event of my filter component.

The TextInput component looks like this:

import React from 'react';

export default ({ placeholder, value, onChange, ref }) => (  
  <input 
    type='text'
    ref={ref} 
    className='form-control' 
    placeholder={placeholder} 
    value={value} 
    onChange={onChange} />;
);

So back to the task at hand. How best to test the the interaction? Functionally, a keypress event triggers the change in the value of the input, which calls the onChange prop. That prop was supplied by the parent component as an arrow function that calls it's onFilter prop.

Lets start with that by doing a full render and simulating the event.

import React form 'react';  
import { mount } from 'enzyme';  
import chai, { expect } from 'chai';  
import sinon from 'sinon';

import ItemFilter from ./item-filter.jx';  
import TextInput from '../shared/text-input.jxs';

describe('<ItemFilter />', () => {  
  it('should trigger onFilter when query changes', () => {  
    // stub the callback from onFilter
    let onFilter = sinon.stub();

    // generate a fully render wrapper with jsdom
    let wrapper = mount(<ItemFilter onFilter={onFilter} />);

    // find the TextInput, and since it is an Input
    // directly perform the simulate against it
    wrapper.find(TextInput).first().simulate('change', { target: { value: 'iron man' }});

    // assert that onFilter was called with our 
    // expected arguments: 'query' and 'marvel'
    expect(onFilter.getCall(0).args).to.deep.equal(['query', 'marvel']);
  });
});

If you run this you'll find that it's actually pretty slow. A better way is to use the shallow rendering technique.

import React form 'react';  
import { mount } from 'enzyme';  
import chai, { expect } from 'chai';  
import sinon from 'sinon';

import ItemFilter from ./item-filter.jx';  
import TextInput from '../shared/text-input.jxs';

describe('<ItemFilter />', () => {  
  it('should trigger onFilter when query changes', () => {
    // stub the callback from onFilter
    let onFilter = sinon.stub();

    // generate a shallow render of the component
    let wrapper = shallow(<ItemFilter onFilter={onFilter} />);

    // find the TextInput and trigger the prop directly
    // and supply a fake event in the call
    wrapper.find(TextInput).first().prop('onChange')({ target: { value: 'marvel' }});

    // assert that onFilter was called with our 
    // expected arguments: 'query' and 'marvel'
    expect(onFilter.getCall(0).args).to.deep.equal(['query', 'marvel']);
  });
});

There you have it!

comments powered by Disqus