I have a React + TypeScript project where I want to use Jest and testing-library to confirm that a div element is removed from the DOM after a CSS animation completes. However, the removal must only happen after an animation with the name “fadeOut” is completed.
The code below is a simplified and contrived example. I only want the div to be removed when an animation with the name “fadeOut” is completed.
The problem is that jest’s JSDOM headless browser doesn’t fire css animation events. I need to fire them manually. And it seems that I can’t pass the animationName property along with the event to my component. A console.log of the animationName always indicates that the animationName is undefined in jest, but correct in the browser.
Here is the SASS file Example.module.scss
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
Here is the Example.tsx
component React code:
import React, { useCallback, useState } from 'react';
import s from './Example.module.scss';
const Example = () => {
const [isMounted, setIsMounted] = useState<boolean>(true);
const [isFading, setIsFading] = useState<boolean>(false);
const handleOnAnimationEnd = useCallback(
(ev: React.AnimationEvent<HTMLDivElement>) => {
console.log(`The animationName is : "${ev.animationName}"`);
if (ev.animationName === s.fadeOut) {
console.log('finished fading out.');
setIsMounted(false);
}
if (ev.animationName === s.fadeIn) {
console.log('finished fading in.');
}
},
[]
);
const handleOnClick = useCallback(() => {
if (isMounted) {
setIsFading(true);
}
}, [isMounted]);
return (
<>
<h1>Contrived example for Stack Overflow:</h1>
<button onClick={handleOnClick}>Toggle</button>
{(isMounted || isFading) && (
<div
data-testid="div"
style={{
animation: `${isFading ? s.fadeOut : s.fadeIn} 400ms forwards`,
}}
onAnimationEnd={handleOnAnimationEnd}
>
Hello!
</div>
)}
</>
);
};
export default Example;
Here is my Example.test.tsx
test file:
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Example from './Example';
describe('Example', () => {
it('should fade out', async () => {
expect.hasAssertions();
const user = userEvent.setup();
render(<Example />);
const divEl = screen.getByTestId('div');
const buttonEl = screen.getByRole('button');
user.click(buttonEl);
// jsdom doesn't fire animation events
fireEvent.animationStart(divEl);
// manually fire animationEnd event because jsdom doesn't.
fireEvent.animationEnd(divEl, { animationName: 'fadeOut' });
await waitFor(() => {
expect(screen.queryByTestId('div')).not.toBeInTheDocument();
});
});
});
And this is the CLI output from running my test:
console.log
The animationName is : "undefined"
at lib/Example/Example.tsx:10:15
FAIL lib/Example/Example.test.tsx
Example
✕ should fade out (1064 ms)
● Example › should fade out
expect(element).not.toBeInTheDocument()
expected document not to contain element, found <div data-testid="div" style="animation: fadeOut 400ms forwards;">Hello!</div> instead
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<h1>
Contrived example for Stack Overflow:
</h1>
<button>
Toggle
</button>
<div
data-testid="div"
style="animation: fadeOut 400ms forwards;"
>
Hello!
</div>
</div>
</body>
</html>
20 |
21 | await waitFor(() => {
> 22 | expect(screen.queryByTestId('div')).not.toBeInTheDocument();
| ^
23 | });
24 | });
25 | });
at lib/Example/Example.test.tsx:22:47
at runWithExpensiveErrorDiagnosticsDisabled (../../node_modules/@testing-library/dom/dist/config.js:47:12)
at checkCallback (../../node_modules/@testing-library/dom/dist/wait-for.js:124:77)
at checkRealTimersCallback (../../node_modules/@testing-library/dom/dist/wait-for.js:118:16)
at Timeout.task [as _onTimeout] (../../node_modules/jsdom/lib/jsdom/browser/Window.js:520:19)
As you can see in the output, the onAnimationEnd callback function is called, but the animationName property is undefined. You can also see that the animation did switch to fadeOut from fadeIn, so the correct animation is getting applied.
If I take out the check for the animationName, it works. But in this contrived example, I really need to be able to reference the animationName. It is a requirement for my actual code in my real project.
How does one pass the animationName property along with the event using testing-library’s fireEvent.animationEnd() method?