React Testing Library mock offsetWidth/Left 修复resize测试用例问题
现有测试的问题点
- 执行顺序完全颠倒:
resize方法只有在isResizing为true时才会执行宽度计算,现有代码先调用resize再触发enableResize,第一次调用resize时状态还是未拖拽,根本不会走计算逻辑。 - DOM属性mock时机错误:
offsetWidth、offsetLeft是resize执行时实时读取的,现有代码在第一次调用resize之后才mock这两个值,mock完全不生效。 - 事件触发完全不对:Hook内部监听的是
document上的mousemove事件触发resize,和window的resize事件没有任何关系,window.dispatchEvent(new Event('resize'))这行代码完全不会触发宽度计算。 - 手动调用
setWidth属于无效测试:核心逻辑是拖拽时自动计算宽度,手动调用setWidth塞值完全绕过了resize的计算逻辑,等于没测。 - 没有做mock还原:直接修改document.body的原生属性,会污染全局测试环境,导致其他用例随机失败。
- mock写法错误:
offsetWidth是DOM元素的只读属性,直接defineProperty只传value的话默认不可写,赋值会静默失败。
可行测试方案
按照真实用户拖拽路径设计用例,提前mock DOM属性并做好还原,通过真实事件派发触发逻辑,不要手动篡改Hook内部状态。
完整测试代码示例:
import { renderHook, act } from '@testing-library/react'; import useResize from './useResize'; // 测试默认参数 const defaultProps = { minWidth: 300, maxWidth: 1200, }; // 保存DOM原生属性,测试后还原避免环境污染 const originalOffsetWidth = Object.getOwnPropertyDescriptor( HTMLElement.prototype, 'offsetWidth' ); const originalOffsetLeft = Object.getOwnPropertyDescriptor( HTMLElement.prototype, 'offsetLeft' ); describe('useResize 宽度拖拽Hook', () => { // 每个用例执行前重置DOM属性mock beforeEach(() => { Object.defineProperty(document.body, 'offsetWidth', { writable: true, configurable: true, value: 1536, // 模拟页面可视区宽度1536px }); Object.defineProperty(document.body, 'offsetLeft', { writable: true, configurable: true, value: 0, }); }); // 每个用例执行后还原原生属性 afterEach(() => { if (originalOffsetWidth) { Object.defineProperty(document.body, 'offsetWidth', originalOffsetWidth); } if (originalOffsetLeft) { Object.defineProperty(document.body, 'offsetLeft', originalOffsetLeft); } }); it('拖拽过程中应该根据鼠标位置正确计算更新宽度', () => { const { result } = renderHook(() => useResize(defaultProps)); // 模拟鼠标按下拖拽条,进入调整状态 act(() => { result.current.enableResize(); }); expect(result.current.isResizing).toBe(true); // 模拟鼠标移动到clientX=1000位置,派发mousemove事件触发计算 const mouseMoveEvent = new MouseEvent('mousemove', { clientX: 1000, bubbles: true, cancelable: true, }); act(() => { document.dispatchEvent(mouseMoveEvent); }); // 计算预期值:1536 - (1000 - 0) = 536,在300~1200区间内,应该更新 expect(result.current.width).toBe(536); }); it('计算宽度小于最小宽度时不更新', () => { const { result } = renderHook(() => useResize(defaultProps)); act(() => { result.current.enableResize(); }); // 模拟鼠标移动到最右侧,计算宽度=1536-1300=236 < 300,保持最小宽度 const mouseMoveEvent = new MouseEvent('mousemove', { clientX: 1300 }); act(() => { document.dispatchEvent(mouseMoveEvent); }); expect(result.current.width).toBe(defaultProps.minWidth); }); it('计算宽度大于最大宽度时不更新', () => { const { result } = renderHook(() => useResize(defaultProps)); act(() => { result.current.enableResize(); }); // 模拟鼠标移动到最左侧,计算宽度=1536-200=1336 > 1200,保持最小宽度 const mouseMoveEvent = new MouseEvent('mousemove', { clientX: 200 }); act(() => { document.dispatchEvent(mouseMoveEvent); }); expect(result.current.width).toBe(defaultProps.minWidth); }); it('鼠标松开后退出拖拽状态,不再响应鼠标移动更新宽度', () => { const { result } = renderHook(() => useResize(defaultProps)); act(() => { result.current.enableResize(); }); // 模拟鼠标松开 act(() => { document.dispatchEvent(new MouseEvent('mouseup')); }); expect(result.current.isResizing).toBe(false); // 再次移动鼠标,宽度不应该变化 const mouseMoveEvent = new MouseEvent('mousemove', { clientX: 1000 }); act(() => { document.dispatchEvent(mouseMoveEvent); }); expect(result.current.width).toBe(defaultProps.minWidth); }); });
关键注意点
- mock DOM只读属性时必须配置
writable: true和configurable: true,否则原生属性无法修改,mock会静默失败。 - 不要手动调用Hook返回的
resize方法测试,要通过派发真实事件触发逻辑,才能覆盖事件绑定、解绑、依赖更新等边界场景。 - 所有修改全局对象(document、window)的操作必须在测试结束后还原,避免污染其他用例的执行环境。
- 测试流程要和真实用户操作路径完全一致:按下拖拽条→移动鼠标→松开鼠标,不要跳步或者手动调用内部方法篡改状态。
内容的提问来源于stack exchange,提问作者user901916
相关产品推荐
相关产品推荐

