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

React Testing Library:如何Mock DOM Ref属性避免被覆盖?

问题描述

我正在测试一个带有overflow:hidden容器及子元素的React组件,该组件通过Effect Hook计算可见元素数量。尝试Mock容器ref的clientWidth值来测试功能,但Mock的useRef虽被调用,首次渲染后Mock值就被覆盖,测试失败,错误信息如下:

Unable to find an element with the text: 3 elements are visible!. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style

NaN elements are visible!

原因分析

直接MockReact.useRef返回自定义对象的方式无效,因为React在组件渲染完成后,会将真实的DOM元素赋值给ref.current,覆盖预先Mock的对象。此时Effect中读取的是真实DOM的clientWidth(测试环境中通常为0或无法正确获取),导致计算出NaN。

解决方案

方法一:Mock HTMLElement的clientWidth属性

由于clientWidth是DOM元素的只读属性,可通过重写HTMLElement.prototype.clientWidth的getter方法,让不同元素返回指定宽度值:

describe('OverflowingComponent', () => {
  it('显示3个可见元素(容器宽度150px)', () => {
    // 重写clientWidth的getter,区分容器和子元素
    Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
      get() {
        // 通过样式判断是否为容器元素
        if (this.style.display === 'flex') {
          return 150;
        }
        // 子元素宽度固定为50px
        return 50;
      },
      configurable: true, // 允许测试后清理Mock
    });

    render(<OverflowingComponent />);

    expect(screen.getByText('3 elements are visible!')).toBeInTheDocument();

    // 测试后清理Mock,避免影响其他用例
    delete HTMLElement.prototype.clientWidth;
  });
});

方法二:重构组件,通过props传入宽度值(推荐)

修改组件,让容器宽度和子元素宽度可通过props传入,测试时无需Mock DOM,直接传参即可,大幅提升可测试性:

重构后的组件代码

function OverflowingComponent({ 
  containerWidth = null, 
  childWidth = 50 
}: {
  containerWidth?: number | null;
  childWidth?: number;
}) {
  const divRef = React.useRef<HTMLDivElement>(null);
  const [visibleChildren, setVisibleChildren] = React.useState(0);

  React.useEffect(() => {
    if (divRef.current) {
      // 优先使用props传入的宽度,否则读取DOM值
      const width = containerWidth ?? divRef.current.clientWidth;
      const childElWidth = childWidth ?? divRef.current.children[0].clientWidth;
      
      if (width && childElWidth) {
        const maxVisibleChildren = Math.round(width / childElWidth);
        setVisibleChildren(
          divRef.current.children.length < maxVisibleChildren 
            ? divRef.current.children.length 
            : maxVisibleChildren
        );
      }
    }
  }, [containerWidth, childWidth]);

  return (
    <>
      <label>{visibleChildren} elements are visible!</label>
      <div ref={divRef} style={{ display: 'flex', overflow: 'hidden' }}>
        <div style={{ width: 50, flexShrink: 0 }}>foo</div>
        <div style={{ width: 50, flexShrink: 0 }}>bar</div>
        <div style={{ width: 50, flexShrink: 0 }}>boo</div>
        <div style={{ width: 50, flexShrink: 0 }}>far</div>
      </div>
    </>
  );
}

对应的测试代码

describe('OverflowingComponent', () => {
  it('显示3个可见元素(容器宽度150px)', () => {
    render(<OverflowingComponent containerWidth={150} childWidth={50} />);
    expect(screen.getByText('3 elements are visible!')).toBeInTheDocument();
  });
});

方法三:手动触发Effect并修改ref.current

若无法重构组件,可在渲染后手动修改ref的current属性,再重新执行Effect(此方法较为繁琐,仅作备选):

import { render, screen, act } from '@testing-library/react';
import React from 'react';

describe('OverflowingComponent', () => {
  it('显示3个可见元素(容器宽度150px)', () => {
    // 保存原始的useEffect
    const originalUseEffect = React.useEffect;
    let effectCallback: (() => void) | undefined;

    // Mock useEffect,捕获回调函数
    React.useEffect = jest.fn((callback) => {
      effectCallback = callback;
      return originalUseEffect(callback, []);
    });

    render(<OverflowingComponent />);

    // 构造Mock的ref对象
    const mockRef = {
      current: {
        clientWidth: 150,
        children: Array(4).fill({ clientWidth: 50 }),
      },
    };

    // 获取容器元素并替换其ref.current
    const container = screen.getByLabelText(/elements are visible!/).nextElementSibling as HTMLDivElement;
    (container as any)._internalInstance.ref.current = mockRef.current;

    // 手动执行Effect回调
    if (effectCallback) {
      act(() => {
        effectCallback();
      });
    }

    expect(screen.getByText('3 elements are visible!')).toBeInTheDocument();

    // 恢复原始的useEffect
    React.useEffect = originalUseEffect;
  });
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 13:21:24