如何使用Testing Library触发含animationName的React animationEnd事件?
问题描述
我有一个React + TypeScript项目,希望通过Jest和Testing Library验证div元素在fadeOut CSS动画完成后从DOM中移除,但仅当该动画完成时执行移除操作。
问题在于Jest的JSDOM无头浏览器不会自动触发CSS动画事件,手动触发时animationName属性始终为undefined,导致组件无法识别动画类型,无法执行移除逻辑。以下是相关代码及测试报错信息:
SASS文件 Example.module.scss
@keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fadeOut { 0% { opacity: 1; } 100% { opacity: 0; } }
React组件代码 Example.tsx
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;
测试文件 Example.test.tsx(原错误版本)
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(); }); }); });
测试CLI输出
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)
解决方案
核心问题有两个:一是CSS Modules编译后的动画名是哈希值,而非原始字符串fadeOut;二是JSDOM手动触发事件时需正确传递符合React合成事件规范的参数。
方法1:传递CSS Modules编译后的真实动画名
修改测试文件,导入样式模块并使用s.fadeOut作为animationName的值,同时确保异步操作的正确性:
import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Example from './Example'; import s from './Example.module.scss'; // 导入样式模块 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'); // userEvent的click是异步操作,需await确保状态更新 await user.click(buttonEl); // 传递CSS Modules编译后的真实动画名,同时开启事件冒泡 fireEvent.animationEnd(divEl, { animationName: s.fadeOut, bubbles: true }); await waitFor(() => { expect(screen.queryByTestId('div')).not.toBeInTheDocument(); }); }); });
方法2:模拟getComputedStyle(备选方案)
如果方法1仍不生效,可通过模拟window.getComputedStyle让组件获取正确的动画名:
import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Example from './Example'; import s from './Example.module.scss'; 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'); await user.click(buttonEl); // 模拟getComputedStyle返回正确的动画名 const originalGetComputedStyle = window.getComputedStyle; jest.spyOn(window, 'getComputedStyle').mockImplementation(el => { if (el === divEl) { return { animationName: s.fadeOut } as CSSStyleDeclaration; } return originalGetComputedStyle(el); }); fireEvent.animationEnd(divEl, { bubbles: true }); await waitFor(() => { expect(screen.queryByTestId('div')).not.toBeInTheDocument(); }); // 还原mock window.getComputedStyle.mockRestore(); }); });
关键注意事项
- CSS Modules哈希名:不要直接使用字符串
'fadeOut',必须用导入的s.fadeOut,因为编译后的动画名是带哈希的唯一标识。 - 异步操作:
user.click是异步方法,需添加await确保组件状态更新后再触发动画事件。 - 事件冒泡:手动触发事件时添加
bubbles: true,符合React合成事件的冒泡机制要求。
内容的提问来源于stack exchange,提问作者mrbinky3000
相关产品推荐
相关产品推荐

