Why does Enzyme's `find()` incorrectly return elements that don't exist?

Viewed 603

Below I have an example React component that behaves as follows -

  • When the showDiv state is false (initial state), it renders nothing.
  • When the showDiv state is updated to true (maybe by some external component, redux, etc..), it renders a <div> with a single radio input.

I've also included Jest tests:

import { expect } from 'chai';
import { mount } from 'enzyme';
import PropTypes from 'prop-types';
import React from 'react';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { showDiv: false };
  }

  render() {
    const { showDiv } = this.state;

    if (!showDiv) { return null; }

    return (
      <div className='my-component'>
        <input type="radio" value="yes" id="foo" checked={true} />
      </div>
    );
  }
}


describe('<MyComponent />', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = mount(<MyComponent />);
  });

  it('should render the div', () => {
    // This INCORRECTLY passes
    expect(wrapper.find('#foo').at(0)).to.not.be.null;
    expect(wrapper.find('#foo').at(0)).to.be.checked;
  });
});

I wrote the test to verify that the <input> exists and that it is checked, and the test passes.

However, on initial component mount/load that <input> should not be rendered (since showDiv is false), so why is the test passing?

I even output the state of the component with console.log(wrapper.find('#foo').at(0).debug()); and I can verify that nothing is being rendered... but it still passes.

$ npx jest --no-cache --config test/js/jest.config.js test/js/components/test.spec.jsx

  ● Console

    console.log test/js/components/test.spec.jsx:34

Any reason why this gives a false positive? Specifically, why the find() returns elements that don't exist, but still pass the checks (e.g. .to.be.checked)

1 Answers

I've personally never worked with chai, but it may require some additional setup for React to work (I know jest with enzyme requires an adapter). On the same note, I'd personally recommend just using Jest over chai since it's already wrapped over enzyme... but ultimately up to your preference which testing suite to use.

Side note: A popular add-on for jest is jest-enzyme, which has assertion methods like toExist() and toBeChecked().

Working demo using Jest (switch from the Browser tab to the Tests tab to run assertions):

Edit Jest with Enzyme Testing


App.test.js

import React from "react";
import { configure, mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import MyComponent from "./App";

configure({ adapter: new Adapter() });

describe("<MyComponent />", () => {
  let wrapper;

  beforeEach(() => {
    wrapper = mount(<MyComponent />);
  });

  it("initially displays a button", () => {
    expect(wrapper.find("#show-radio")).toHaveLength(1);
  });

  it("renders the 'my-component' div", () => {
    wrapper.find("#show-radio").simulate("click");

    expect(wrapper.find(".my-component")).toHaveLength(1);
    expect(wrapper.find("#foo").props().checked).toBeFalsy();
  });

  it("selects a radio", () => {
    wrapper.find("#show-radio").simulate("click");
    wrapper.find("#foo").prop("onChange")({ target: { value: "1" } });
    wrapper.update();

    expect(wrapper.find("#foo").props().checked).toBeTruthy();
  });

  it("resets radio value", () => {
    wrapper.find("#show-radio").simulate("click");
    wrapper.find("#foo").prop("onChange")({ target: { value: "1" } });
    wrapper.find("#reset").simulate("click");

    wrapper.update();

    expect(wrapper.find("#foo").props().checked).toBeFalsy();
  });

  it("hides radios", () => {
    wrapper.find("#show-radio").simulate("click");
    wrapper.find("#close-radio").simulate("click");

    wrapper.update();

    expect(wrapper.find("#show-radio")).toHaveLength(1);
    expect(wrapper.find(".my-component")).toHaveLength(0);
  });
});

App.js

import React from "react";
import "./styles.css";

export default class MyComponent extends React.Component {
  state = { showDiv: false, value: 0 };

  setValue = ({ target: { value } }) => this.setState({ value });

  resetValue = () => this.setState({ value: 0 });

  toggleRadio = () =>
    this.setState((prevState) => ({ showDiv: !prevState.showDiv }));

  render() {
    const { showDiv, value } = this.state;

    if (!showDiv) {
      return (
        <button id="show-radio" type="button" onClick={this.toggleRadio}>
          Toggle
        </button>
      );
    }

    return (
      <div className="my-component">
        <label htmlFor="foo">
          <input
            type="radio"
            id="foo"
            value="1"
            checked={value === "1"}
            onChange={this.setValue}
          />
          1
        </label>
        <label htmlFor="bar">
          <input
            type="radio"
            id="bar"
            value="2"
            checked={value === "2"}
            onChange={this.setValue}
          />
          2
        </label>
        <br />
        <button id="reset" type="button" onClick={this.resetValue}>
          Reset
        </button>
        <button id="close-radio" type="button" onClick={this.toggleRadio}>
          Close
        </button>
      </div>
    );
  }
}
Related