90 lines
3.4 KiB
TypeScript
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);
|
|
});
|
|
});
|