搭建 Vitest 测试框架;新增答案等价判定 lib(answer-match),QuizQ 支持 judgeMode

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-20 13:55:16 +08:00
co-authored by Claude Haiku 4.5
parent 637c6032d4
commit bb8bb25096
7 changed files with 2023 additions and 10 deletions
+1924 -8
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -11,7 +11,8 @@
"db:push": "drizzle-kit push",
"db:seed": "tsx scripts/seed.ts",
"db:seed-math": "tsx scripts/seed-math.ts",
"db:seed-exam": "tsx scripts/seed-exam.ts"
"db:seed-exam": "tsx scripts/seed-exam.ts",
"test": "vitest run"
},
"dependencies": {
"@fontsource-variable/plus-jakarta-sans": "^5.3.0",
@@ -34,13 +35,18 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.5",
"@types/node": "^24.0.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"dotenv": "^17.2.0",
"drizzle-kit": "^0.31.4",
"jsdom": "^30.0.1",
"tailwindcss": "^4.1.11",
"tsx": "^4.20.3",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^4.1.11"
}
}
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { answersMatch, normalizeAnswer } from "./answer-match";
describe("normalizeAnswer", () => {
it("分数转小数", () => {
expect(normalizeAnswer("1/2")).toBe("0.5");
expect(normalizeAnswer("2/4")).toBe("0.5");
});
it("百分数转小数", () => {
expect(normalizeAnswer("50%")).toBe("0.5");
});
it("小数去尾零", () => {
expect(normalizeAnswer("0.50")).toBe("0.5");
});
it("整数原样", () => {
expect(normalizeAnswer("10")).toBe("10");
});
it("文本去空白/全角/大小写", () => {
expect(normalizeAnswer("ABC")).toBe("abc");
expect(normalizeAnswer("10 个")).toBe("10个");
});
});
describe("answersMatch", () => {
it("等值命中:1/2 ≡ 0.5", () => {
expect(answersMatch(["0.5", "1/2"], "1/2")).toBe(true);
expect(answersMatch(["0.5"], "0.50")).toBe(true);
});
it("百分数命中", () => {
expect(answersMatch(["0.5"], "50%")).toBe(true);
});
it("文本大小写/全角不敏感", () => {
expect(answersMatch(["across"], "ACROSS")).toBe(true);
});
it("单位严格:10个 ≠ 10", () => {
expect(answersMatch(["10个"], "10")).toBe(false);
expect(answersMatch(["10", "10个"], "10个")).toBe(true);
});
it("空白答案不判对", () => {
expect(answersMatch(["0.5"], " ")).toBe(false);
});
it("空期望数组不判对", () => {
expect(answersMatch([], "0.5")).toBe(false);
});
});
+27
View File
@@ -0,0 +1,27 @@
// xuchao: 答案等价判定——数值归一(分数/小数/百分数互转),文本归一(去空白/全角/小写)
const toHalf = (s: string) =>
s.replace(/[!-~]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0)).replace(/ /g, " ");
function stripWs(s: string): string {
return toHalf(s).replace(/\s+/g, "").toLowerCase();
}
export function normalizeAnswer(s: string): string {
const t = stripWs(s);
const frac = t.match(/^(-?\d+)\/(-?\d+)$/);
if (frac && Number(frac[2]) !== 0) {
const v = Number(frac[1]) / Number(frac[2]);
if (Number.isFinite(v)) return String(v);
}
const pct = t.match(/^(-?\d+(?:\.\d+)?)%$/);
if (pct) return String(Number(pct[1]) / 100);
if (/^-?\d+\.\d+$/.test(t)) return String(Number(t));
return t;
}
// xuchao: 任一期望答案等值命中即判对;空白答案一律不判对
export function answersMatch(expected: string[], given: string): boolean {
const g = normalizeAnswer(given);
if (!g) return false;
return expected.some((x) => normalizeAnswer(x) === g);
}
+6
View File
@@ -13,6 +13,9 @@ export type QuizQ = {
points?: number; // solve:本题满分
reference?: string; // solve:完整参考解答
rubric?: { point: string; score: number }[]; // solve:评分点,合计 = points
// xuchao: 应用题判定模式——rubric=AI 逐评分点(缺省,存量兼容);answer=最终答案数组匹配秒判
judgeMode?: "rubric" | "answer";
// judgeMode==="answer" 时复用上面 answers 数组(等价答法都填)
// xuchao: 错题本里的解答题作答快照(照片 + 逐点判定),交卷入错题本时写入
solveSnap?: { ossKey: string; earned: number; hits: { point: string; score: number; hit: boolean; reason: string }[] };
explain: string;
@@ -33,3 +36,6 @@ export function extractQuiz(md: string): QuizQ[] {
}
return out;
}
// xuchao: 应用题 answer 模式的提交载荷(最终答案 + 照片 key,供秒判与错题本快照)
export type SolveAnswerPayload = { answer: string; ossKey: string };
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";
+12
View File
@@ -0,0 +1,12 @@
import path from "node:path";
import { defineConfig } from "vitest/config";
// xuchao: Vitest 配置——@ 别名对齐 tsconfig;组件测试文件用 // @vitest-environment jsdom 标注
export default defineConfig({
resolve: { alias: { "@": path.resolve(__dirname, "src") } },
test: {
environment: "node",
include: ["src/**/*.test.{ts,tsx}"],
setupFiles: ["src/test-setup.ts"],
},
});