0

the problem is pretty straightforward. I have a material-ui menu I would like to test closes appropriately (through a user clicking outside of the menu).

This has proven to be quite challenging. Here's the component (ripped right from the the docs):

class App extends React.Component {
  state = {
    anchorEl: null
  };

  handleClick = event => {
    this.setState({ anchorEl: event.currentTarget });
  };

  handleClose = () => {
    this.setState({ anchorEl: null });
  };

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

    return (
      <div>
        <Typography variant="body1"> Click Me</Typography>
        <Button
          aria-owns={anchorEl ? "simple-menu" : undefined}
          aria-haspopup="true"
          onClick={this.handleClick}
        >
          Open Menu
        </Button>
        <Menu
          id="simple-menu"
          anchorEl={anchorEl}
          open={Boolean(anchorEl)}
          onClose={this.handleClose}
        >
          <MenuItem onClick={this.handleClose}>Profile</MenuItem>
          <MenuItem onClick={this.handleClose}>My account</MenuItem>
          <MenuItem onClick={this.handleClose}>Logout</MenuItem>
        </Menu>
      </div>
    );
  }
}

And the tests:

const props = {
  classes: {}
};

describe("test", () => {
  it("closes the menu", () => {
    const wrapper = mount(<App {...props} />);
    wrapper.find(Button).simulate("click");
    expect(wrapper.find(Menu).prop("open")).toBe(true);
    wrapper.find(Typography).simulate("click");
    expect(wrapper.find(Menu).prop("open")).toBe(false);
    wrapper.unmount();
  });

  it("closes the menu a different way", () => {
    const outerNode = document.createElement("div");
    outerNode.setAttribute("id", "root-node");
    document.body.appendChild(outerNode);
    const wrapper = mount(<App {...props} />, {
      attachTo: outerNode
    });
    wrapper.find(Button).simulate("click");
    expect(wrapper.find(Menu).prop("open")).toBe(true);
    outerNode.click();
    expect(wrapper.find(Menu).prop("open")).toBe(false);
    wrapper.unmount();
  });

  it("closes the menu yet a different way", () => {
    const eventMap = {};
    window.addEventListener = jest.fn((event, cb) => {
      eventMap[event] = cb;
    });
    const wrapper = mount(<App {...props} />);
    wrapper.find(Button).simulate("click");
    expect(wrapper.find(Menu).prop("open")).toBe(true);
    wrapper.find(Typography).simulate("click");
    eventMap.click();
    expect(wrapper.find(Menu).prop("open")).toBe(false);
    wrapper.unmount();
  });
});

None of these tests work, unfortunately. I was doing some digging and came across this SO post which links this github issue where the solutions and third approaches originally came from, however, I can't get any of these to work.

I created a CodeSandbox with the code below for any kind generous souls here who desire to help.

Edit n3n96op120

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Dane Jordan
  • 1,071
  • 1
  • 17
  • 27
  • I think this is not how you should write unit tests. You should only test your code. Here you are trying to test material-ui code. You should just test if your onClose function should set isOpen state to false and rest will be handled by material-ui as they have tested their logic in their code. – Amit Chauhan Dec 18 '18 at 18:04
  • I agree, however, I like to test how users test / use the app. I would like to trigger closing the menu not through manually firing `onClose` but by interacting with the ui in a way that `onClose` gets called as a result. This also ensures that `onClose` is correctly connected to the element. – Dane Jordan Dec 18 '18 at 18:59
  • This perhaps is another way of guaranteeing `onClose` was called without mocking onClose, since the result of that is closing the menu. Either way, it is unsuccessful. – Dane Jordan Dec 18 '18 at 19:07

1 Answers1

3

It may be because, the elements are not present immediately after the click.

Can you try - enzyme-async-helpers,

await waitForElement(wrapper, Menu);

Edit:

After digging in to simulate() method and Menu component, following are my findings:

  • simulate('click') internally looks for 'onClick' prop on the component you are simulating and invokes that method if it is there, otherwise it will not do anything. In your case Typography doesn't have any onClick handler, so it will not do anything. Even if it's there, it will invoke function that you passed in, not the actual internal implementation of Menu, where it closes.
  • Menu internally using Backdrop component, which is having onClick method and closes the menu when you click outside.

Solution for your problem would be:

   wrapper.find(Backdrop).simulate("click");

and also import:

import { Button, Menu, Typography, Backdrop } from "@material-ui/core";

You can find the running example here: https://codesandbox.io/s/vyon4kvjpy

Rakesh Makluri
  • 647
  • 4
  • 10