I am hoping to get some expert advice and guidance from the React Testing Library community on the best way to ensure a Material UI ripple animation has been completed prior to taking a snapshot.
This issue has been causing flakey tests for us with everything passing locally all the time but when we run on our CI servers the tests are intermittent and failing as the animation finishes quicker than it does locally meaning it is does not match the stored snapshot.
The issue only arose once we swapped out fireEvent
for userEvent
which makes sense with the additional actions under the hood userEvent delivers but want I want to get to is having snapshots stored that don't contain any UI transitions.
I have created a simple test below which will help illustrate the issue (also on codesandbox):
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Button from "@material-ui/core/Button";
test("clicks material button with fireEvent - no ripple implication", () => {
render(
<Button variant="contained" color="primary">
Ripple
</Button>
);
const btnContainer = screen.getByRole("button", { name: /ripple/i });
expect(btnContainer).toBeInTheDocument();
screen.debug(btnContainer);
fireEvent.click(btnContainer);
screen.debug(btnContainer);
});
test("clicks material button with userEvent", async () => {
render(
<Button variant="contained" color="primary">
Ripple
</Button>
);
const btnContainer = screen.getByRole("button", { name: /ripple/i });
expect(btnContainer).toBeInTheDocument();
screen.debug(btnContainer);
userEvent.click(btnContainer);
screen.debug(btnContainer);
});
test("clicks material button with userEvent wait for ripples", async () => {
render(
<Button variant="contained" color="primary">
Ripple
</Button>
);
const btnContainer = screen.getByRole("button", { name: /ripple/i });
expect(btnContainer).toBeInTheDocument();
screen.debug(btnContainer);
userEvent.click(btnContainer);
screen.debug(btnContainer);
await waitForRippleToRemove(btnContainer);
// without the ripple transition being complete inconsitent tests runs can occur
// when using snapshots as per commented line below
// (ie some have ripples some don't)
// expect(asFragment()).toMatchSnapshot();
screen.debug(btnContainer);
});
function waitForRippleToRemove(container) {
return waitFor(() => {
expect(
container.querySelector("span.MuiTouchRipple-root")
).toBeEmptyDOMElement();
});
}
The first test works fine using the fireEvent
with the button always looking like:
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
tabindex="0"
type="button"
>
<span
class="MuiButton-label"
>
Ripple
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
However, when using the userEvent
in test 2 the buttons looks like the following after the click:
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
tabindex="0"
type="button"
>
<span
class="MuiButton-label"
>
Ripple
</span>
<span
class="MuiTouchRipple-root"
>
<span
class="MuiTouchRipple-ripple MuiTouchRipple-rippleVisible"
style="width: 2.8284271247461903px; height: 2.8284271247461903px; top: -1.4142135623730951px; left: -1.4142135623730951px;"
>
<span
class="MuiTouchRipple-child MuiTouchRipple-childLeaving"
/>
</span>
</span>
</button>
The span.MuiTouchRipple-ripple
element will be removed as part of the transition however my issue is what is the best approach to wait on this occurring as my test shows about I am checking implementation detail (ie using classnames of 3rd party dependency) which feels a bit horrible.
await waitFor(() => {
expect(
container.querySelector("span.MuiTouchRipple-root")
).toBeEmptyDOMElement();
});
Also, it is worth pointing out that depending on the component under test we may have had to use the userEvent.click
multiple time so having numerous calls to waitForRippleToRemove(btnContainer)
throughout the tests feels like the wrong thing to do but at present is a workable solution.