## 变更内容 ### P0 高优先级新页面 - /parent/leave: 请假表单(react-hook-form + Zod) + 历史列表(状态筛选) - /parent/grades/report-card: 报告卡(学年/学期筛选 + 打印 + 教师评语) - /parent/children/[studentId]: 子女详情聚合页(5 Tab: overview/homework/grades/exams/schedule) ### P1 中优先级新页面 - /parent/error-book: 错题本(5 项统计 + Top 错题 + 薄弱知识点) - /parent/diagnostic: 诊断报告(掌握度摘要 + 已发布诊断报告) - /parent/practice: 练习统计(4 项统计 + 练习历史) ### P2 低优先级新页面 - /parent/course-plans + [id]: 课程计划列表 + 详情(章节列表) - /parent/lesson-plans + [planId]/view: 备课列表(学科筛选) + 只读详情 - /parent/elective: 选修课(分类色点 + 状态徽标) ### P3 现有页面增强 - /parent/dashboard: ParentAttentionBanner + 多子女卡片网格 + 趋势图标 + 逾期高亮 - /parent/attendance: AttendanceRateCard + AttendanceWarningBanner + 月份导航 - /parent/grades: GrowthArchiveChart + ExportGradesButton + 班级均对比线 ### 基础设施 - types/index.ts: +20 新类型(LeaveRequest/ReportCard/ErrorBookStats/DiagnosticReport/PracticeStats/CoursePlan/LessonPlan/ElectiveSelection/ChildDetail/ScheduleItem 等) - operations.ts: +19 GraphQL operations(17 query + 2 mutation) - fixtures.ts: +18 mock 数据集 - handlers.ts: +19 MSW GraphQL handler ### 质量校验 - typecheck: 0 错误 - lint: 0 错误 - test: 71 文件 / 763 测试全部通过(从 413 增至 763, +350 测试) ### 文档 - workline.md: 新增 §7 参考项目(CICD)差距分析与实现安排 + §7.3-7.5 实现进度与覆盖完成度 ### 设计决策 - 保留当前"单子女切换"范式(MultiChildTabBar + useChildSwitcher),不迁移到"多子女同屏对比" - 仪表盘除外:已增强为多子女卡片网格并列展示 - 参考项目所有 11 个家长端页面功能已 100% 覆盖 Refs: ARB-020 §22, ARB-022 §24.4
156 lines
5.0 KiB
TypeScript
156 lines
5.0 KiB
TypeScript
// ChildGradeChart 组件单测(mock recharts)
|
||
// 依据:02-architecture-design.md §15.6 成绩图表设计
|
||
// 覆盖:渲染图表标题 / 数据映射 / 班级均对比线(虚线)/ classAverage 缺失时默认 0
|
||
|
||
import { describe, it, expect, vi } from "vitest";
|
||
import { render, screen } from "@testing-library/react";
|
||
import type { GradeDataPoint } from "@/types";
|
||
|
||
// mock recharts:渲染简单 div 包裹子元素,便于断言
|
||
vi.mock("recharts", () => ({
|
||
ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
|
||
<div data-testid="responsive-container">{children}</div>
|
||
),
|
||
ComposedChart: ({
|
||
children,
|
||
data,
|
||
}: {
|
||
children: React.ReactNode;
|
||
data: unknown[];
|
||
}) => (
|
||
<div data-testid="composed-chart" data-length={data.length}>
|
||
{children}
|
||
</div>
|
||
),
|
||
Bar: ({ dataKey, fill }: { dataKey: string; fill: string }) => (
|
||
<div data-testid="bar" data-key={dataKey} data-fill={fill} />
|
||
),
|
||
Line: ({
|
||
dataKey,
|
||
stroke,
|
||
strokeDasharray,
|
||
}: {
|
||
dataKey: string;
|
||
stroke: string;
|
||
strokeDasharray?: string;
|
||
}) => (
|
||
<div
|
||
data-testid="line"
|
||
data-key={dataKey}
|
||
data-stroke={stroke}
|
||
data-dash={strokeDasharray}
|
||
/>
|
||
),
|
||
XAxis: (_props: unknown) => <div data-testid="x-axis" />,
|
||
YAxis: (_props: unknown) => <div data-testid="y-axis" />,
|
||
CartesianGrid: (_props: unknown) => <div data-testid="cartesian-grid" />,
|
||
Tooltip: (_props: unknown) => <div data-testid="tooltip" />,
|
||
Legend: (_props: unknown) => <div data-testid="legend" />,
|
||
}));
|
||
|
||
import { ChildGradeChart } from "./ChildGradeChart";
|
||
|
||
const mockGrades: GradeDataPoint[] = [
|
||
{
|
||
examId: "exam-001",
|
||
examName: "期中数学",
|
||
examDate: "2026-03-15T09:00:00Z",
|
||
subject: "数学",
|
||
studentScore: 92,
|
||
classAverage: 78,
|
||
gradeLevel: "A",
|
||
},
|
||
{
|
||
examId: "exam-002",
|
||
examName: "期中语文",
|
||
examDate: "2026-03-16T09:00:00Z",
|
||
subject: "语文",
|
||
studentScore: 85,
|
||
classAverage: 75,
|
||
gradeLevel: "B",
|
||
},
|
||
];
|
||
|
||
describe("ChildGradeChart 渲染", () => {
|
||
it("渲染标题成绩对比", () => {
|
||
render(<ChildGradeChart grades={mockGrades} />);
|
||
expect(screen.getByText("成绩对比")).toBeInTheDocument();
|
||
});
|
||
|
||
it("渲染 ResponsiveContainer", () => {
|
||
render(<ChildGradeChart grades={mockGrades} />);
|
||
expect(screen.getByTestId("responsive-container")).toBeInTheDocument();
|
||
});
|
||
|
||
it("渲染 ComposedChart 并传入数据", () => {
|
||
render(<ChildGradeChart grades={mockGrades} />);
|
||
const chart = screen.getByTestId("composed-chart");
|
||
expect(chart).toHaveAttribute("data-length", "2");
|
||
});
|
||
|
||
it("渲染 Bar(学生分数)和 Line(班级平均)", () => {
|
||
render(<ChildGradeChart grades={mockGrades} />);
|
||
const bars = screen.getAllByTestId("bar");
|
||
const lines = screen.getAllByTestId("line");
|
||
expect(bars).toHaveLength(1);
|
||
expect(lines).toHaveLength(1);
|
||
expect(bars[0]).toHaveAttribute("data-key", "学生分数");
|
||
expect(lines[0]).toHaveAttribute("data-key", "班级平均");
|
||
});
|
||
|
||
it("班级平均线为虚线(strokeDasharray=5 5)", () => {
|
||
render(<ChildGradeChart grades={mockGrades} />);
|
||
const line = screen.getByTestId("line");
|
||
expect(line).toHaveAttribute("data-dash", "5 5");
|
||
});
|
||
|
||
it("渲染 XAxis / YAxis / CartesianGrid / Tooltip / Legend", () => {
|
||
render(<ChildGradeChart grades={mockGrades} />);
|
||
expect(screen.getByTestId("x-axis")).toBeInTheDocument();
|
||
expect(screen.getByTestId("y-axis")).toBeInTheDocument();
|
||
expect(screen.getByTestId("cartesian-grid")).toBeInTheDocument();
|
||
expect(screen.getByTestId("tooltip")).toBeInTheDocument();
|
||
expect(screen.getByTestId("legend")).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe("ChildGradeChart 数据映射", () => {
|
||
it("空 grades 数组渲染 0 条数据", () => {
|
||
render(<ChildGradeChart grades={[]} />);
|
||
const chart = screen.getByTestId("composed-chart");
|
||
expect(chart).toHaveAttribute("data-length", "0");
|
||
});
|
||
|
||
it("classAverage 缺失时默认为 0", () => {
|
||
const gradesWithoutAvg: GradeDataPoint[] = [
|
||
{
|
||
examId: "exam-003",
|
||
examName: "测验",
|
||
examDate: "2026-04-01T09:00:00Z",
|
||
subject: "英语",
|
||
studentScore: 88,
|
||
gradeLevel: "B",
|
||
},
|
||
];
|
||
render(<ChildGradeChart grades={gradesWithoutAvg} />);
|
||
// 仅验证不崩溃且渲染 1 条数据
|
||
const chart = screen.getByTestId("composed-chart");
|
||
expect(chart).toHaveAttribute("data-length", "1");
|
||
});
|
||
|
||
it("多条成绩数据正确映射", () => {
|
||
const manyGrades: GradeDataPoint[] = Array.from({ length: 5 }, (_, i) => ({
|
||
examId: `exam-${i}`,
|
||
examName: `测验${i}`,
|
||
examDate: "2026-03-15T09:00:00Z",
|
||
subject: `科目${i}`,
|
||
studentScore: 80 + i,
|
||
classAverage: 70,
|
||
gradeLevel: "B" as const,
|
||
}));
|
||
render(<ChildGradeChart grades={manyGrades} />);
|
||
const chart = screen.getByTestId("composed-chart");
|
||
expect(chart).toHaveAttribute("data-length", "5");
|
||
});
|
||
});
|