Skip to content

Instantly share code, notes, and snippets.

@MrLeebo
Last active January 3, 2017 15:28
Show Gist options
  • Save MrLeebo/1b93c1dba6e8b1829288132e995f20ae to your computer and use it in GitHub Desktop.
Save MrLeebo/1b93c1dba6e8b1829288132e995f20ae to your computer and use it in GitHub Desktop.

Inconsistent Enzyme find Selectors

There is a curious inconsistency between enzyme selectors for shallow rendered components (shallow) and fully rendered components (mount). In order to organize my thoughts, this is a gist describing the issue. First, let's propose a component.

const RepeatDiv = ({depth}) => (<div>{depth > 1 && <RepeatDiv depth={depth-1} />}</div>);

This snippet will define a component called RepeatDiv that will accept a depth prop and will render itself recursively to draw N div elements.

For example, we can fully render our component by writing mount(<RepeatDiv depth={3} />) which will render:

<div>
  <div>
    <div />
  </div>
</div>

This is different from shallow rendering, written as shallow(<RepeatDiv depth={3} />), where the component itself is rendered but its children do not get rendered and display their definition instead:

<div>
  <RepeatDiv depth={2} />
</div>

Both rendering techniques have their uses. Mounting is good for interaction-based or integration tests. You can update textboxes, submit HTML forms, and observe how those changes propagate through the props/state of the component and its children. Shallow rendering is useful for testing a component as a single unit; separated from the behaviors associated with its children.

Next, we should verify if our mental model about how these rendering techniques work matches with their implementations inside enzyme.

Let's try console.log(shallow(<RepeatDiv depth={3} />).debug());:

<div>
<RepeatDiv depth={2} />
</div>

Looks good. Missing the indent, but otherwise it fits our expectation. What about selectors?

subject.find('RepeatDiv').length == 1 // true

As expected, we can select the RepeatDiv by its name and the component is returned.

subject.find('RepeatDiv[depth=2]').length == 1 // true

We can also apply properties to the selector and the same match is found.

So what about full DOM rendering? What does console.log(mount(<RepeatDiv depth={3} />).debug()); return?

<RepeatDiv depth={3}>
  <div>
    <RepeatDiv depth={2}>
      <div>
        <RepeatDiv depth={1}>
          <div />
        </RepeatDiv>
      </div>
    </RepeatDiv>
  </div>
</RepeatDiv>

Hmmm. This is different. Each <div> layer is decorated with the RepeatDiv component that spawned it, even though those elements are not present in the final HTML. But I wonder, are those components just part of debug() trying to be helpful?

subject.find('RepeatDiv').length == 0 // false. 3 != 0

I can find the RepeatDiv elements by the component's display name. They show up in the selector's results and therefore can impact my test results, even though they won't be present in the actual HTML.

So, can I select a specific RepeatDiv component based on its props?

subject.find('RepeatDiv[depth=2]').length == 1 // false. 0 != 1

No, the component is missing when I use any other props or selectors to try to select it.

But I know it was in the results when I did the first find query. Can we use that to write an equivalent of the second query?

subject.find('RepeatDiv').findWhere(n => n.prop('depth') == 2).length == 1 // true

Yes. You can still access the props of the non-DOM component when mounted. Non-DOM components are in some kind of quasi-selectable state. This explains a number of difficulties I've had in the past while trying to write tests with enzyme and I had a shaky understanding of the difference between shallow and mount rendering.

Conclusion

It seems to me that the output from mounting a component in enzyme should match the expected final HTML:

<div>
  <div>
    <div />
  </div>
</div>

However, it may be useful to be able to query on the non-DOM components and their props in certain tests. I think that the behavior of subject.find('RepeatDiv').length returning anything but 0 for fully mounted rendering should be deprecated from enzyme (with a warning if you write this query) and that any selector should be made to work with non-DOM components by adding an option subject.find(..., {includeNonDom: true}) which supports selecting non-DOM components for ALL selectors, not just display name.

import React from 'react';
import jsdom from 'jsdom';
import assert from 'assert';
import { shallow, mount } from 'enzyme';
global.document = jsdom.jsdom('');
global.window = document.defaultView;
const RepeatDiv = ({depth}) => (<div>{depth > 1 && <RepeatDiv depth={depth-1} />}</div>);
describe('RepeatDiv', () => {
let subject;
describe('shallow', () => {
beforeEach(() => {
subject = shallow(<RepeatDiv depth={3} />);
})
it('finds by constructor', () => {
assert.equal(subject.find(RepeatDiv).length, 1)
})
it("finds by display name", () => {
assert.equal(subject.find('RepeatDiv').length, 1);
})
it('finds RepeatDiv by props too', () => {
assert.equal(subject.find('RepeatDiv[depth=2]').length, 1);
})
})
describe('mount', () => {
beforeEach(() => {
subject = mount(<RepeatDiv depth={3} />)
})
it('finds by constructor', () => {
assert.equal(subject.find(RepeatDiv).length, 3)
})
it('finds by display name', () => {
assert.equal(subject.find('RepeatDiv').length, 3)
})
it('cannot find with props though', () => {
assert.equal(subject.find('RepeatDiv[depth=2]').length, 0)
})
it('reveals props even though it cannot find them', () => {
assert.deepEqual(subject.find(RepeatDiv).first().props(), {depth: 3})
})
it('can be queried on props another way', () => {
assert.equal(subject.find(RepeatDiv).findWhere(n => n.prop('depth') == 2).length, 1)
})
})
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment