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

使用Enzyme测试带计时器的React表格行事件时遇组件卸载错误

解决React组件单击双击计时器与单元测试的卸载问题

你遇到的核心问题是测试异步回调执行时,Enzyme挂载的组件实例已经被React卸载了。这是因为使用真实setTimeout时,Jest测试环境可能在等待回调的过程中,默认认为测试已完成(哪怕你用了done),或者组件在state更新后出现了意外卸载。另外,组件里的全局计时器变量也会导致测试用例间的干扰,加重了这个问题。

问题拆解

你的测试用了真实的300ms延迟,这段等待时间里,Jest的测试上下文可能已经发生变化,Enzyme的wrapper对应的组件实例已被卸载。当你在回调里调用rows2.at(3).simulate('click')时,React找不到对应的DOM节点,就抛出了「无法在已卸载组件上找到节点」的错误。

同时,组件里的timer和prevent是全局变量,这会导致多行事件互相干扰,也会让测试结果变得不可预测。

具体解决方案

1. 改用Jest的Fake Timers

Jest提供了模拟计时器的能力,不用等待真实时间,手动控制计时器执行即可,既加快测试速度,又能避免组件被卸载的问题。修改测试代码如下:

it('selects row on single click', (done) => {
  // 启用Jest模拟计时器
  jest.useFakeTimers();
  const mockCallback = jest.fn();
  const newProps = {...props, data: data, schema: schema, onSelect: mockCallback};
  const wrapper = mount(<QueryRecordsTable {...newProps} />);
  const rows = wrapper.find(Tooltip);
  
  expect(rows.length).toEqual(6);

  // 模拟第一次点击
  rows.at(0).simulate('click');
  // 快进计时器到延迟时间之后,同步执行setTimeout回调
  jest.runAllTimers();

  // 验证第一次点击的状态变化
  expect(mockCallback.mock.calls[0][0]).toEqual(ID1);
  expect((wrapper.state() as any).selectedRecord.ID).toEqual(ID1);
  expect(wrapper).toMatchSnapshot();

  // 模拟第二次点击
  const rows2 = wrapper.find(Tooltip);
  rows2.at(3).simulate('click');
  jest.runAllTimers();

  // 这里可以添加第二次点击后的断言逻辑
  // ...

  // 恢复真实计时器,避免影响其他测试
  jest.useRealTimers();
  done();
});

2. 修复组件中的全局变量问题

把组件里的timer和prevent改成组件实例的属性,避免不同行或测试用例间的干扰:

// 在组件类的构造函数或useEffect(函数组件)中初始化
this.timer = null;
this.prevent = false;

// 修改事件处理函数
return {
  onClick: e => {
    this.timer = setTimeout(() => {
      if (!this.prevent && rowInfo && rowInfo.row) {
        this.setSelectedRecord(rowInfo.original);
        this.props.onSelect && this.props.onSelect(rowInfo.original[primaryKey]);
      }
      this.prevent = false;
    }, delay);
  },
  onDoubleClick: e => {
    clearTimeout(this.timer);
    this.prevent = true;
    if (rowInfo && rowInfo.row) {
      this.setSelectedRecord(null);
      this.props.onDoubleClick && this.props.onDoubleClick(rowInfo.original[primaryKey]);
    }
  }
};

3. 测试结束后清理计时器

为避免测试用例间的干扰,在每个测试结束后清除所有计时器:

afterEach(() => {
  jest.clearAllTimers();
});

为什么之前的方式行不通?

使用真实setTimeout时,测试会进入等待状态,Jest的测试环境可能在这段时间里清理了未被「持续引用」的组件,导致Enzyme的wrapper虽然还在变量中,但对应的DOM节点已被移除。改用fake timers后,所有计时器操作都是同步执行的,组件不会有被卸载的机会,测试流程完全可控。

内容的提问来源于stack exchange,提问作者herbertwest

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:24:16