## 变更内容 ### 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
132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
// useChildCoursePlanDetail Hook 单测
|
||
// 依据:02-architecture-design.md §4.2 GraphQL 接入
|
||
// 覆盖:无 currentChildId / 无 planId 暂停 / 加载态 / 成功返回详情(含章节)/ 错误态 / 详情为 null
|
||
|
||
import {
|
||
describe,
|
||
it,
|
||
expect,
|
||
beforeAll,
|
||
afterAll,
|
||
afterEach,
|
||
beforeEach,
|
||
} from "vitest";
|
||
import { renderHook, waitFor, cleanup } from "@testing-library/react";
|
||
import {
|
||
Provider as UrqlProvider,
|
||
Client,
|
||
cacheExchange,
|
||
fetchExchange,
|
||
} from "urql";
|
||
import { graphql, HttpResponse } from "msw";
|
||
import type { ReactNode } from "react";
|
||
import { server } from "@/test/mocks/server";
|
||
import { useChildStore } from "@/store/child-store";
|
||
import { useChildCoursePlanDetail } from "./useChildCoursePlanDetail";
|
||
|
||
function createTestClient(): Client {
|
||
return new Client({
|
||
url: "/api/v1/parent/v1/graphql",
|
||
exchanges: [cacheExchange, fetchExchange],
|
||
fetchOptions: { headers: { "X-Requested-With": "XMLHttpRequest" } },
|
||
});
|
||
}
|
||
|
||
function renderUseChildCoursePlanDetail(planId: string) {
|
||
const client = createTestClient();
|
||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||
<UrqlProvider value={client}>{children}</UrqlProvider>
|
||
);
|
||
return renderHook(() => useChildCoursePlanDetail(planId), { wrapper });
|
||
}
|
||
|
||
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||
afterEach(() => {
|
||
cleanup();
|
||
server.resetHandlers();
|
||
});
|
||
afterAll(() => server.close());
|
||
|
||
beforeEach(() => {
|
||
localStorage.clear();
|
||
useChildStore.setState({
|
||
children: [],
|
||
currentChildId: null,
|
||
isLoading: false,
|
||
});
|
||
});
|
||
|
||
describe("useChildCoursePlanDetail", () => {
|
||
it("无 currentChildId 时暂停查询,返回 null", () => {
|
||
const { result } = renderUseChildCoursePlanDetail("cp-001");
|
||
expect(result.current.coursePlan).toBeNull();
|
||
expect(result.current.loading).toBe(false);
|
||
expect(result.current.error).toBeUndefined();
|
||
});
|
||
|
||
it("无 planId 时暂停查询,返回 null", () => {
|
||
useChildStore.setState({ currentChildId: "student-001" });
|
||
const { result } = renderUseChildCoursePlanDetail("");
|
||
expect(result.current.coursePlan).toBeNull();
|
||
expect(result.current.loading).toBe(false);
|
||
expect(result.current.error).toBeUndefined();
|
||
});
|
||
|
||
it("有 currentChildId 和 planId 时加载成功返回课程计划详情", async () => {
|
||
useChildStore.setState({ currentChildId: "student-001" });
|
||
const { result } = renderUseChildCoursePlanDetail("cp-001");
|
||
await waitFor(() => {
|
||
expect(result.current.coursePlan).not.toBeNull();
|
||
});
|
||
expect(result.current.coursePlan?.id).toBe("cp-001");
|
||
expect(result.current.coursePlan?.title).toBe("初一数学下学期课程计划");
|
||
expect(result.current.coursePlan?.chapters).toHaveLength(4);
|
||
expect(result.current.coursePlan?.chapters[0]!.title).toBe(
|
||
"第一章 一元二次方程",
|
||
);
|
||
expect(result.current.coursePlan?.chapters[0]!.sortOrder).toBe(1);
|
||
expect(result.current.loading).toBe(false);
|
||
expect(result.current.error).toBeUndefined();
|
||
});
|
||
|
||
it("加载中 loading 为 true", () => {
|
||
useChildStore.setState({ currentChildId: "student-001" });
|
||
const { result } = renderUseChildCoursePlanDetail("cp-001");
|
||
expect(result.current.loading).toBe(true);
|
||
});
|
||
|
||
it("GraphQL 返回错误时透传 error", async () => {
|
||
useChildStore.setState({ currentChildId: "student-001" });
|
||
server.resetHandlers(
|
||
graphql.query("ChildCoursePlanDetail", () =>
|
||
HttpResponse.json(
|
||
{ errors: [{ message: "课程计划详情查询失败" }] },
|
||
{ status: 200 },
|
||
),
|
||
),
|
||
);
|
||
|
||
const { result } = renderUseChildCoursePlanDetail("cp-001");
|
||
await waitFor(() => {
|
||
expect(result.current.error).toBeDefined();
|
||
});
|
||
expect(result.current.coursePlan).toBeNull();
|
||
expect(result.current.error?.message).toContain("课程计划详情查询失败");
|
||
});
|
||
|
||
it("详情为 null 时返回 null", async () => {
|
||
useChildStore.setState({ currentChildId: "student-001" });
|
||
server.resetHandlers(
|
||
graphql.query("ChildCoursePlanDetail", () =>
|
||
HttpResponse.json({ data: { childCoursePlanDetail: null } }),
|
||
),
|
||
);
|
||
|
||
const { result } = renderUseChildCoursePlanDetail("not-exist");
|
||
await waitFor(() => {
|
||
expect(result.current.loading).toBe(false);
|
||
});
|
||
expect(result.current.coursePlan).toBeNull();
|
||
});
|
||
});
|