Reach-ui: How to test @reach/menu-button with react-testing-library

Created on 20 Jan 2020  Â·  4Comments  Â·  Source: reach/reach-ui

Hey, first of all, I'd like to thank all of you for your continuous work on reach-ui.

I'm having some difficulties testing the menu button with react-testing library. It most likely has to do with the portal rendering outside of the body.

Given a menu button like this

import {
  Menu,
  MenuList,
  MenuButton,
  MenuItem,
  MenuLink
} from "@reach/menu-button";


const Selection = ()= > (
    <Menu>
      <MenuButton data-testid="action-button">
        Actions <span aria-hidden>â–¾</span>
      </MenuButton>
      <MenuList>
        <MenuItem
          data-testid="action-button-download"
          onSelect={() => setIsDownloading(true)}
        >
          Download
        </MenuItem>
        <MenuLink as="a" href="https://reach.tech/workshops">
          Attend a Workshop
        </MenuLink>
      </MenuList>
    </Menu>
)

and a simple test that first clicks the MenuButton and then tries to get the MenuItem, will always fail.

import { waitForElement, fireEvent, render } from "@testing-library/react";
it("can click on a menu button item", async () => {
  const { getByTestId, getAllByTestId } = render(<Selection />);

  const actionButtons = getAllByTestId("action-button");
  fireEvent.click(actionButtons.pop());

  // fails here as it can't find the download button
  const downloadAction = await waitForElement(() =>
    getByTestId("action-button-download")
  );
})

The reason for this is that the getByTestId is scoped to the parent (Selection) container while the MenuItems are being rendered nowhere near since they live inside the portal.

As a workaround, I thought I might be able to destructure the container when rendering the Selection and then find the most upper container (ideally the html element) by calling container.parentElement. I then tried to retrieve the MenuItem by the test id using the imported getByTestId that accepts a container as a first argument getByTestId(container.parentElement, "action-button-download"). That too did not work.

I replicated my attempts in tests inside this Codesandbox.

Let me know if you need more information.

Relates to #327

Question

Most helpful comment

The event that triggers the menu is mouseDown, not click. You would have to call fireEvent.mouseDown() on the menu button instead, but this also means that your test is testing implementation details, which is not ideal.

I've updated your codesandbox with reakit-test-utils, but you can also use @testing-library/user-event (you may need to wrap events within act() with this one): https://codesandbox.io/s/testing-reach-ui-menu-button-o9wf0

All 4 comments

I'm having the same issue on my side, but I think saying that react-portals are not rendering in the body is not true 😅 . Or at least, on my cases, they are appended at the end of the body.

Here's a codesandbox reflecting the problem : https://codesandbox.io/s/objective-greider-u8vw7 . You can run the test by clicking the test tab on the top right side of the page 😊

The event that triggers the menu is mouseDown, not click. You would have to call fireEvent.mouseDown() on the menu button instead, but this also means that your test is testing implementation details, which is not ideal.

I've updated your codesandbox with reakit-test-utils, but you can also use @testing-library/user-event (you may need to wrap events within act() with this one): https://codesandbox.io/s/testing-reach-ui-menu-button-o9wf0

Thank you so much. This is super helpful.

Do you by chance know why @testing-library click does not fire the mouseDown event while the click of reakit-test-utils does?

That's because fireEvent.click(element) is basically element.dispatchEvent(new MouseEvent("click", ...)).

reakit-test-utils and @testing-library/user-event's click simulates a real click as if it were done by the user on the browser, which includes dispatching all the other related events. Take a look at the code:

Was this page helpful?
0 / 5 - 0 ratings