How to trigger onClose for react ui Menu with react-testing-library?
Asked Answered
A

4

8

I'm testing a react Material UI Menu component using react-testing-library with an onClose prop that is triggered when the menu loses focus. I cannot trigger this state though even when I add a click to a component outside of the menu or a focus an input element outside.

const UserMenu: React.FunctionComponent<UserMenuProps> = ({ className }) => {
  const signedIn = useAuthState(selectors => selectors.SignedIn);
  const username = useAuthState(selectors => selectors.Username);
  const messages = useMapState((state: AppRootState) => state.app.messages);
  const signOut = useSignOut();

  const [open, updateOpenStatus] = useState(false);
  const anchorRef = useRef(null);

  if (!signedIn) {
    return <div data-testid="user-menu" className={className}>
      <LinkButton to={ROUTES.SignIn.link()}>{messages.SignIn.Title}</LinkButton>
      <LinkButton to={ROUTES.SignUp.link()}>{messages.SignUp.Title}</LinkButton>
      <LinkButton to={ROUTES.ConfirmSignUp.link()}>{messages.ConfirmSignUp.Title}</LinkButton>
    </div>;
  }

  return <div data-testid="user-menu" className={className}>
    <Grid container direction="row" alignItems="center">
      <Typography noWrap variant="subtitle2">
        <span id="username" className="bold">{username}</span>
      </Typography>
      <IconButton id="menu-toggle" buttonRef={anchorRef} onClick={() => updateOpenStatus(true)}>
        <AccountCircle/>
      </IconButton>
      <Menu
        anchorEl={anchorRef.current}
        anchorOrigin={{
          vertical: 'top',
          horizontal: 'right'
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'right'
        }}
        open={open}
        onClose={() => updateOpenStatus(false)}
      >
        <MenuItem id="sign-out" onClick={() => { updateOpenStatus(false); signOut(); }}>{messages.SignOut.Action}</MenuItem>
      </Menu>
    </Grid>
  </div>;
};

Test code

    it('should open and close menu', async () => {
      const { getByTestId } = render(<><UserMenu/>
        <input data-testid="other"/>
      </>, { state });

      fireEvent.click(getByTestId('menu-toggle'));

      expect(MockMenu).toHaveBeenLastCalledWith(expect.objectContaining({ open: true }), {});

      fireEvent.focus(getByTestId('other'));

      expect(MockMenu).toHaveBeenLastCalledWith(expect.objectContaining({ open: false }), {});
    });

I've also tried fireEvent.click(getByTestId('other')); without success.

This question for enzyme has a solution by using tree.find(Menu).simulate("close");, but I don't think that's possible with react-testing-library.

Academia answered 6/3, 2019 at 19:31 Comment(1)
Not sure it's related but you have getByTestId('menu-toggle') but you don't have any data-testid="menu-toggle"Foe
P
11

You can trigger the close by clicking on the backdrop generated by the Menu. The easiest way I found to do that was to select the backdrop via the getByRole('presentation') method of @testing-library.

Test Code:

it('should open and close the menu', () => {
  const { getByTestId, getByRole } = render(<UserMenu />);

  fireEvent.click(getByTestId('menu-toggle'));

  // Get the backdrop, then get the firstChild because this is where the event listener is attached
  fireEvent.click(getByRole('presentation').firstChild));

  expect(MockMenu).toHaveBeenLastCalledWith(expect.objectContaining({ open: false }), {});
});
Protomartyr answered 22/2, 2020 at 1:17 Comment(6)
Sorry, but searching by role 'presentation' doesn't seem to return anything Error: Unable to find an element by [role=presentation]Academia
@Academia If you use debug from RTL renderResult, do you have a backdrop of any kind?Protomartyr
where is MockMenu declared?Mauer
I originally took that from the author. I assumed it was a mock of their callback, but the setup might need to be tweaked. Generally, you'd just test that the close callback is called whenever the backdrop is clicked, or that that component is no longer visible (or both). In this case, maybe: expect(queryByRole('presentation')).toBeNull()Protomartyr
This was the only thing that worked for closing the dropdown. Not even pressing {esc} worked. Thank you!Polyadelphous
@dimiguel, I have updated my answer where I mentioned using {esc}. Apparently, a month after I did my answer they changed their syntax and it should be {Escape} for the keypress now.Chang
C
4

I use @testing-library/user-event to trigger an Escape keypress.

NEW ANSWER

As of @testing-library/user-event v14.0.0 (2022-03-29), they have changed the pattern matching for escape to be {Escape} instead of {esc}

import {
  render,
  screen,
  waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('should open and close the menu', async () => {
  render(<UserMenu />);

  // Click to open
  userEvent.click(
    // Note that grabbing by test id is frowned upon if there are other ways to grab it https://testing-library.com/docs/queries/about/#priority
    screen.getByTestId('menu-toggle')
  );

  // Wait for dialog to open
  await waitFor(() => expect(MockMenu).toHaveBeenLastCalledWith(expect.objectContaining({ open: true }), {}));

  // Press `esc` to close
  userEvent.keyboard('{Escape}');

  // Wait for dialog to close
  await waitFor(() => expect(MockMenu).toHaveBeenLastCalledWith(expect.objectContaining({ open: false }), {}));
});

ORIGINAL ANSWER: Prior to @testing-library/user-event v14.0.0 (2022-03-29)

import {
  render,
  screen,
  waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('should open and close the menu', async () => {
  render(<UserMenu />);

  // Click to open
  userEvent.click(
    // Note that grabbing by test id is frowned upon if there are other ways to grab it https://testing-library.com/docs/queries/about/#priority
    screen.getByTestId('menu-toggle')
  );

  // Wait for dialog to open
  await waitFor(() => expect(MockMenu).toHaveBeenLastCalledWith(expect.objectContaining({ open: true }), {}));

  // Press `esc` to close
  userEvent.keyboard('{esc}');

  // Wait for dialog to close
  await waitFor(() => expect(MockMenu).toHaveBeenLastCalledWith(expect.objectContaining({ open: false }), {}));
});
Chang answered 28/2, 2022 at 18:52 Comment(0)
I
3

I wanted to post this because I faced a lot of issues with testing the MUI <Menu /> component and hopefully this helps someone else.

I was only able to get this working after copying Dylan Walker's solution and utilizing waitForElementToBeRemoved

My final testing solution looks like:

import { 
    render,
    queryByText,
    waitForElementToBeRemoved,
    screen,
    fireEvent
} from "@testing-library/react"

it("should open and close", async () => {
    render(<><MyMenuComponent /></>)

    // open the menu by clicking button
    userEvent.click(screen.getByText("my-button-text"));

    // close the menu and wait for disappearance
    fireEvent.click(screen.getByRole("presentation").firstChild)
    await waitForElementToBeRemoved(() => screen.queryByText("my-menu-item-text")));

    // make assertion
    expect( ... )
})
Ilarrold answered 11/11, 2022 at 20:56 Comment(0)
B
2

I followed the test case written in the mui git files. Basically they click on the backdrop to trigger onClose.

              MenuProps={{ BackdropProps: { "aria-label": "backdrop-select" } }}

Then in test case,

act(() => {
      screen.getByLabelText("backdrop-select").click();
    });

Source:

https://github.com/mui/material-ui/blob/master/packages/mui-material/src/Select/Select.test.js
Barb answered 7/3, 2023 at 3:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.