@coffeeandfun/maths-captcha
Version:
Maths-Captcha is a simple Node.js module that generates random math questions to verify human interaction. It creates math problems like addition, subtraction, multiplication, and division, and checks if the user's answer is correct. Ideal for adding a qu
191 lines (165 loc) • 7.5 kB
JavaScript
import { generateRandomMathQuestion,
validateAnswer,
checkIfSolvedCorrectly, } from '../index.js';
describe("Math Quiz Module", () => {
test("should generate a valid math question", () => {
const question = generateRandomMathQuestion();
expect(question).toHaveProperty("question");
expect(question).toHaveProperty("answer");
});
test("should validate the correct answer", () => {
const question = { question: "10 + 5", answer: 15 };
const result = validateAnswer(question, 15);
expect(result).toBe(true);
});
test('should return "Correct!" if the user answers correctly', () => {
const question = { question: "10 - 5", answer: 5 };
const result = checkIfSolvedCorrectly(question, 5);
expect(result).toBe(true);
});
test('should return "Wrong!" if the user answers incorrectly', () => {
const question = { question: "10 * 5", answer: 50 };
const result = checkIfSolvedCorrectly(question, 60);
expect(result).toBe(false);
});
describe("Decimal answer validation", () => {
test("accepts correctly rounded input for division", () => {
const question = { question: "10 / 7", answer: "1.43" };
expect(validateAnswer(question, "1.43")).toBe(true);
expect(validateAnswer(question, "1.3948")).toBe(false); // 1.3948.toFixed(2) → '1.39'
});
test("rejects input that rounds to a different value", () => {
const question = { question: "10 / 7", answer: "1.43" };
expect(validateAnswer(question, "1.42")).toBe(false);
expect(validateAnswer(question, "1.44")).toBe(false);
});
});
describe("Whole-number division formatting", () => {
test("accepts integer and truncated inputs when answer is a whole number", () => {
const question = { question: "8 / 2", answer: "4.00" };
expect(validateAnswer(question, "4")).toBe(true);
expect(validateAnswer(question, "4.0")).toBe(true);
expect(validateAnswer(question, "4.00")).toBe(true);
});
test("rejects off-by-one decimals", () => {
const question = { question: "8 / 2", answer: "4.00" };
expect(validateAnswer(question, "3.99")).toBe(false);
expect(validateAnswer(question, "4.01")).toBe(false);
});
});
describe("checkIfSolvedCorrectly wrapper for decimals", () => {
test("correctly identifies correct decimal answers", () => {
const question = { question: "10 / 4", answer: "2.50" };
expect(checkIfSolvedCorrectly(question, "2.5")).toBe(true);
expect(checkIfSolvedCorrectly(question, "2.50")).toBe(true);
});
test("identifies incorrect decimal answers", () => {
const question = { question: "10 / 4", answer: "2.50" };
expect(checkIfSolvedCorrectly(question, "2.49")).toBe(false);
expect(checkIfSolvedCorrectly(question, "2.6")).toBe(false);
});
});
describe("Invalid input handling", () => {
test("validateAnswer returns false for non-numeric input", () => {
const question = { question: "5 + 5", answer: "10" };
expect(validateAnswer(question, "abc")).toBe(false);
expect(validateAnswer(question, "")).toBe(false);
expect(validateAnswer(question, null)).toBe(false);
expect(validateAnswer(question, undefined)).toBe(false);
});
test("checkIfSolvedCorrectly wraps invalid inputs as false", () => {
const question = { question: "5 + 5", answer: "10" };
expect(checkIfSolvedCorrectly(question, "xyz")).toBe(false);
expect(checkIfSolvedCorrectly(question, "")).toBe(false);
});
});
});
describe("Edge-Case Validation", () => {
test("strict integer equality when no decimals expected", () => {
const q = { question: "5 + 0", answer: "5" };
// 4.6 would round to 5 if we did toFixed(0) → but we want false
expect(validateAnswer(q, "4.6")).toBe(false);
// '5.0' or '5.00' should also be rejected for integer answers
expect(validateAnswer(q, "5.0")).toBe(false);
expect(validateAnswer(q, "5.00")).toBe(false);
// exact integer still works
expect(validateAnswer(q, "5")).toBe(true);
});
test("rounding boundaries for halves", () => {
const q = { question: "1 / 1", answer: "1.33" };
// 1.334 → rounds down to '1.33'
expect(validateAnswer(q, "1.334")).toBe(true);
// 1.335 → rounds up to '1.34'
expect(validateAnswer(q, "1.335")).toBe(false);
});
test("negative zero matches positive zero format", () => {
const q = { question: "0 - 0", answer: "0.00" };
// parseFloat('-0') is -0 but toFixed should normalize to '0.00'
expect(validateAnswer(q, "-0")).toBe(true);
// but a stray '-0.00' string shouldn't pass
expect(validateAnswer(q, "-0.00")).toBe(false);
});
test("rejects malformed numeric strings", () => {
const q = { question: "3 + 1.14", answer: "4.14" };
expect(validateAnswer(q, "3.1.4")).toBe(false);
expect(validateAnswer(q, "4,14")).toBe(false);
expect(validateAnswer(q, "--5")).toBe(false);
expect(validateAnswer(q, " ")).toBe(false);
});
test("handles very large numbers without rounding glitches", () => {
const big = "10000000000000000"; // 1e16
const q = { question: "1e16 + 0", answer: big };
expect(validateAnswer(q, big)).toBe(true);
// adding a tiny fraction should fail
expect(validateAnswer(q, "10000000000000000.1")).toBe(false);
});
});
describe("Additional Validation Tests", () => {
// --- Negative numbers ---
test("accepts negative integer answers, rejects decimals", () => {
const qInt = { question: "-3 + 0", answer: "-3" };
expect(validateAnswer(qInt, "-3")).toBe(true);
expect(validateAnswer(qInt, "-3.0")).toBe(false);
expect(validateAnswer(qInt, "-2.9")).toBe(false);
});
test("rejects negative decimal answers even if they match expected", () => {
const qDec = { question: "-5 / 2", answer: "-2.50" };
// we currently forbid any negative input with a dot
expect(validateAnswer(qDec, "-2.50")).toBe(false);
expect(validateAnswer(qDec, "-2.5")).toBe(false);
});
// --- Zero handling ---
test("zero answer formatting", () => {
const qZeroInt = { question: "0 + 0", answer: "0" };
expect(validateAnswer(qZeroInt, "0")).toBe(true);
expect(validateAnswer(qZeroInt, "0.0")).toBe(false);
const qZeroDec = { question: "0 / 1", answer: "0.00" };
expect(validateAnswer(qZeroDec, "0")).toBe(true);
expect(validateAnswer(qZeroDec, "0.00")).toBe(true);
});
// --- Rounding edge for long decimals ---
test("manualRound half-up behavior on long inputs", () => {
const q = { question: "1 / 3", answer: "0.33" };
expect(validateAnswer(q, "0.3349")).toBe(true); // 0.3349 → 0.33
expect(validateAnswer(q, "0.3350")).toBe(false); // 0.3350 → 0.34
expect(validateAnswer(q, "0.3399")).toBe(false); // → 0.34
});
// --- Random generator sanity check ---
test("generated question answer matches eval’d result", () => {
for (let i = 0; i < 20; i++) {
const q = generateRandomMathQuestion(3);
// evaluate the question string
// eslint-disable-next-line no-eval
const raw = eval(q.question);
if (q.question.includes("/")) {
// division → 3 decimal places
expect(q.answer).toBe(raw.toFixed(3));
// and validateAnswer should accept it
expect(validateAnswer(q, q.answer)).toBe(true);
} else {
expect(q.answer).toBe(String(raw));
expect(validateAnswer(q, raw)).toBe(true);
}
}
});
});