Files
Edu/apps/parent-portal/src/components/ChildGradeChart.test.tsx
SpecialX 18d94c0cc2 feat(parent-portal): 完成参考项目(CICD)家长端功能全量覆盖 - 9 新页面 + 3 页面增强 + 763 测试通过
## 变更内容

### 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
2026-07-13 15:12:56 +08:00

156 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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");
});
});