Files
openmaic/OpenMAIC/tests/edit/round-trip/quiz.test.ts
2026-08-16 14:58:47 +08:00

90 lines
3.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import type { ManifestScene } from '@/lib/export/classroom-zip-types';
import type { QuizContent } from '@/lib/types/stage';
import {
addOption,
addQuestion,
setQuestionType,
toggleCorrect,
updateOptionLabel,
updateQuestion,
} from '@/components/edit/surfaces/quiz/quiz-edit-ops';
/**
* Round-trip gate for the quiz surface (issue #657, design principle #1).
*
* A quiz scene is NOT a PPTX citizen (`use-export-pptx` filters to slide
* scenes). Its export target is the classroom ZIP manifest, which serializes
* the full `scene.content` to JSON on export (`use-export-classroom`) and
* reads it back verbatim on import (`use-import-classroom`). The gate: an
* edited `QuizContent` survives that JSON round-trip with every field intact
* and no non-serializable values leaking in.
*/
/** Mirror the manifest's JSON serialize → deserialize boundary. */
function manifestRoundTrip(content: QuizContent): QuizContent {
const scene: ManifestScene = { type: 'quiz', title: 'Quiz', order: 1, content };
const json = JSON.stringify(scene);
const parsed = JSON.parse(json) as ManifestScene;
return parsed.content as QuizContent;
}
function buildEditedQuiz(): QuizContent {
let c: QuizContent = { type: 'quiz', questions: [] };
// A multiple-choice question with two correct answers.
c = addQuestion(c, 'multiple', 'q1');
c = updateQuestion(c, 'q1', {
question: 'Which are prime?',
analysis: '2 and 3 are prime.',
points: 2,
});
c = addOption(c, 'q1'); // now A,B,C
c = updateOptionLabel(c, 'q1', 0, '2');
c = updateOptionLabel(c, 'q1', 1, '4');
c = updateOptionLabel(c, 'q1', 2, '3');
c = toggleCorrect(c, 'q1', 0); // 2 correct
c = toggleCorrect(c, 'q1', 2); // 3 correct
// A short-answer question with grading guidance.
c = addQuestion(c, 'short_answer', 'q2');
c = updateQuestion(c, 'q2', {
question: 'Explain primality.',
commentPrompt: 'Mention divisors.',
});
c = setQuestionType(c, 'q2', 'short_answer'); // no-op, but exercises the path
return c;
}
describe('quiz classroom-manifest round-trip', () => {
it('survives JSON serialize → deserialize with all fields intact', () => {
const edited = buildEditedQuiz();
const restored = manifestRoundTrip(edited);
expect(restored).toEqual(edited);
});
it('preserves multiple-choice answer keys and option letters', () => {
const restored = manifestRoundTrip(buildEditedQuiz());
const q1 = restored.questions.find((q) => q.id === 'q1')!;
expect(q1.options?.map((o) => o.value)).toEqual(['A', 'B', 'C']);
expect(q1.answer).toEqual(['A', 'C']); // "2" and "3"
expect(q1.points).toBe(2);
});
it('preserves short-answer grading fields', () => {
const restored = manifestRoundTrip(buildEditedQuiz());
const q2 = restored.questions.find((q) => q.id === 'q2')!;
expect(q2.type).toBe('short_answer');
expect(q2.commentPrompt).toBe('Mention divisors.');
expect(q2.options).toBeUndefined();
});
it('introduces no non-JSON-serializable values (no data lost to JSON)', () => {
const edited = buildEditedQuiz();
// If any field were a function / undefined-in-array / Map, stringify would
// drop or mangle it and the deep-equal above would already fail; this
// asserts the serialized form is itself stable (idempotent re-parse).
const once = JSON.stringify(edited);
const twice = JSON.stringify(JSON.parse(once));
expect(twice).toBe(once);
});
});