// 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(); }); });