diff --git a/.changeset/mighty-students-pay.md b/.changeset/mighty-students-pay.md new file mode 100644 index 0000000000..8691f7b767 --- /dev/null +++ b/.changeset/mighty-students-pay.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Move getOneCorrectAnswerFromRubric from React components to WidgetExports diff --git a/packages/perseus/src/__tests__/widgets.test.ts b/packages/perseus/src/__tests__/widgets.test.ts index 4a28df6bdd..5c81061af0 100644 --- a/packages/perseus/src/__tests__/widgets.test.ts +++ b/packages/perseus/src/__tests__/widgets.test.ts @@ -26,14 +26,6 @@ describe("Widget API support", () => { expect(Widget).toHaveProperty("getUserInputFromProps"); }, ); - - it.each(["expression", "input-number", "numeric-input"])( - "%s widget should provide static getOneCorrectAnswerFromRubric function", - (widgetType) => { - const Widget = Widgets.getWidget(widgetType); - expect(Widget).toHaveProperty("getOneCorrectAnswerFromRubric"); - }, - ); }); describe("replaceWidget", () => { diff --git a/packages/perseus/src/types.ts b/packages/perseus/src/types.ts index 31b3658e9a..46b432895d 100644 --- a/packages/perseus/src/types.ts +++ b/packages/perseus/src/types.ts @@ -630,6 +630,9 @@ export type WidgetExports< staticTransform?: WidgetTransform; // this is a function of some sort, validator?: WidgetValidatorFunction; + getOneCorrectAnswerFromRubric?: ( + rubric: Rubric, + ) => string | null | undefined; /** A map of major version numbers (as a string, eg "1") to a function that diff --git a/packages/perseus/src/util/extract-perseus-data.ts b/packages/perseus/src/util/extract-perseus-data.ts index f00b0be43b..9b99e1f263 100644 --- a/packages/perseus/src/util/extract-perseus-data.ts +++ b/packages/perseus/src/util/extract-perseus-data.ts @@ -678,14 +678,13 @@ export const getAnswerFromUserInput = (widgetType: string, userInput: any) => { export const getCorrectAnswerForWidgetId = ( widgetId: string, itemData: PerseusItem, -): string | undefined => { +): string | null | undefined => { const rubric = itemData.question.widgets[widgetId].options; const widgetMap = getWidgetsMapFromItemData(itemData); const widgetType = getWidgetTypeByWidgetId(widgetId, widgetMap) as string; - const widget = Widgets.getWidget(widgetType); + const widget = Widgets.getWidgetExport(widgetType); - // @ts-expect-error - TS2339 - Property 'getOneCorrectAnswerFromRubric' does not exist on type 'ComponentType'. return widget?.getOneCorrectAnswerFromRubric?.(rubric); }; diff --git a/packages/perseus/src/widgets.ts b/packages/perseus/src/widgets.ts index 57f7b440d0..ed1af2276a 100644 --- a/packages/perseus/src/widgets.ts +++ b/packages/perseus/src/widgets.ts @@ -133,6 +133,10 @@ export const getWidget = ( return widgets[name].widget; }; +export const getWidgetExport = (name: string): WidgetExports | null => { + return widgets[name] ?? null; +}; + export const getWidgetValidator = ( name: string, ): WidgetValidatorFunction | null => { diff --git a/packages/perseus/src/widgets/expression/expression.test.tsx b/packages/perseus/src/widgets/expression/expression.test.tsx index 7ad755d0da..be8d76b63c 100644 --- a/packages/perseus/src/widgets/expression/expression.test.tsx +++ b/packages/perseus/src/widgets/expression/expression.test.tsx @@ -9,7 +9,7 @@ import { import * as Dependencies from "../../dependencies"; import {renderQuestion} from "../__testutils__/renderQuestion"; -import {Expression} from "./expression"; +import ExpressionWidgetExport from "./expression"; import { expressionItem2, expressionItem3, @@ -264,7 +264,8 @@ describe("Expression Widget", function () { } as const; // Act - const result = Expression.getOneCorrectAnswerFromRubric(rubric); + const result = + ExpressionWidgetExport.getOneCorrectAnswerFromRubric?.(rubric); // Assert expect(result).toBeUndefined(); @@ -287,7 +288,8 @@ describe("Expression Widget", function () { } as const; // Act - const result = Expression.getOneCorrectAnswerFromRubric(rubric); + const result = + ExpressionWidgetExport.getOneCorrectAnswerFromRubric?.(rubric); // Assert expect(result).toEqual("123"); @@ -316,7 +318,8 @@ describe("Expression Widget", function () { } as const; // Act - const result = Expression.getOneCorrectAnswerFromRubric(rubric); + const result = + ExpressionWidgetExport.getOneCorrectAnswerFromRubric?.(rubric); // Assert expect(result).toEqual("123"); diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx index 54f2e78dc3..2dc06dc088 100644 --- a/packages/perseus/src/widgets/expression/expression.tsx +++ b/packages/perseus/src/widgets/expression/expression.tsx @@ -115,19 +115,6 @@ export class Expression return normalizeTex(props.value); } - static getOneCorrectAnswerFromRubric( - rubric: PerseusExpressionRubric, - ): string | null | undefined { - const correctAnswers = (rubric.answerForms || []).filter( - (answerForm) => answerForm.considered === "correct", - ); - if (correctAnswers.length === 0) { - return; - } - return correctAnswers[0].value; - } - //#endregion - static defaultProps: DefaultProps = { value: "", times: false, @@ -532,13 +519,8 @@ const ExpressionWithDependencies = React.forwardRef< // methods and instead adjust Perseus to provide these facilities through // instance methods on our Renderers. // @ts-expect-error - TS2339 - Property 'validate' does not exist on type -ExpressionWithDependencies.validate = expressionValidator; -// @ts-expect-error - TS2339 - Property 'validate' does not exist on type ExpressionWithDependencies.getUserInputFromProps = Expression.getUserInputFromProps; -// @ts-expect-error - TS2339 - Property 'validate' does not exist on type -ExpressionWithDependencies.getOneCorrectAnswerFromRubric = - Expression.getOneCorrectAnswerFromRubric; export default { name: "expression", @@ -571,4 +553,16 @@ export default { // For use by the editor isLintable: true, validator: expressionValidator, + + getOneCorrectAnswerFromRubric( + rubric: PerseusExpressionRubric, + ): string | null | undefined { + const correctAnswers = (rubric.answerForms || []).filter( + (answerForm) => answerForm.considered === "correct", + ); + if (correctAnswers.length === 0) { + return; + } + return correctAnswers[0].value; + }, } as WidgetExports; diff --git a/packages/perseus/src/widgets/input-number/input-number.test.ts b/packages/perseus/src/widgets/input-number/input-number.test.ts index e838b2a7dd..31db9a3c31 100644 --- a/packages/perseus/src/widgets/input-number/input-number.test.ts +++ b/packages/perseus/src/widgets/input-number/input-number.test.ts @@ -276,7 +276,7 @@ describe("getOneCorrectAnswerFromRubric", () => { const rubric: Record = {}; // Act - const result = InputNumber.widget.getOneCorrectAnswerFromRubric(rubric); + const result = InputNumber.getOneCorrectAnswerFromRubric?.(rubric); // Assert expect(result).toBeUndefined(); @@ -291,7 +291,7 @@ describe("getOneCorrectAnswerFromRubric", () => { } as const; // Act - const result = InputNumber.widget.getOneCorrectAnswerFromRubric(rubric); + const result = InputNumber.getOneCorrectAnswerFromRubric?.(rubric); // Assert expect(result).toEqual("0"); @@ -306,7 +306,7 @@ describe("getOneCorrectAnswerFromRubric", () => { } as const; // Act - const result = InputNumber.widget.getOneCorrectAnswerFromRubric(rubric); + const result = InputNumber.getOneCorrectAnswerFromRubric?.(rubric); // Assert expect(result).toEqual("0 ± 0.1"); diff --git a/packages/perseus/src/widgets/input-number/input-number.tsx b/packages/perseus/src/widgets/input-number/input-number.tsx index 2e49049603..4b8a5d916f 100644 --- a/packages/perseus/src/widgets/input-number/input-number.tsx +++ b/packages/perseus/src/widgets/input-number/input-number.tsx @@ -100,19 +100,6 @@ class InputNumber extends React.Component implements Widget { }; } - static getOneCorrectAnswerFromRubric( - rubric: any, - ): string | null | undefined { - if (rubric.value == null) { - return; - } - let answerString = String(rubric.value); - if (rubric.inexact && rubric.maxError) { - answerString += " \u00B1 " + rubric.maxError; - } - return answerString; - } - shouldShowExamples: () => boolean = () => { return this.props.answerType !== "number"; }; @@ -297,4 +284,15 @@ export default { transform: propTransform, isLintable: true, validator: inputNumberValidator, + + getOneCorrectAnswerFromRubric(rubric: any): string | null | undefined { + if (rubric.value == null) { + return; + } + let answerString = String(rubric.value); + if (rubric.inexact && rubric.maxError) { + answerString += " \u00B1 " + rubric.maxError; + } + return answerString; + }, } as WidgetExports; diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts b/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts index 0308d628f6..88bda6c402 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts @@ -5,7 +5,7 @@ import {testDependencies} from "../../../../../testing/test-dependencies"; import * as Dependencies from "../../dependencies"; import {renderQuestion} from "../__testutils__/renderQuestion"; -import {NumericInput, unionAnswerForms} from "./numeric-input"; +import NumericInputWidgetExport, {unionAnswerForms} from "./numeric-input"; import { question1AndAnswer, multipleAnswers, @@ -90,13 +90,14 @@ describe("static function getOneCorrectAnswerFromRubric", () => { const answers: ReadonlyArray = (widgetOptions && widgetOptions.answers) || []; - const singleAnswer = NumericInput.getOneCorrectAnswerFromRubric({ - answers, - labelText: "", - size: "medium", - static: false, - coefficient: false, - }); + const singleAnswer = + NumericInputWidgetExport.getOneCorrectAnswerFromRubric?.({ + answers, + labelText: "", + size: "medium", + static: false, + coefficient: false, + }); expect(singleAnswer).toBe("12.2"); }); @@ -105,25 +106,27 @@ describe("static function getOneCorrectAnswerFromRubric", () => { const widgetOptions = widget && widget.options; const answers: ReadonlyArray = (widgetOptions && widgetOptions.answers) || []; - const singleAnswer = NumericInput.getOneCorrectAnswerFromRubric({ - answers, - labelText: "", - size: "medium", - static: false, - coefficient: false, - }); + const singleAnswer = + NumericInputWidgetExport.getOneCorrectAnswerFromRubric?.({ + answers, + labelText: "", + size: "medium", + static: false, + coefficient: false, + }); expect(singleAnswer).toBe("1252"); }); it("can not get a correct answer from a rubric with no answer", () => { const answers: Array = []; - const singleAnswer = NumericInput.getOneCorrectAnswerFromRubric({ - answers, - labelText: "", - size: "medium", - static: false, - coefficient: false, - }); + const singleAnswer = + NumericInputWidgetExport.getOneCorrectAnswerFromRubric?.({ + answers, + labelText: "", + size: "medium", + static: false, + coefficient: false, + }); expect(singleAnswer).toBeUndefined(); }); @@ -145,7 +148,8 @@ describe("static function getOneCorrectAnswerFromRubric", () => { static: false, coefficient: true, }; - const singleAnswer = NumericInput.getOneCorrectAnswerFromRubric(rubric); + const singleAnswer = + NumericInputWidgetExport.getOneCorrectAnswerFromRubric?.(rubric); expect(singleAnswer).toBe("1 ± 0.2"); }); }); diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx index e0755a5a35..b2606319f8 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx @@ -101,40 +101,6 @@ export class NumericInput }; } - static getOneCorrectAnswerFromRubric( - rubric: PerseusNumericInputRubric, - ): string | null | undefined { - const correctAnswers = rubric.answers.filter( - (answer) => answer.status === "correct", - ); - const answerStrings = correctAnswers.map((answer) => { - // Figure out how this answer is supposed to be - // displayed - let format = "decimal"; - if (answer.answerForms && answer.answerForms[0]) { - // NOTE(johnsullivan): This isn't exactly ideal, but - // it does behave well for all the currently known - // problems. See D14742 for some discussion on - // alternate strategies. - format = answer.answerForms[0]; - } - - // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'MathFormat | undefined'. - let answerString = KhanMath.toNumericString(answer.value, format); - if (answer.maxError) { - answerString += - " \u00B1 " + - // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'MathFormat | undefined'. - KhanMath.toNumericString(answer.maxError, format); - } - return answerString; - }); - if (answerStrings.length === 0) { - return; - } - return answerStrings[0]; - } - state: State = { // keeps track of the other set of values when switching // between 0 and finite solutions @@ -384,4 +350,38 @@ export default { transform: propsTransform, isLintable: true, validator: numericInputValidator, + + getOneCorrectAnswerFromRubric( + rubric: PerseusNumericInputRubric, + ): string | null | undefined { + const correctAnswers = rubric.answers.filter( + (answer) => answer.status === "correct", + ); + const answerStrings = correctAnswers.map((answer) => { + // Figure out how this answer is supposed to be + // displayed + let format = "decimal"; + if (answer.answerForms && answer.answerForms[0]) { + // NOTE(johnsullivan): This isn't exactly ideal, but + // it does behave well for all the currently known + // problems. See D14742 for some discussion on + // alternate strategies. + format = answer.answerForms[0]; + } + + // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'MathFormat | undefined'. + let answerString = KhanMath.toNumericString(answer.value, format); + if (answer.maxError) { + answerString += + " \u00B1 " + + // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'MathFormat | undefined'. + KhanMath.toNumericString(answer.maxError, format); + } + return answerString; + }); + if (answerStrings.length === 0) { + return; + } + return answerStrings[0]; + }, } as WidgetExports;