9

I have a component which is wrapped in both a Material-UI withStyles HOC and a React memo HOC.

I am unable to test this component as I am unable to call dive():

ShallowWrapper::dive() can only be called on components

The only option I am currently aware of is to independently export Demo and export default withStyles(styles)(Demo). This allows me to test the component that isn't wrapped in withStyles. I would like to avoid this method.

If I remove memo(), I am able to test the component. Likewise, if I remove withStyles(), I am also able to test the component. The combination of these HOCs render my component un-testable.

What are some available strategies to effectively test this component?

demo.js

import React, { memo } from "react";
import MUIIconButton from "@material-ui/core/IconButton";
import { withStyles } from "@material-ui/core/styles";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";

const styles = () => ({
  root: {
    backgroundColor: "red"
    /* more styles... */
  }
});

const Demo = memo(({ label, classes }) => (
  <div className={classes.root}>
    <Tooltip disableFocusListener title={label}>
      <Typography>label</Typography>
    </Tooltip>
  </div>
));

export default withStyles(styles)(Demo);

demo.test.js

import React from "react";
import Adapter from "enzyme-adapter-react-16";
import { configure, shallow } from "enzyme";
import Demo from "./demo";
import MUIIconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

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

describe("Demo", () => {
  it("Should have a tooltip with label", () => {
    const tooltip = "My tooltip";

    const el = shallow(<Demo label={tooltip} />).dive();

    expect(el.find(Tooltip).props().title).toEqual(tooltip);
  });
});

Full working Sandbox

Edit 2j3o14zxy0

Ed Allonby
  • 157
  • 1
  • 2
  • 9
  • check https://github.com/mui-org/material-ui/issues/9266 - there is some workaround with `createShallow`. But I'd rather keep using separate export as you already do. It's more reliable way. In some cases like Redux-connected components it's like official suggestion. So would not it be better to use the same approach across different components? – skyboyer Mar 05 '19 at 13:56

3 Answers3

9

When I wrap with memo, I get a shape that looks like this

import MemoizedFoo from './Foo'
console.log(MemoizedFoo) 

    { '$$typeof': Symbol(react.memo),
      type:
       { [Function: Foo]
         displayName: 'Foo',
         defaultProps: { theme: {} } },
      compare: null }

so in my jest test, i can get the inner component by referencing the type key

import MemoizedFoo from './Foo'
const Foo = MemoizedFoo.type

describe() { it() { shallow(Foo) ...etc } }

This is great for shallow unit tests.

If i was mounting a parent component and looking for children to be present, you could do something like this:

wrapper = mount(Layout)
wrapper.find('Memo(Foo)')
random-forest-cat
  • 33,652
  • 11
  • 120
  • 99
4

As skyboyer suggests, you should just export the memoized function. You can import the default export HOC and utilize mount, but you'll need to mock the classes object to match how it's being used within the component.

Working example: https://codesandbox.io/s/4r492qvoz9

components/Demo/demo.js

import React, { memo } from "react";
import MUIIconButton from "@material-ui/core/IconButton";
import { withStyles } from "@material-ui/core/styles";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";

const styles = () => ({
  root: {
    backgroundColor: "red"
    /* more styles... */
  }
});

export const Demo = memo(({ label, classes }) => {
  return (
    <div className={classes.root}>
      <Tooltip disableFocusListener title={label}>
        <Typography>label</Typography>
      </Tooltip>
    </div>
  );
});

export default withStyles(styles)(Demo);

components/Demo/__tests__/demo.test.js if ever need to see the DOM structure, then just use console.log(wrapper.debug()); -- for example console.log(mountHOComponent.debug());)

import React from "react";
import Adapter from "enzyme-adapter-react-16";
import { configure, shallow, mount } from "enzyme";
import { Demo } from "../demo";
import HOCDemo from "../demo";

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

const initialProps = {
  label: "My tooltip",
  classes: {
    root: "component-example"
  }
};

const shallowWrapper = shallow(<Demo {...initialProps} />);
const mountWrapper = mount(<Demo {...initialProps} />);
const mountHOComponent = mount(<HOCDemo {...initialProps} />);

describe("Demo", () => {
  afterAll(() => {
    shallowWrapper.unmount();
    mountWrapper.unmount();
  });

  it("shallowWrap renders a tooltip with label", () => {
    expect(shallowWrapper.find("WithStyles(Tooltip)").props().title).toBe(
      initialProps.label
    );
  });

  it("mountWrap renders a tooltip with label", () => {
    expect(mountWrapper.find("Tooltip").props().title).toBe(initialProps.label);
  });

  it("mountHOComponent renders a tooltip with label", () => {
    expect(mountHOComponent.find("Tooltip").props().title).toBe(
      initialProps.label
    );
  });
});
Matt Carlotta
  • 18,972
  • 4
  • 39
  • 51
  • Thanks, we have decided to bite the bullet and go with this approach and it's working well. But I still don't really know why a shallow(el).dive().dive() (One dive per HOC rule) didn't work as expected. – Ed Allonby Mar 07 '19 at 09:25
0

This is now fixed as of enzyme-adapter-react-16 v1.13.0, which added memo dive() support. Here is a forked sandbox with the dependency updated to show that both of the test methods (dive and export workaround) now pass.

Edit Testing Composed Memo and withStyles

Ed Allonby
  • 157
  • 1
  • 2
  • 9