Initial: Intelligrade Lite — client-side digital exam platform MVP

This commit is contained in:
Bun Bun
2026-06-20 18:24:02 +00:00
commit 9ddf67148c
14 changed files with 3038 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
import assert from 'node:assert';
import test from 'node:test';
// Test grading utilities
function gradeQuestion(question, answer) {
const answerValue = answer.value;
switch (question.type) {
case 'multiple_choice':
if (typeof answerValue === 'string' && question.correctAnswer) {
return answerValue === question.correctAnswer ? question.points : 0;
}
return 0;
case 'true_false':
if (typeof answerValue === 'string' && question.correctAnswer) {
return answerValue === question.correctAnswer ? question.points : 0;
}
return 0;
case 'fill_blank': {
if (typeof answerValue !== 'string' || !question.correctAnswer)
return 0;
const correct = Array.isArray(question.correctAnswer)
? question.correctAnswer
: [question.correctAnswer];
const submitted = question.caseSensitive ? answerValue.trim() : answerValue.trim().toLowerCase();
for (const c of correct) {
const expected = question.caseSensitive ? c.trim() : c.trim().toLowerCase();
if (submitted === expected)
return question.points;
}
return 0;
}
case 'short_answer':
case 'essay':
return -1;
default:
return 0;
}
}
test('grade multiple choice - correct', () => {
const q = { type: 'multiple_choice', points: 2, correctAnswer: '1', options: ['A', 'B', 'C'] };
const a = { questionId: '1', value: '1' };
assert.strictEqual(gradeQuestion(q, a), 2);
});
test('grade multiple choice - incorrect', () => {
const q = { type: 'multiple_choice', points: 2, correctAnswer: '1', options: ['A', 'B', 'C'] };
const a = { questionId: '1', value: '0' };
assert.strictEqual(gradeQuestion(q, a), 0);
});
test('grade true/false - correct', () => {
const q = { type: 'true_false', points: 1, correctAnswer: 'true' };
const a = { questionId: '1', value: 'true' };
assert.strictEqual(gradeQuestion(q, a), 1);
});
test('grade fill_blank - exact match', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria'], caseSensitive: false };
const a = { questionId: '1', value: 'mitochondria' };
assert.strictEqual(gradeQuestion(q, a), 3);
});
test('grade fill_blank - case insensitive', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria'], caseSensitive: false };
const a = { questionId: '1', value: 'Mitochondria' };
assert.strictEqual(gradeQuestion(q, a), 3);
});
test('grade fill_blank - multiple acceptable', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria', 'Mitochondrion'], caseSensitive: false };
const a = { questionId: '1', value: 'Mitochondrion' };
assert.strictEqual(gradeQuestion(q, a), 3);
});
test('grade fill_blank - no match', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria'], caseSensitive: false };
const a = { questionId: '1', value: 'nucleus' };
assert.strictEqual(gradeQuestion(q, a), 0);
});
test('grade essay - requires manual', () => {
const q = { type: 'essay', points: 10 };
const a = { questionId: '1', value: 'An essay...' };
assert.strictEqual(gradeQuestion(q, a), -1);
});
test('grade short_answer - requires manual', () => {
const q = { type: 'short_answer', points: 5 };
const a = { questionId: '1', value: 'Short answer' };
assert.strictEqual(gradeQuestion(q, a), -1);
});
// Test store utilities
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
test('generateId produces unique strings', () => {
const id1 = generateId();
const id2 = generateId();
assert.strictEqual(typeof id1, 'string');
assert.strictEqual(id1.length > 0, true);
assert.notStrictEqual(id1, id2);
});
console.log('All tests passed!');
//# sourceMappingURL=grade.test.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"grade.test.js","sourceRoot":"","sources":["grade.test.ts"],"names":[],"mappings":"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"}
+105
View File
@@ -0,0 +1,105 @@
import assert from 'node:assert';
import test from 'node:test';
// Test grading utilities
function gradeQuestion(question: any, answer: any): number {
const answerValue = answer.value;
switch (question.type) {
case 'multiple_choice':
if (typeof answerValue === 'string' && question.correctAnswer) {
return answerValue === question.correctAnswer ? question.points : 0;
}
return 0;
case 'true_false':
if (typeof answerValue === 'string' && question.correctAnswer) {
return answerValue === question.correctAnswer ? question.points : 0;
}
return 0;
case 'fill_blank': {
if (typeof answerValue !== 'string' || !question.correctAnswer) return 0;
const correct = Array.isArray(question.correctAnswer)
? question.correctAnswer
: [question.correctAnswer];
const submitted = question.caseSensitive ? answerValue.trim() : answerValue.trim().toLowerCase();
for (const c of correct) {
const expected = question.caseSensitive ? c.trim() : c.trim().toLowerCase();
if (submitted === expected) return question.points;
}
return 0;
}
case 'short_answer':
case 'essay':
return -1;
default:
return 0;
}
}
test('grade multiple choice - correct', () => {
const q = { type: 'multiple_choice', points: 2, correctAnswer: '1', options: ['A', 'B', 'C'] };
const a = { questionId: '1', value: '1' };
assert.strictEqual(gradeQuestion(q, a), 2);
});
test('grade multiple choice - incorrect', () => {
const q = { type: 'multiple_choice', points: 2, correctAnswer: '1', options: ['A', 'B', 'C'] };
const a = { questionId: '1', value: '0' };
assert.strictEqual(gradeQuestion(q, a), 0);
});
test('grade true/false - correct', () => {
const q = { type: 'true_false', points: 1, correctAnswer: 'true' };
const a = { questionId: '1', value: 'true' };
assert.strictEqual(gradeQuestion(q, a), 1);
});
test('grade fill_blank - exact match', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria'], caseSensitive: false };
const a = { questionId: '1', value: 'mitochondria' };
assert.strictEqual(gradeQuestion(q, a), 3);
});
test('grade fill_blank - case insensitive', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria'], caseSensitive: false };
const a = { questionId: '1', value: 'Mitochondria' };
assert.strictEqual(gradeQuestion(q, a), 3);
});
test('grade fill_blank - multiple acceptable', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria', 'Mitochondrion'], caseSensitive: false };
const a = { questionId: '1', value: 'Mitochondrion' };
assert.strictEqual(gradeQuestion(q, a), 3);
});
test('grade fill_blank - no match', () => {
const q = { type: 'fill_blank', points: 3, correctAnswer: ['mitochondria'], caseSensitive: false };
const a = { questionId: '1', value: 'nucleus' };
assert.strictEqual(gradeQuestion(q, a), 0);
});
test('grade essay - requires manual', () => {
const q = { type: 'essay', points: 10 };
const a = { questionId: '1', value: 'An essay...' };
assert.strictEqual(gradeQuestion(q, a), -1);
});
test('grade short_answer - requires manual', () => {
const q = { type: 'short_answer', points: 5 };
const a = { questionId: '1', value: 'Short answer' };
assert.strictEqual(gradeQuestion(q, a), -1);
});
// Test store utilities
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
test('generateId produces unique strings', () => {
const id1 = generateId();
const id2 = generateId();
assert.strictEqual(typeof id1, 'string');
assert.strictEqual(id1.length > 0, true);
assert.notStrictEqual(id1, id2);
});
console.log('All tests passed!');