feat(admin-portal): 完成参考项目差距闭环 - 4 批次补齐 + Vitest 测试

差距分析闭环(workline §7-§8):
- P0:审计子模块 3 页 + 学校组织 7 页
- P1:系统设置 4 Card + 公告管理 + 邀请码管理
- P2:文件管理 + AI 配置
- P6:5 测试文件 / 69 用例全过

共享基础设施:
- permissions +11 权限点 +14 路由
- view-models +20 接口
- i18n +100 key
- graphql-client +25 operation
- fixtures/handlers +15 mock +20 handler

质量:typecheck + lint 零错误,vitest 69/69 通过,arch.db 已更新
This commit is contained in:
SpecialX
2026-07-13 13:05:29 +08:00
parent fad6e43b47
commit 7cf9aec20e
40 changed files with 6707 additions and 7 deletions

View File

@@ -0,0 +1,267 @@
/**
* 通用 UI 组件测试
*/
import { describe, expect, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import {
PaperCard,
PageHeader,
Button,
Input,
Select,
Badge,
Table,
TableRow,
TableCell,
EmptyState,
LoadingState,
ErrorState,
Pagination,
} from "./ui";
describe("PaperCard", () => {
it("渲染 children", () => {
render(<PaperCard></PaperCard>);
expect(screen.getByText("内容")).toBeInTheDocument();
});
it("支持自定义 className", () => {
const { container } = render(
<PaperCard className="custom-class"></PaperCard>,
);
expect(container.firstChild).toHaveClass("custom-class");
});
});
describe("PageHeader", () => {
it("渲染标题", () => {
render(<PageHeader title="用户管理" />);
expect(screen.getByText("用户管理")).toBeInTheDocument();
});
it("渲染描述", () => {
render(<PageHeader title="用户管理" description="管理所有用户" />);
expect(screen.getByText("管理所有用户")).toBeInTheDocument();
});
it("渲染操作按钮", () => {
render(<PageHeader title="用户管理" actions={<button></button>} />);
expect(screen.getByText("新建")).toBeInTheDocument();
});
});
describe("Button", () => {
it("默认 secondary variant", () => {
render(<Button></Button>);
expect(screen.getByText("点击")).toBeInTheDocument();
});
it("点击触发 onClick", () => {
const onClick = vi.fn();
render(<Button onClick={onClick}></Button>);
fireEvent.click(screen.getByText("点击"));
expect(onClick).toHaveBeenCalledTimes(1);
});
it("disabled 时不触发 onClick", () => {
const onClick = vi.fn();
render(
<Button onClick={onClick} disabled>
</Button>,
);
fireEvent.click(screen.getByText("点击"));
expect(onClick).not.toHaveBeenCalled();
});
it("支持所有 variant", () => {
const { rerender } = render(<Button variant="primary"></Button>);
expect(screen.getByText("主按钮")).toBeInTheDocument();
rerender(<Button variant="danger"></Button>);
expect(screen.getByText("危险按钮")).toBeInTheDocument();
rerender(<Button variant="ghost"></Button>);
expect(screen.getByText("幽灵按钮")).toBeInTheDocument();
});
});
describe("Input", () => {
it("渲染输入框并支持输入", () => {
render(<Input placeholder="请输入" />);
const input = screen.getByPlaceholderText("请输入");
fireEvent.change(input, { target: { value: "测试内容" } });
expect(input).toHaveValue("测试内容");
});
});
describe("Select", () => {
it("渲染下拉框和选项", () => {
render(
<Select>
<option value="a">A</option>
<option value="b">B</option>
</Select>,
);
expect(screen.getByText("选项A")).toBeInTheDocument();
expect(screen.getByText("选项B")).toBeInTheDocument();
});
});
describe("Badge", () => {
it("渲染 active 状态标签", () => {
render(<Badge status="active" />);
expect(screen.getByText("启用")).toBeInTheDocument();
});
it("渲染 locked 状态标签", () => {
render(<Badge status="locked" />);
expect(screen.getByText("锁定")).toBeInTheDocument();
});
it("渲染公告相关状态", () => {
const { rerender } = render(<Badge status="draft" />);
expect(screen.getByText("草稿")).toBeInTheDocument();
rerender(<Badge status="published" />);
expect(screen.getByText("已发布")).toBeInTheDocument();
rerender(<Badge status="archived" />);
expect(screen.getByText("已归档")).toBeInTheDocument();
});
it("渲染邀请码状态", () => {
const { rerender } = render(<Badge status="unused" />);
expect(screen.getByText("未使用")).toBeInTheDocument();
rerender(<Badge status="used" />);
expect(screen.getByText("已使用")).toBeInTheDocument();
rerender(<Badge status="expired" />);
expect(screen.getByText("已过期")).toBeInTheDocument();
});
it("未知状态显示原始值", () => {
render(<Badge status="unknown-status" />);
expect(screen.getByText("unknown-status")).toBeInTheDocument();
});
});
describe("Table", () => {
it("渲染表头和数据行", () => {
render(
<Table headers={["姓名", "邮箱"]}>
<TableRow>
<TableCell></TableCell>
<TableCell>zhangsan@test.com</TableCell>
</TableRow>
</Table>,
);
expect(screen.getByText("姓名")).toBeInTheDocument();
expect(screen.getByText("邮箱")).toBeInTheDocument();
expect(screen.getByText("张三")).toBeInTheDocument();
expect(screen.getByText("zhangsan@test.com")).toBeInTheDocument();
});
});
describe("EmptyState", () => {
it("渲染空状态消息", () => {
render(<EmptyState message="暂无数据" />);
expect(screen.getByText("暂无数据")).toBeInTheDocument();
});
});
describe("LoadingState", () => {
it("渲染加载中文案", () => {
render(<LoadingState />);
expect(screen.getByText("加载中...")).toBeInTheDocument();
});
});
describe("ErrorState", () => {
it("渲染错误消息", () => {
render(<ErrorState message="加载失败" />);
expect(screen.getByText("加载失败")).toBeInTheDocument();
});
it("提供 onRetry 时渲染重试按钮", () => {
const onRetry = vi.fn();
render(<ErrorState message="加载失败" onRetry={onRetry} />);
const retryBtn = screen.getByText("重试");
fireEvent.click(retryBtn);
expect(onRetry).toHaveBeenCalledTimes(1);
});
it("不提供 onRetry 时不渲染重试按钮", () => {
render(<ErrorState message="加载失败" />);
expect(screen.queryByText("重试")).not.toBeInTheDocument();
});
});
describe("Pagination", () => {
it("渲染总数和页码", () => {
render(
<Pagination
page={2}
pageSize={20}
total={45}
hasNext={true}
onPageChange={() => {}}
/>,
);
expect(screen.getByText(/共 45 条/)).toBeInTheDocument();
expect(screen.getByText(/第 2\/3 页/)).toBeInTheDocument();
});
it("第一页禁用上一页按钮", () => {
render(
<Pagination
page={1}
pageSize={20}
total={45}
hasNext={true}
onPageChange={() => {}}
/>,
);
expect(screen.getByText("上一页")).toBeDisabled();
expect(screen.getByText("下一页")).not.toBeDisabled();
});
it("最后一页禁用下一页按钮", () => {
render(
<Pagination
page={3}
pageSize={20}
total={45}
hasNext={false}
onPageChange={() => {}}
/>,
);
expect(screen.getByText("下一页")).toBeDisabled();
expect(screen.getByText("上一页")).not.toBeDisabled();
});
it("点击上一页/下一页触发 onPageChange", () => {
const onPageChange = vi.fn();
render(
<Pagination
page={2}
pageSize={20}
total={45}
hasNext={true}
onPageChange={onPageChange}
/>,
);
fireEvent.click(screen.getByText("上一页"));
expect(onPageChange).toHaveBeenCalledWith(1);
fireEvent.click(screen.getByText("下一页"));
expect(onPageChange).toHaveBeenCalledWith(3);
});
it("total 为 0 时显示第 1/1 页", () => {
render(
<Pagination
page={1}
pageSize={20}
total={0}
hasNext={false}
onPageChange={() => {}}
/>,
);
expect(screen.getByText(/第 1\/1 页/)).toBeInTheDocument();
});
});