如何使用React Testing Library测试登录按钮点击后错误消息的出现及内容正确性
我之前也碰到过类似的测试问题,咱们一步步拆解你的代码里的问题,然后给出可行的解决方案:
问题根源分析
你的测试出现的几个问题,本质上都是对React Testing Library的查询方法和waitFor的用法理解不到位:
第一个写法测试无差别通过:
你最初的waitFor回调里只调用了screen.findAllByText,但既没有await这个异步查询,也没有添加任何断言。waitFor的作用是等待回调中的代码不再抛出错误,但你这里没有断言,不管查询是否成功,回调都不会抛出错误,所以测试直接通过了。第二个写法的类型错误:
你写expect(getByText("...")).toBeInTheDocument()时,没有从screen调用getByText,也没有传入容器元素。getByText需要知道在哪个DOM容器里查找元素,直接调用的话会导致参数错误,所以抛出了容器类型不匹配的报错。找不到错误文本的问题:
报错提示文本可能被多个元素拆分,这说明页面上的错误消息不是一个单独的元素包含完整文本,可能是多个元素拼接而成的,直接用精确字符串匹配会失败。
正确的解决方案
方案1:用findByText直接等待元素(推荐)
findByText本身就是异步查询方法,内部已经封装了waitFor的逻辑,会自动等待元素出现,不需要额外嵌套waitFor:
// 点击按钮后 const errorMessage = await screen.findByText("Email oder Passwort sind falsch."); expect(errorMessage).toBeInTheDocument();
方案2:用waitFor配合同步查询和断言
如果你需要更精细的等待控制,可以用waitFor包裹同步查询的断言。注意一定要用screen.getByText(确保从screen调用,指定查找容器):
await waitFor(() => { expect(screen.getByText("Email oder Passwort sind falsch.")).toBeInTheDocument(); });
处理文本被拆分的情况
如果错误文本被多个DOM元素拆分(比如<span>Email oder</span> <span>Passwort sind falsch.</span>),可以用以下两种方式匹配:
- 正则表达式匹配:
await screen.findByText(/Email oder Passwort sind falsch\./); - 函数匹配器:
await screen.findByText((content) => content.trim() === "Email oder Passwort sind falsch."); // 或者更宽松的包含匹配 await screen.findByText((content) => content.includes("Email oder Passwort sind falsch."));
额外的优化建议
- 不要硬编码错误文本,建议用你的i18n常量(比如
tr.LOGIN.ERROR_INVALID_CREDENTIALS之类的,假设你的i18n里有这个键),这样测试会更易维护:await screen.findByText(tr.LOGIN.ERROR_INVALID_CREDENTIALS); - 点击按钮后,如果你想先验证加载状态,可以先等待按钮文本变成加载提示:
fireEvent.click(btn); // 先验证加载状态 await screen.findByText(tr.LOGIN.BTN_LOADING); // 再等待错误消息 const errorMessage = await screen.findByText(tr.LOGIN.ERROR_INVALID_CREDENTIALS); expect(errorMessage).toBeInTheDocument();
修正后的完整测试代码示例
import { ROUTES } from "@company/app/features"; import { fireEvent, MockedRouter, render, screen, waitFor, } from "@company/test-utils"; import { tr } from "@company/utils/i18n"; import { NextRouter } from "next/router"; import { LoginPage } from "./Login.stories"; describe("Login", () => { it("shows error message on failed login", async () => { const router: Partial<NextRouter> = { pathname: "/login", push: jest.fn(), }; render( <MockedRouter router={router}> <LoginPage /> </MockedRouter> ); // 等待页面加载完成 await screen.findByText(tr.LOGIN.TITLE); // 输入错误凭据 const email = screen.getByPlaceholderText(tr.LOGIN.PLACEHOLDER_EMAIL); const password = screen.getByPlaceholderText(tr.LOGIN.PLACEHOLDER_PASSWORD); fireEvent.input(email, { target: { value: "info@abc.de" } }); fireEvent.input(password, { target: { value: "wrong" } }); // 点击登录按钮 const btn = screen.getByRole("button", { name: tr.LOGIN.BTN_LOGIN }); // 这里也用i18n常量更可靠 fireEvent.click(btn); // 等待错误消息出现并验证 const errorMessage = await screen.findByText("Email oder Passwort sind falsch."); expect(errorMessage).toBeInTheDocument(); }); });
内容的提问来源于stack exchange,提问作者sel-nef

