使用 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 是合适的。
我正在尝试测试类似于以下内容的 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 是合适的。