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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 16:25:27