reactjsjestjsmaterial-uireact-testing-libraryuser-event

testing-library/user-event - how to handle `act(...)` warnings related to material-ui's animations?


I understand that I'm not supposed to add act() around user events because the testing library does it for me, but I get warnings if I don't. Specifically, for example given a MUI component with the ripple effect, here's what I find:

import { Tab as MuiTab } from "@mui/material";

describe("Tab", () => {

    it("MuiTab creates a warning ", async () => {
        const user = userEvent.setup();
        render(<MuiTab label="hello" />);

        // yep.  There's a warning here.
        await user.click(screen.getByRole("tab"));
    });

    it("MuiTab does NOT create a warning with disableTouchRipple", async () => {
        const user = userEvent.setup();

        // disabling the animation fixes the warning
        render(<MuiTab label="hello" disableTouchRipple />);

        await user.click(screen.getByRole("tab"));
    });

    it("MuiTab does NOT create a warning with act", async () => {
        const user = userEvent.setup();
        render(<MuiTab label="hello" />);

        // no warning
        await act(async () => await user.click(screen.getByRole("tab")));
    });
});

These warnings started when I upgraded @testing-library/react to ^14.0.0, which fixed other act warnings.

But now with the upgrade, I have 1.7M lines of warnings in my test suite. It's not "wrong" in that the ripple effect hasn't ended by the end of the test and I also understand that using jest timers can help resolve this.

But it feels wrong to add act throughout the testing suite or to litter my test code with fake timers just because MUI has some animations.

Given all that, what's the recommended way for me to fix these warnings?

Versions:

(Also, please don't tell me that I should be asserting on the expected behavior after the click. I know that. I'm just using the Tab as an example and I get the warnings whether there's an expect afterwards or not.)


Solution

  • The root cause ended up being that npm resolved version dependencies of @testing-library/dom incorrectly.

    This took a lot of time to figure out. Specifically, I would tweak the package.json and pin specific versions, then run npm install && npm run test &> output##-version-details.log so that I could track what happened with each change. When I started seeing conflicts in my output is when I looked at the package-lock.json changes. By leveraging git I could then start easily compare package-lock.json files.

    Where @testing-library/react wanted @testing-library/dom as ^9.0.0, what npm had actually resolved was 8.20.0. I don't know why -- the package.json did not specify a dom version.

    Once @testing-library/react was using the correct dom version, the bulk of the warnings went away.

    The upgrade also resulted in 7 new test failures out of ~950 tests, but those should be easy to resolve.


    Regarding wrapping all user input in act() or waitFor()

    While I don't have specific evidence, I read a number of reports about waitFor suppressing error messages. It doesn't make sense to waitFor a click event since there's nothing identifiable for the library to say "yes, okay, it's done". I suspect that it "works" because it's suppressing the warnings and/or just a stand-in for wrapping it in another act() directly.

    On that note, wrapping events in act() seemed like it should have been superfluous since testing-library already wraps user events in act. And when I did start adding it through my test suite, I started getting the Warning: The current testing environment is not configured to support act(...) error in some cases.

    Hence none of the documentation on react testing library nor my own experiences indicated to me this was the "right" solution.


    Regarding configuring MUI

    This was a dead-end.

    Before I figured out the package issue, I also spent a lot of time trying to disable the TouchRipple effect in MUI and transitions in MUI. This resulted in ~20K warning lines reduced total, but it also resulted in more failed tests, too. I might go back later and disable TouchRipple just because it's not necessary for tests.

    source for globally disabling

    import { createTheme } from '@mui/material';
    
    const themeOverrides = createTheme({
      transitions: {
        // So we have `transition: none;` everywhere
        create: () => 'none',
      },
      components: {
        // Name of the component ⚛️
        MuiButtonBase: {
          defaultProps: {
            // The props to apply
            disableRipple: true, // No more ripple, on the whole application 💣!
          },
        },
      },
    });
    ## Then use the new these in a ThemeProvider for your tests.  
    ## You can nest themes.