如何用Jest测试接收HTMLElement的React自定义Hook?
React自定义Hook useScroll的测试问题:模拟HTMLElement与滚动逻辑验证
自定义Hook代码
import {useEffect, useState} from 'react'; // ... 省略其他代码 const isScrollerAtBottom = (elt: HTMLElement) => { return ( Math.floor(Number(elt?.scrollHeight) - Number(elt?.scrollTop)) > Number(elt?.clientHeight) ); }; export function useScroll(container: HTMLElement): useScrollProps { console.dir(container, {depth: 12}); const [displayScroller, setDisplayScroller] = useState(true); const [scrollerTop, setScrollerTop] = useState(0); const [scrollerLeft, setScrollerLeft] = useState(0); useEffect(() => { const containerDimensions = container ? container.getBoundingClientRect() : null; console.dir(containerDimensions, {depth: 12}); const left = (containerDimensions?.x || 0) + (containerDimensions?.width || 0) - 40; const top = containerDimensions?.height || 0; setScrollerTop(top); setScrollerLeft(left); setDisplayScroller(isScrollerAtBottom(container)); }); // ... 省略其他代码 const handleScroll = (event: React.UIEvent<HTMLElement>) => { const elt = event.target as HTMLElement; setDisplayScroller(isScrollerAtBottom(elt)); }; return { displayScroller, scrollerTop, scrollerLeft, handleScroll, }; }
第一次测试尝试及问题
测试代码:
import { JSXElement } from "@babel/types"; import React from 'react'; import { render, renderHook, screen } from "@testing-library/react"; import { useScroll, useScrollProps } from "../../hooks/useScroll"; describe("useScroll", () => { const container: HTMLElement = document.createElement("div"); container.style.width = "300px"; container.style.height = "800px"; container.style.display = "block"; const content: HTMLElement = document.createElement("p"); content.innerHTML = `... ... ...`; container.appendChild(content); test("should return scroller position and display flag", () => { const { result } = renderHook(() => useScroll(container)); console.log(result.current); }); });
问题:Hook无法获取容器正确尺寸,getBoundingClientRect返回全0:
{ x: 0, y: 0, bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 }
补充说明
希望使用真实模拟HTML元素验证Hook的计算逻辑,核心测试点:滚动到容器底部时,Hook能否正确返回true/false,不希望mockgetBoundingClientRect这类方法。
第二次测试尝试及问题
测试代码:
import { fireEvent, render, renderHook } from "@testing-library/react"; import { useScroll } from "../../hooks/useScroll"; describe("useScroll", () => { test("should return scroller position and display flag", () => { const { container } = render( <div style={{ width: "200px", height: "700px" }}></div> ); const { result } = renderHook(() => useScroll(container)); container.addEventListener("scroll", result.current.handleScroll); fireEvent.scroll(container, { target: { scrollTop: 0 } }); expect(result.current.displayScroller).toBeFalsy(); fireEvent.scroll(container, { target: { scrollTop: 700 } }); expect(result.current.displayScroller).toBeTruthy(); }); });
问题:最后一个断言expect(result.current.displayScroller).toBeTruthy();失败,结果为false。
解决思路
1. 解决模拟元素尺寸为0的问题
直接用document.createElement创建的元素未挂载到DOM树,浏览器不会计算布局尺寸。两种修复方式:
- 手动挂载到DOM:
describe("useScroll", () => { const container: HTMLElement = document.createElement("div"); container.style.width = "300px"; container.style.height = "800px"; container.style.display = "block"; container.style.overflow = "auto"; const content = document.createElement("div"); content.style.height = "1500px"; container.appendChild(content); // 测试前挂载到DOM beforeAll(() => { document.body.appendChild(container); }); // 测试后清理 afterAll(() => { document.body.removeChild(container); }); test("should get correct container dimensions", () => { const { result } = renderHook(() => useScroll(container)); // 此时可获取正确的尺寸值 }); }); - 使用
render渲染组件:render会自动将元素挂载到测试专用DOM容器,可直接获取正确尺寸。
2. 修复滚动逻辑与测试断言失败问题
(1)核心逻辑错误:isScrollerAtBottom判断反向
当前函数逻辑是未滚动到底部时返回true,和需求完全相反。修正为:
const isScrollerAtBottom = (elt: HTMLElement) => { if (!elt) return false; const { scrollHeight, scrollTop, clientHeight } = elt; // 滚动到底部时,scrollTop + clientHeight 约等于 scrollHeight(留1px容错避免浮点精度问题) return scrollTop + clientHeight >= scrollHeight - 1; };
(2)测试容器需具备可滚动条件
测试用的容器必须有足够长的内容,让scrollHeight > clientHeight,否则无法触发滚动到底部的状态:
const { container } = render( <div style={{ width: "200px", height: "700px", overflow: "auto" }}> {/* 内容高度大于容器高度,形成可滚动区域 */} <div style={{ height: "1500px" }}>测试滚动内容</div> </div> );
(3)等待异步状态更新
Hook的状态更新是异步的,触发滚动事件后需用waitFor等待状态变化再断言:
import { fireEvent, render, renderHook, waitFor } from "@testing-library/react"; test("should toggle displayScroller correctly when scrolling", async () => { const { container } = render( <div style={{ width: "200px", height: "700px", overflow: "auto" }}> <div style={{ height: "1500px" }}>测试滚动内容</div> </div> ); const { result } = renderHook(() => useScroll(container)); // 初始状态:未滚动到底部,displayScroller为false await waitFor(() => { expect(result.current.displayScroller).toBeFalsy(); }); // 滚动到底部:直接设置scrollTop到最大值,再触发滚动事件 container.scrollTop = container.scrollHeight - container.clientHeight; fireEvent.scroll(container); // 等待状态更新后断言 await waitFor(() => { expect(result.current.displayScroller).toBeTruthy(); }); });
(4)避免模拟scrollTop的不准确
直接修改容器的scrollTop属性(贴近真实场景),再触发滚动事件,比通过fireEvent传递模拟target更可靠。
内容的提问来源于stack exchange,提问作者manou
相关产品推荐
相关产品推荐

