使用 React 测试库测试文档监听器

Test document listener with React Testing Library

我正在尝试测试类似于以下内容的 React 组件:

import React, { useState, useEffect, useRef } from "react";

export default function Tooltip({ children }) {
  const [open, setOpen] = useState(false);
  const wrapperRef = useRef(null);

  const handleClickOutside = (event) => {
    if (
      open &&
      wrapperRef.current &&
      !wrapperRef.current.contains(event.target)
    ) {
      setOpen(false);
    }
  };

  useEffect(() => {
    document.addEventListener("click", handleClickOutside);

    return () => {
      document.removeEventListener("click", handleClickOutside);
    };
  });

  const className = `tooltip-wrapper${(open && " open") || ""}`;

  return (
    <span ref={wrapperRef} className={className}>
      <button type="button" onClick={() => setOpen(!open)} />
      <span>{children}</span>
      <br />
      <span>DEBUG: className is {className}</span>
    </span>
  );
}

单击工具提示按钮会将状态更改为打开(更改类名),再次单击组件外部会将其更改为关闭。

组件有效(具有适当的样式),并且所有 React 测试库(具有用户事件)测试都有效,除了单击外部。

  it("should close the tooltip on click outside", () => {
    // Arrange
    render(
      <div>
        <p>outside</p>
        <Tooltip>content</Tooltip>
      </div>
    );

    const button = screen.getByRole("button");
    userEvent.click(button);

    // Temporary assertion - passes
    expect(button.parentElement).toHaveClass("open");

    // Act
    const outside = screen.getByText("outside");

    // Gives should be wrapped into act(...) warning otherwise
    act(() => {
      userEvent.click(outside);
    });

    // Assert
    expect(button.parentElement).not.toHaveClass("open"); // FAILS
  });

我不明白为什么我必须在 act 中包装点击事件 - React 测试库通常不需要这样做。

我也不明白为什么最终断言失败了。单击处理程序被调用两次,但 open 两次都是 true

有很多关于 React 合成事件限制的文章,但我不清楚如何将所有这些放在一起。

我终于成功了。

  it("should close the tooltip on click outside", async () => {
    // Arrange
    render(
      <div>
        <p data-testid="outside">outside</p>
        <Tooltip>content</Tooltip>
      </div>
    );

    const button = screen.getByRole("button");
    userEvent.click(button);

    // Verify initial state
    expect(button.parentElement).toHaveClass("open");

    const outside = screen.getByTestId("outside");

    // Act
    userEvent.click(outside);

    // Assert
    await waitFor(() => expect(button.parentElement).not.toHaveClass("open"));
  });

关键似乎是确保所有 activity 在测试结束前完成。

假设一个测试触发了一个点击事件,该事件又设置了状态。设置状态通常会导致重新渲染,您的测试需要等待它发生。通常,您可以通过等待显示新状态来做到这一点。

在这种特殊情况下 waitFor 是合适的。