编辑器新建应用题仅 answer 模式+数据完整性修复(最终评审:rubric 0分守卫/答案下标/points/ossKey 前缀/测试修正)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-20 14:49:21 +08:00
co-authored by Claude Haiku 4.5
parent fe68ed7512
commit 5e4904742a
10 changed files with 83 additions and 52 deletions
+42 -10
View File
@@ -1,22 +1,29 @@
// @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 { beforeEach, describe, expect, it, vi } from "vitest";
import { QuestionList } from "./question-list";
import type { QuizQ } from "@/lib/quiz-parse";
const choiceQ = { type: "choice" as const, stem: "1+1=?", options: ["1", "2"], answer: 1, explain: "e" };
beforeEach(() => {
vi.spyOn(window, "confirm").mockReturnValue(true);
});
describe("QuestionList 结构化编辑", () => {
it("渲染题目卡片(题型徽标+题干)并可删除", () => {
render(<QuestionList value={[{ ...choiceQ }]} onChange={() => {}} />);
let saved: QuizQ[] = [{ ...choiceQ }];
render(<QuestionList value={saved} onChange={(v) => (saved = v)} />);
expect(screen.getByText("选择题")).toBeInTheDocument();
expect(screen.getByText("1+1=?")).toBeInTheDocument();
fireEvent.click(screen.getByLabelText("删除第 1 题"));
expect(saved.length).toBe(0);
});
it("新增选择题:填题干/选项/点选答案/解析后保存进列表", async () => {
const user = userEvent.setup();
let saved: unknown[] = [];
let saved: QuizQ[] = [];
render(<QuestionList value={[]} onChange={(v) => (saved = v)} />);
await user.click(screen.getByText("+ 新增题目"));
await user.selectOptions(screen.getByLabelText("题型"), "choice");
@@ -33,23 +40,48 @@ describe("QuestionList 结构化编辑", () => {
expect((saved[0] as Record<string, unknown>).explain).toBe("e1");
});
it("应用题 judgeMode 切换:answer 显示最终答案数组,rubric 显示评分点", async () => {
it("新建应用题仅 answer 模式(无判分模式 select)", async () => {
const user = userEvent.setup();
render(<QuestionList value={[{ type: "solve", stem: "解方程", points: 10, reference: "x=2", explain: "e" }]} onChange={() => {}} />);
render(<QuestionList value={[]} onChange={() => {}} />);
await user.click(screen.getByText("+ 新增题目"));
await user.selectOptions(screen.getByLabelText("题型"), "solve");
expect(screen.queryByLabelText("判分模式")).not.toBeInTheDocument();
expect(screen.getByText("仅最终答案(秒判)")).toBeInTheDocument();
});
it("编辑存量 rubric 题显示只读徽标", () => {
render(
<QuestionList
value={[{ type: "solve", stem: "解方程", points: 10, judgeMode: "rubric" as const, reference: "x=2", rubric: [{ point: "过程", score: 6 }], 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();
expect(screen.queryByLabelText("判分模式")).not.toBeInTheDocument();
expect(screen.getByText("AI 逐评分点判分(存量题,保留原评分点)")).toBeInTheDocument();
});
it("题干为空保存时报错,不写入列表", async () => {
const user = userEvent.setup();
let saved: unknown[] = [];
let saved: QuizQ[] = [];
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();
});
it("删除选项后 answer 下标正确回落", async () => {
const user = userEvent.setup();
let saved: QuizQ[] = [{ type: "choice" as const, stem: "q", options: ["a", "b", "c"], answer: 2, explain: "e" }];
render(<QuestionList value={saved} onChange={(v) => (saved = v)} />);
fireEvent.click(screen.getByLabelText("编辑第 1 题"));
// xuchao: buttons = [上移,下移,编辑,删除, 取消, 添加选项, 删a, 删b, 删c, 完成] → 删b = [7]
const buttons = screen.getAllByRole("button");
fireEvent.click(buttons[7]);
// xuchao: 点"完成"保存,onChange 被调用
await user.click(screen.getByText("完成"));
const d = saved[0] as Record<string, unknown>;
expect(d.answer).toBe(1);
});
});