You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 14:36:02