diff --git a/packages/number-line/controller/src/__tests__/index.test.js b/packages/number-line/controller/src/__tests__/index.test.js index 9e6e94ebd9..e28fb75391 100644 --- a/packages/number-line/controller/src/__tests__/index.test.js +++ b/packages/number-line/controller/src/__tests__/index.test.js @@ -152,7 +152,7 @@ describe('controller', () => { const assertOutcome = (label, question, session, env, expected) => { it(label, async () => { const result = await controller.outcome(question, session, env); - expect(result).toMatchObject(expected); + expect(result).toEqual(expect.objectContaining(expected)); }); }; @@ -178,7 +178,7 @@ describe('controller', () => { }; const e = { mode: 'evaluate' }; const result = await controller.outcome(q, s, e); - expect(result).toEqual({ score: 1 }); + expect(result.score).toEqual(1); }); }); diff --git a/packages/number-line/controller/src/index.js b/packages/number-line/controller/src/index.js index 62d2ee27dd..cd8d0bd8c4 100644 --- a/packages/number-line/controller/src/index.js +++ b/packages/number-line/controller/src/index.js @@ -35,12 +35,100 @@ const accumulateAnswer = (correctResponse) => (total, answer) => { return total + (isCorrectResponse ? 1 : 0); }; +/** + * Generates detailed trace log for scoring evaluation + * @param {Object} model - the question model + * @param {Object} session - the student session + * @param {Object} env - the environment + * @returns {Array} traceLog - array of trace messages + */ +export const getLogTrace = (model, session, env) => { + const traceLog = []; + + const studentAnswers = session?.answer || []; + const correctResponse = model?.correctResponse || []; + + if (!studentAnswers.length) { + return ['Student did not interact with the number line.']; + } + + const corrected = getCorrected(studentAnswers, cloneDeep(correctResponse)); + const { correct, incorrect, notInAnswer, noCorrectResponse } = corrected; + + if (noCorrectResponse) { + return ['No correct response is defined for this item.']; + } + + const correctCount = correct.length; + const incorrectCount = incorrect.length; + const missingCount = notInAnswer.length; + + if (correctCount > 0) { + traceLog.push(`${correctCount} object(s) correctly placed.`); + } + + if (incorrectCount > 0) { + traceLog.push(`${incorrectCount} incorrect object(s) placed.`); + } + + if (missingCount > 0) { + traceLog.push(`${missingCount} expected object(s) were not placed.`); + } + + const studentAnswersCopy = [...studentAnswers]; + const correctResponseCopy = [...correctResponse]; + + correct.forEach((index) => { + const answer = studentAnswersCopy[index]; + const objectType = answer?.type || 'object'; + traceLog.push(`${objectType.charAt(0).toUpperCase() + objectType.slice(1)} at position ${answer.domainPosition} is correct.`); + }); + + incorrect.forEach((index) => { + const answer = studentAnswersCopy[index]; + const objectType = answer?.type || 'object'; + traceLog.push(`${objectType.charAt(0).toUpperCase() + objectType.slice(1)} at position ${answer.domainPosition} does not match the expected response.`); + }); + + notInAnswer.forEach((expectedObject) => { + const objectType = expectedObject?.type || 'object'; + traceLog.push(`Expected ${objectType} at position ${expectedObject.domainPosition} was not placed by the student.`); + }); + + const partialScoringEnabled = partialScoring.enabled(model, env); + + if (partialScoringEnabled) { + traceLog.push('Score calculated using partial scoring.'); + traceLog.push(`Partial scoring is based on the number of correct objects, with deductions for extras.`); + } else { + traceLog.push('Score calculated using all-or-nothing scoring.'); + } + + const total = correctResponse.length || 1; + const extraPlacements = + studentAnswers.length > total ? studentAnswers.length - total : 0; + + if (extraPlacements > 0) { + traceLog.push( + `${extraPlacements} extra object(s) beyond the required amount were placed and deducted.`, + ); + } + + const rawScore = Math.max(0, (correctCount - extraPlacements) / total); + const finalScore = partialScoringEnabled ? rawScore : rawScore === 1 ? 1 : 0; + + traceLog.push(`Final score: ${finalScore}.`); + + return traceLog; +}; + + /** */ export function outcome(model, session, env) { return new Promise((resolve) => { if (!session || isEmpty(session)) { - resolve({ score: 0, empty: true }); + resolve({ score: 0, empty: true, logTrace: ['Student did not interact with the number line.'] }); } else { const partialScoringEnabled = partialScoring.enabled(model, env); const numCorrect = (session.answer || []).reduce(accumulateAnswer(model.correctResponse), 0); @@ -62,7 +150,10 @@ export function outcome(model, session, env) { score = 0; } - resolve({ score: partialScoringEnabled ? score : score === 1 ? 1 : 0 }); + resolve({ + score: partialScoringEnabled ? score : score === 1 ? 1 : 0, + logTrace: getLogTrace(model, session, env) + }); } }); }