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!