test(web): 薄 Web 面加 RTL 组件测试 —— 补最大覆盖缺口 (P1)

完成度审计:web 端几乎零覆盖(仅3个 api 纯逻辑用例、无任何组件测试)——三前端里
最薄。补上 RTL 基建 + 入口流组件测试。

- 加 @testing-library/react+jest-dom+user-event,src/test/setup.ts,
  vitest setupFiles 接入。
- AuthPage.test.tsx(5 例,mock api 层):登录/注册态切换(名字字段显隐)、
  邮箱密码空时按钮禁用、登录成功回调 onAuthed 且带对参数、登录失败显示后端
  错误文案不回调、注册态走 authRegister 带名字。

web 测试 3→8。CI web 矩阵已跑 npm test,自动纳入关卡。
tsc + 三前端(desktop 68/admin 41/web 8)全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-18 12:28:57 +08:00
parent e29bc9a91e
commit a742d118ad
5 changed files with 329 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AuthPage } from "./AuthPage";
// 隔离 api 层:只测组件行为(态切换/校验/提交/报错),不打真网络。
vi.mock("../api", () => ({
authLogin: vi.fn(),
authRegister: vi.fn(),
}));
import { authLogin, authRegister } from "../api";
describe("AuthPage", () => {
beforeEach(() => vi.clearAllMocks());
it("默认登录态:无名字字段,切到注册后出现", async () => {
render(<AuthPage onAuthed={vi.fn()} />);
// 登录态无「名字」标签
expect(screen.queryByText(/名字/)).toBeNull();
await userEvent.click(screen.getByText(/还没有账户/));
expect(screen.getByText(/名字/)).toBeInTheDocument();
// 切回登录,名字字段消失
await userEvent.click(screen.getByText(/已有账户/));
expect(screen.queryByText(/名字/)).toBeNull();
});
it("邮箱或密码为空时提交按钮禁用", async () => {
render(<AuthPage onAuthed={vi.fn()} />);
const btn = screen.getByRole("button", { name: "登录" });
expect(btn).toBeDisabled();
await userEvent.type(screen.getByPlaceholderText(/you@example/), "a@b.com");
expect(btn).toBeDisabled(); // 还差密码
await userEvent.type(screen.getByPlaceholderText("••••••"), "pass123");
expect(btn).toBeEnabled();
});
it("登录成功回调 onAuthed", async () => {
const user = { id: "u1", email: "a@b.com" };
(authLogin as ReturnType<typeof vi.fn>).mockResolvedValue(user);
const onAuthed = vi.fn();
render(<AuthPage onAuthed={onAuthed} />);
await userEvent.type(screen.getByPlaceholderText(/you@example/), "a@b.com");
await userEvent.type(screen.getByPlaceholderText("••••••"), "pass123");
await userEvent.click(screen.getByRole("button", { name: "登录" }));
await waitFor(() => expect(onAuthed).toHaveBeenCalledWith(user));
expect(authLogin).toHaveBeenCalledWith("a@b.com", "pass123");
expect(authRegister).not.toHaveBeenCalled();
});
it("登录失败显示后端错误文案,不回调", async () => {
(authLogin as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("邮箱或密码不正确"));
const onAuthed = vi.fn();
render(<AuthPage onAuthed={onAuthed} />);
await userEvent.type(screen.getByPlaceholderText(/you@example/), "a@b.com");
await userEvent.type(screen.getByPlaceholderText("••••••"), "wrong");
await userEvent.click(screen.getByRole("button", { name: "登录" }));
await waitFor(() => expect(screen.getByText("邮箱或密码不正确")).toBeInTheDocument());
expect(onAuthed).not.toHaveBeenCalled();
});
it("注册态走 authRegister 并带上名字", async () => {
const user = { id: "u2", email: "c@d.com", name: "小明" };
(authRegister as ReturnType<typeof vi.fn>).mockResolvedValue(user);
const onAuthed = vi.fn();
render(<AuthPage onAuthed={onAuthed} />);
await userEvent.click(screen.getByText(/还没有账户/));
await userEvent.type(screen.getByPlaceholderText(/怎么称呼你/), "小明");
await userEvent.type(screen.getByPlaceholderText(/you@example/), "c@d.com");
await userEvent.type(screen.getByPlaceholderText("••••••"), "pass123");
await userEvent.click(screen.getByRole("button", { name: "注册" }));
await waitFor(() => expect(onAuthed).toHaveBeenCalledWith(user));
expect(authRegister).toHaveBeenCalledWith("c@d.com", "pass123", "小明");
});
});
+2
View File
@@ -0,0 +1,2 @@
// Vitest 全局初始化:jest-dom 断言(toBeInTheDocument 等)。
import "@testing-library/jest-dom/vitest";