// ChildDetailPanel 组件单测
// 依据:02-architecture-design.md §15 组件设计
// 覆盖:Tab 导航 / Tab 切换 / overview / homework / grades / exams / schedule 五个视图
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ChildDetailPanel } from "./ChildDetailPanel";
import type { ChildDetail } from "@/types";
// mock next/link 为普通 ,便于断言
vi.mock("next/link", () => ({
default: ({
href,
children,
...rest
}: {
href: string;
children: React.ReactNode;
[key: string]: unknown;
}) => (
{children}
),
}));
const mockChildDetail: ChildDetail = {
childId: "student-001",
basicInfo: {
name: "张小明",
avatar: "https://example.com/avatar.png",
grade: "grade.7",
className: "初一(1)班",
schoolName: "实验中学",
relation: "父亲",
},
todaySchedule: [
{
id: "sch-today-1",
dayOfWeek: 1,
period: 1,
subject: "数学",
teacherName: "王老师",
classroom: "初一(1)班教室",
startTime: "08:00",
endTime: "08:45",
},
{
id: "sch-today-2",
dayOfWeek: 1,
period: 2,
subject: "语文",
teacherName: "李老师",
classroom: "初一(1)班教室",
startTime: "08:55",
endTime: "09:40",
},
],
weeklySchedule: [
{
id: "sch-w-1",
dayOfWeek: 1,
period: 1,
subject: "数学",
teacherName: "王老师",
classroom: "初一(1)班教室",
startTime: "08:00",
endTime: "08:45",
},
{
id: "sch-w-2",
dayOfWeek: 2,
period: 1,
subject: "英语",
teacherName: "张老师",
classroom: "初一(1)班教室",
startTime: "08:00",
endTime: "08:45",
},
],
homeworkSummary: {
pendingCount: 3,
overdueCount: 1,
submittedCount: 5,
gradedCount: 8,
},
gradeSummary: {
avgScore: 90,
classRank: 5,
classSize: 45,
trend: "up",
},
examResults: {
upcoming: 2,
completed: 4,
avgScore: 88,
},
};
describe("ChildDetailPanel Tab 导航", () => {
it("渲染 5 个 Tab(概览/作业/成绩/考试/课表)", () => {
render(
,
);
expect(screen.getByRole("tablist", { name: "子女详情视图" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "概览" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "作业" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "成绩" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "考试" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "课表" })).toBeInTheDocument();
});
it("activeTab=overview 时概览 tab 选中", () => {
render(
,
);
expect(screen.getByRole("tab", { name: "概览" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "作业" })).toHaveAttribute("aria-selected", "false");
});
it("点击 Tab 调用 onTabChange", async () => {
const user = userEvent.setup();
const onTabChange = vi.fn();
render(
,
);
await user.click(screen.getByRole("tab", { name: "作业" }));
expect(onTabChange).toHaveBeenCalledWith("homework");
});
it("点击成绩 Tab 调用 onTabChange 传 grades", async () => {
const user = userEvent.setup();
const onTabChange = vi.fn();
render(
,
);
await user.click(screen.getByRole("tab", { name: "成绩" }));
expect(onTabChange).toHaveBeenCalledWith("grades");
});
});
describe("ChildDetailPanel overview Tab", () => {
it("渲染三个 Section(作业摘要/成绩摘要/今日课表)", () => {
render(
,
);
expect(screen.getByText("作业摘要")).toBeInTheDocument();
expect(screen.getByText("成绩摘要")).toBeInTheDocument();
expect(screen.getByText("今日课表")).toBeInTheDocument();
});
it("渲染作业摘要数据(待完成/已逾期/已提交/已评分)", () => {
render(
,
);
expect(screen.getByText("待完成")).toBeInTheDocument();
expect(screen.getByText("已逾期")).toBeInTheDocument();
expect(screen.getByText("已提交")).toBeInTheDocument();
expect(screen.getByText("已评分")).toBeInTheDocument();
// 验证数值(3/1/5/8)
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getByText("5")).toBeInTheDocument();
expect(screen.getByText("8")).toBeInTheDocument();
});
it("渲染成绩摘要数据(平均分/班级排名/趋势)", () => {
render(
,
);
expect(screen.getByText("平均分")).toBeInTheDocument();
expect(screen.getByText("班级排名")).toBeInTheDocument();
expect(screen.getByText("趋势")).toBeInTheDocument();
// 趋势 up → "↑ 上升"
expect(screen.getByText("↑ 上升")).toBeInTheDocument();
});
it("渲染今日课表数据", () => {
render(
,
);
expect(screen.getByText("数学")).toBeInTheDocument();
expect(screen.getByText("语文")).toBeInTheDocument();
// 教师姓名与教室渲染在同一 span(如 "王老师 · 初一(1)班教室"),用 regex 验证
expect(screen.getByText(/王老师/)).toBeInTheDocument();
expect(screen.getByText(/李老师/)).toBeInTheDocument();
});
});
describe("ChildDetailPanel homework Tab", () => {
it("渲染作业摘要 Section", () => {
render(
,
);
expect(screen.getByText("作业摘要")).toBeInTheDocument();
});
it("渲染查看全部作业链接", () => {
render(
,
);
const link = screen.getByRole("link", { name: /查看全部作业/ });
expect(link).toHaveAttribute("href", "/parent/homework");
});
});
describe("ChildDetailPanel grades Tab", () => {
it("渲染成绩摘要 Section", () => {
render(
,
);
expect(screen.getByText("成绩摘要")).toBeInTheDocument();
});
it("渲染查看全部成绩链接", () => {
render(
,
);
const link = screen.getByRole("link", { name: /查看全部成绩/ });
expect(link).toHaveAttribute("href", "/parent/grades");
});
});
describe("ChildDetailPanel exams Tab", () => {
it("渲染考试摘要 Section", () => {
render(
,
);
expect(screen.getByText("考试摘要")).toBeInTheDocument();
});
it("渲染考试统计数据(待考/已完成/平均分)", () => {
render(
,
);
expect(screen.getByText("待考")).toBeInTheDocument();
expect(screen.getByText("已完成")).toBeInTheDocument();
expect(screen.getByText("平均分")).toBeInTheDocument();
});
it("渲染查看全部考试链接", () => {
render(
,
);
const link = screen.getByRole("link", { name: /查看全部考试/ });
expect(link).toHaveAttribute("href", "/parent/exams");
});
});
describe("ChildDetailPanel schedule Tab", () => {
it("渲染今日课表 Section", () => {
render(
,
);
expect(screen.getByText(/今日课表/)).toBeInTheDocument();
});
it("渲染周课表 Section", () => {
render(
,
);
expect(screen.getByText("周课表")).toBeInTheDocument();
});
it("渲染周课表分组(周一/周二)", () => {
render(
,
);
// 周课表含 dayOfWeek 1(周一)和 2(周二)
expect(screen.getByText("周一")).toBeInTheDocument();
expect(screen.getByText("周二")).toBeInTheDocument();
});
});
describe("ChildDetailPanel 空态", () => {
it("今日课表为空时显示今日无课程", () => {
const emptySchedule: ChildDetail = {
...mockChildDetail,
todaySchedule: [],
};
render(
,
);
expect(screen.getByText("今日无课程")).toBeInTheDocument();
});
it("周课表为空时显示暂无周课表", () => {
const emptyWeekly: ChildDetail = {
...mockChildDetail,
weeklySchedule: [],
};
render(
,
);
expect(screen.getByText("暂无周课表")).toBeInTheDocument();
});
});
describe("ChildDetailPanel 趋势显示", () => {
it("gradeSummary.trend=down 时显示 ↓ 下降", () => {
const downTrend: ChildDetail = {
...mockChildDetail,
gradeSummary: {
...mockChildDetail.gradeSummary,
trend: "down",
},
};
render(
,
);
expect(screen.getByText("↓ 下降")).toBeInTheDocument();
});
it("gradeSummary.trend=stable 时显示 → 平稳", () => {
const stableTrend: ChildDetail = {
...mockChildDetail,
gradeSummary: {
...mockChildDetail.gradeSummary,
trend: "stable",
},
};
render(
,
);
expect(screen.getByText("→ 平稳")).toBeInTheDocument();
});
});