家长端结构化题目编辑器:题型联动表单替代 JSON 文本域,应用题支持 judgeMode 切换

- 新增 question-list.tsx:结构化题目卡片列表(选择题/判断题/填空题/应用题)
- 新增 question-list.test.tsx:4 个 RTL 组件测试覆盖渲染/新增/编辑/校验
- paper-form.tsx:JSON textarea 替换为 QuestionList,保存时序列化 QuizQ[]
- vitest.config.ts:加 @vitejs/plugin-react 支持 JSX 组件测试
- test-setup.ts:注册 afterEach cleanup 避免跨测试 DOM 污染

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-20 14:16:13 +08:00
co-authored by Claude Haiku 4.5
parent 255c9a244c
commit 0749ba2c38
7 changed files with 1304 additions and 965 deletions
@@ -0,0 +1,55 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { QuestionList } from "./question-list";
const choiceQ = { type: "choice" as const, stem: "1+1=?", options: ["1", "2"], answer: 1, explain: "e" };
describe("QuestionList 结构化编辑", () => {
it("渲染题目卡片(题型徽标+题干)并可删除", () => {
render(<QuestionList value={[{ ...choiceQ }]} onChange={() => {}} />);
expect(screen.getByText("选择题")).toBeInTheDocument();
expect(screen.getByText("1+1=?")).toBeInTheDocument();
fireEvent.click(screen.getByLabelText("删除第 1 题"));
});
it("新增选择题:填题干/选项/点选答案/解析后保存进列表", async () => {
const user = userEvent.setup();
let saved: unknown[] = [];
render(<QuestionList value={[]} onChange={(v) => (saved = v)} />);
await user.click(screen.getByText("+ 新增题目"));
await user.selectOptions(screen.getByLabelText("题型"), "choice");
await user.type(screen.getByLabelText("题干"), "2+2=?");
await user.click(screen.getByText("+ 添加选项"));
await user.type(screen.getByPlaceholderText("选项1"), "3");
await user.click(screen.getByText("+ 添加选项"));
await user.type(screen.getByPlaceholderText("选项2"), "4");
fireEvent.click(screen.getByLabelText("选 4 为正确答案"));
await user.type(screen.getByLabelText("解析"), "e1");
await user.click(screen.getByText("完成"));
expect((saved[0] as Record<string, unknown>).stem).toBe("2+2=?");
expect((saved[0] as Record<string, unknown>).answer).toBe(1);
expect((saved[0] as Record<string, unknown>).explain).toBe("e1");
});
it("应用题 judgeMode 切换:answer 显示最终答案数组,rubric 显示评分点", async () => {
const user = userEvent.setup();
render(<QuestionList value={[{ type: "solve", stem: "解方程", points: 10, reference: "x=2", explain: "e" }]} onChange={() => {}} />);
fireEvent.click(screen.getByLabelText("编辑第 1 题"));
await user.selectOptions(screen.getByLabelText("判分模式"), "answer");
expect(screen.getByText("最终答案(等价答法可填多个)")).toBeInTheDocument();
await user.selectOptions(screen.getByLabelText("判分模式"), "rubric");
expect(screen.getByLabelText("参考解答")).toBeInTheDocument();
});
it("题干为空保存时报错,不写入列表", async () => {
const user = userEvent.setup();
let saved: unknown[] = [];
render(<QuestionList value={[]} onChange={(v) => (saved = v)} />);
await user.click(screen.getByText("+ 新增题目"));
await user.click(screen.getByText("完成"));
expect(saved.length).toBe(0);
expect(screen.getByText(/缺少题干/)).toBeInTheDocument();
});
});