diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index a8962f5714..dfa6d60844 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -1116,7 +1116,7 @@ export type PerseusNumericInputAnswer = { // Translatable Display; A description for why this answer is correct, wrong, or ungraded message: string; // The expected answer - value: number; + value?: number; // Whether this answer is "correct", "wrong", or "ungraded" status: string; // The forms available for this answer. Options: "integer, ""decimal", "proper", "improper", "mixed", or "pi" diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/numeric-input-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/numeric-input-widget.ts index 8b4d36d31b..47bb471c13 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/numeric-input-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/numeric-input-widget.ts @@ -32,7 +32,7 @@ export const parseNumericInputWidget: Parser = parseWidget( answers: array( object({ message: string, - value: number, + value: optional(number), status: string, answerForms: optional(array(parseMathFormat)), strict: boolean, diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/numeric-input-answer-without-value.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/numeric-input-answer-without-value.json new file mode 100644 index 0000000000..0ef7538a66 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/numeric-input-answer-without-value.json @@ -0,0 +1,115 @@ +{ + "question": { + "content": "$\\bigg(\\dfrac{2}{3} \\bigg)^3 =$ [[☃ expression 1]]", + "images": {}, + "widgets": { + "numeric-input 1": { + "type": "numeric-input", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "answers": [ + { + "status": "correct", + "message": "", + "simplify": "required", + "strict": false, + "maxError": null, + "answerForms": [ + "proper", + "improper" + ] + } + ], + "size": "normal", + "coefficient": false, + "labelText": "", + "rightAlign": false, + "static": false, + "multipleNumberInput": false + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "expression 1": { + "options": { + "answerForms": [ + { + "considered": "correct", + "form": true, + "key": 0, + "simplify": true, + "value": "\\frac{8}{27}", + "times": false, + "functions": [ + "f", + "g", + "h" + ], + "buttonSets": [ + "basic" + ], + "buttonsVisible": "focused", + "linterContext": { + "contentType": "", + "highlightLint": false, + "paths": [], + "stack": [] + } + } + ], + "times": false, + "buttonSets": [ + "basic" + ], + "functions": [ + "f", + "g", + "h" + ], + "static": false + }, + "type": "expression", + "version": { + "major": 1, + "minor": 0 + }, + "graded": true, + "alignment": "default", + "static": false + } + } + }, + "answerArea": { + "calculator": false, + "chi2Table": false, + "financialCalculatorMonthlyPayment": false, + "financialCalculatorTotalAmount": false, + "financialCalculatorTimeToPayOff": false, + "periodicTable": false, + "periodicTableWithKey": false, + "tTable": false, + "zTable": false + }, + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "hints": [ + { + "replace": false, + "content": "$\\bigg(\\dfrac{2}{3} \\bigg)^3 =\\dfrac{2}{3}\\times \\dfrac{2}{3}\\times\\dfrac{2}{3}$\n\nLet's perform this multiplication to find the answer.", + "images": {}, + "widgets": {} + }, + { + "replace": false, + "content": "On multiplying, we see that\n\n$\\dfrac{2}{3}\\times \\dfrac{2}{3}\\times\\dfrac{2}{3}=\\boxed{\\dfrac{8}{27}}$.", + "images": {}, + "widgets": {} + } + ] +} diff --git a/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts b/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts index c6ee90ec2c..13ce2a6cb1 100644 --- a/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts +++ b/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts @@ -287,6 +287,121 @@ describe("static function validate", () => { // Assert - "incomplete" expect(score).toHaveBeenAnsweredCorrectly(); }); + + it("ignores missing values when determining if the answer can be formatted as a percentage", () => { + const rubric: PerseusNumericInputRubric = { + answers: [ + { + // This answer is missing its value field. + status: "correct", + maxError: 0, + simplify: "", + strict: false, + message: "", + }, + { + // This is the actual correct answer + value: 0.5, + status: "correct", + maxError: 0, + simplify: "", + strict: false, + message: "", + }, + ], + coefficient: true, + }; + + const score = scoreNumericInput({currentValue: "50%"}, rubric, mockStrings); + + expect(score).toHaveBeenAnsweredCorrectly(); + }) + + it("converts a percentage input value to a decimal", () => { + const rubric: PerseusNumericInputRubric = { + answers: [ + { + value: 0.2, + status: "correct", + maxError: 0, + simplify: "", + strict: false, + message: "", + }, + ], + coefficient: true, + }; + + const score = scoreNumericInput({currentValue: "20%"}, rubric, mockStrings); + + expect(score).toHaveBeenAnsweredCorrectly(); + }); + + it("rejects percentages greater than 100%", () => { + // TODO(benchristel): This seems like incorrect behavior. I've added + // this test to characterize the current behavior. Feel free to + // delete/change it if it's in your way. + const rubric: PerseusNumericInputRubric = { + answers: [ + { + value: 1.2, + status: "correct", + maxError: 0, + simplify: "", + strict: false, + message: "", + }, + ], + coefficient: true, + }; + + const score = scoreNumericInput({currentValue: "120%"}, rubric, mockStrings); + + expect(score).toHaveBeenAnsweredIncorrectly(); + }); + + it("accepts answers with an extra, incorrect percent sign if > 1", () => { + // TODO(benchristel): This seems like incorrect behavior. I've added + // this test to characterize the current behavior. Feel free to + // delete/change it if it's in your way. + const rubric: PerseusNumericInputRubric = { + answers: [ + { + value: 1.1, + status: "correct", + maxError: 0, + simplify: "", + strict: false, + message: "", + }, + ], + coefficient: true, + }; + + const score = scoreNumericInput({currentValue: "1.1%"}, rubric, mockStrings); + + expect(score).toHaveBeenAnsweredCorrectly(); + }); + + it("rejects answers with an extra, incorrect percent sign if < 1", () => { + const rubric: PerseusNumericInputRubric = { + answers: [ + { + value: 0.9, + status: "correct", + maxError: 0, + simplify: "", + strict: false, + message: "", + }, + ], + coefficient: true, + }; + + const score = scoreNumericInput({currentValue: "0.9%"}, rubric, mockStrings); + + expect(score).toHaveBeenAnsweredIncorrectly(); + }); }); describe("maybeParsePercentInput utility function", () => { diff --git a/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts b/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts index 46a98eaf12..256c2b021b 100644 --- a/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts +++ b/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts @@ -111,7 +111,7 @@ function scoreNumericInput( const normalizedAnswerExpected = rubric.answers .filter((answer) => answer.status === "correct") - .every((answer) => Math.abs(answer.value) <= 1); + .every((answer) => answer.value == null || Math.abs(answer.value) <= 1); // The coefficient is an attribute of the widget let localValue: string | number = currentValue;