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

如何用Jest和Testing Library测试addEventListener?自定义Hook测试失败

问题

我在React中写了一个自定义Hook useDynamicDimensions,用来监听元素尺寸变化并返回宽高:

export function useDynamicDimensions(element) {
  const [ width, setWidth ] = useState(element?.innerWidth);
  const [ height, setHeight ] = useState(element?.innerHeight);

  useEffect(() => {
    const handleWindowResize = () => {
      setWidth(element?.innerWidth);
      setHeight(element?.innerHeight);
    };

    element?.addEventListener('resize', handleWindowResize);

    return () => element?.removeEventListener('resize', handleWindowResize);
  }, []);

  return { width, height };
}

用Jest和Testing Library写了测试用例:

it('updates dimensions on window resize', () => {
  const initialWidth = 100;
  const initialHeight = 200;
  const newWidth = 250;
  const newHeight = 300;

  const element = {
    innerWidth: initialWidth,
    innerHeight: initialHeight,
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
  };

  const { result } = renderHook(() => useDynamicDimensions(element));

  expect(result.current.width).toBe(initialWidth);
  expect(result.current.height).toBe(initialHeight);

  act(() => {
    element.innerWidth = newWidth;
    element.innerHeight = newHeight;
    global.dispatchEvent(new Event('resize'));
  });

  expect(result.current.width).toBe(250);
});

但测试失败,报错:

expect(received).toBe(expected)

Expected: 250
Received: 100

错误原因及修复方案

核心问题

  1. 闭包陷阱导致读取旧值:useEffect的依赖数组为空,handleWindowResize函数会捕获初始的element引用,后续修改element的innerWidth/innerHeight时,函数里读取的还是旧的属性值,不会触发状态更新。
  2. 事件触发对象不匹配:Hook监听的是传入element的resize事件,但测试里触发的是global(window)的resize事件,两者不是同一个对象,回调不会执行。
  3. Mock事件未实际绑定回调:测试中用jest.fn()模拟了addEventListener,需要手动获取并触发监听的回调,而不是dispatch全局事件。

修复Hook

方案一:将element加入useEffect依赖数组,确保依赖变化时重新绑定事件:

export function useDynamicDimensions(element) {
  const [ width, setWidth ] = useState(element?.innerWidth);
  const [ height, setHeight ] = useState(element?.innerHeight);

  useEffect(() => {
    if (!element) return;

    const handleResize = () => {
      setWidth(element.innerWidth);
      setHeight(element.innerHeight);
    };

    element.addEventListener('resize', handleResize);

    return () => element.removeEventListener('resize', handleResize);
  }, [element]); // 添加element到依赖数组

  return { width, height };
}

方案二:用useRef保存element,避免频繁解绑绑定(适合element频繁变化的场景):

export function useDynamicDimensions(element) {
  const [ width, setWidth ] = useState(element?.innerWidth);
  const [ height, setHeight ] = useState(element?.innerHeight);
  const elementRef = useRef(element);

  // 实时更新ref指向最新的element
  useEffect(() => {
    elementRef.current = element;
  }, [element]);

  useEffect(() => {
    const handleResize = () => {
      setWidth(elementRef.current?.innerWidth);
      setHeight(elementRef.current?.innerHeight);
    };

    const currentElement = elementRef.current;
    if (!currentElement) return;

    currentElement.addEventListener('resize', handleResize);

    return () => currentElement.removeEventListener('resize', handleResize);
  }, []);

  return { width, height };
}

修复测试用例

获取mock的addEventListener中的回调函数,手动触发它:

it('updates dimensions on element resize', () => {
  const initialWidth = 100;
  const initialHeight = 200;
  const newWidth = 250;
  const newHeight = 300;

  const element = {
    innerWidth: initialWidth,
    innerHeight: initialHeight,
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
  };

  const { result } = renderHook(() => useDynamicDimensions(element));

  expect(result.current.width).toBe(initialWidth);
  expect(result.current.height).toBe(initialHeight);

  // 获取监听的resize回调函数
  const handleResize = element.addEventListener.mock.calls[0][1];

  act(() => {
    element.innerWidth = newWidth;
    element.innerHeight = newHeight;
    // 触发element的resize回调
    handleResize();
  });

  expect(result.current.width).toBe(newWidth);
  expect(result.current.height).toBe(newHeight);
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 20:55:28