diff --git a/.changeset/nice-fans-swim.md b/.changeset/nice-fans-swim.md new file mode 100644 index 0000000000..04c7fe08ce --- /dev/null +++ b/.changeset/nice-fans-swim.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Introduces a validation function for the plotter widget (extracted from the scoring function). diff --git a/packages/perseus/src/validation.types.ts b/packages/perseus/src/validation.types.ts index 68674223c7..924df38960 100644 --- a/packages/perseus/src/validation.types.ts +++ b/packages/perseus/src/validation.types.ts @@ -40,7 +40,6 @@ import type { PerseusNumberLineWidgetOptions, PerseusNumericInputAnswer, PerseusOrdererWidgetOptions, - PerseusPlotterWidgetOptions, PerseusRadioChoice, PerseusGraphCorrectType, } from "./perseus-types"; @@ -185,7 +184,15 @@ export type PerseusOrdererUserInput = { current: ReadonlyArray; }; -export type PerseusPlotterRubric = PerseusPlotterWidgetOptions; +export type PerseusPlotterScoringData = { + // The Y values that represent the correct answer expected + correct: ReadonlyArray; +} & PerseusPlotterValidationData; + +export type PerseusPlotterValidationData = { + // The Y values the graph should start with + starting: ReadonlyArray; +}; export type PerseusPlotterUserInput = ReadonlyArray; @@ -233,7 +240,7 @@ export type Rubric = | PerseusNumberLineRubric | PerseusNumericInputRubric | PerseusOrdererRubric - | PerseusPlotterRubric + | PerseusPlotterScoringData | PerseusRadioRubric | PerseusSorterRubric | PerseusTableRubric; diff --git a/packages/perseus/src/widgets/plotter/plotter.tsx b/packages/perseus/src/widgets/plotter/plotter.tsx index a0b1178d6c..8e985698ac 100644 --- a/packages/perseus/src/widgets/plotter/plotter.tsx +++ b/packages/perseus/src/widgets/plotter/plotter.tsx @@ -18,14 +18,14 @@ import scorePlotter from "./score-plotter"; import type {PerseusPlotterWidgetOptions} from "../../perseus-types"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; import type { - PerseusPlotterRubric, + PerseusPlotterScoringData, PerseusPlotterUserInput, } from "../../validation.types"; import type {UnsupportedWidgetPromptJSON} from "../../widget-ai-utils/unsupported-widget"; type RenderProps = PerseusPlotterWidgetOptions; -type Props = WidgetProps & { +type Props = WidgetProps & { labelInterval: NonNullable; picSize: NonNullable; }; diff --git a/packages/perseus/src/widgets/plotter/score-plotter.test.ts b/packages/perseus/src/widgets/plotter/score-plotter.test.ts index 381caf1847..96dc8595fc 100644 --- a/packages/perseus/src/widgets/plotter/score-plotter.test.ts +++ b/packages/perseus/src/widgets/plotter/score-plotter.test.ts @@ -1,75 +1,40 @@ import scorePlotter from "./score-plotter"; import type { - PerseusPlotterRubric, + PerseusPlotterScoringData, PerseusPlotterUserInput, } from "../../validation.types"; -const baseRubric: PerseusPlotterRubric = { - categories: [ - "$1^{\\text{st}} \\text{}$", - "$2^{\\text{nd}} \\text{}$", - "$3^{\\text{rd}} \\text{}$", - "$4^{\\text{th}} \\text{}$", - "$5^{\\text{th}} \\text{}$", - ], - picBoxHeight: 300, - picSize: 300, - picUrl: "", - plotDimensions: [0, 0], - correct: [15, 25, 5, 10, 10], - labelInterval: 1, - labels: ["School grade", "Number of absent students"], - maxY: 30, - scaleY: 5, - snapsPerLine: 1, - starting: [0, 0, 0, 0, 0], - type: "bar", -}; - -function generateRubric( - extend?: Partial, -): PerseusPlotterRubric { - return {...baseRubric, ...extend}; -} - describe("scorePlotter", () => { - it("is invalid if the start and end are the same", () => { - // Arrange - const rubric = generateRubric(); - - const userInput: PerseusPlotterUserInput = rubric.starting; - - // Act - const result = scorePlotter(userInput, rubric); - - // Assert - expect(result).toHaveInvalidInput(); - }); - it("can be answered correctly", () => { // Arrange - const rubric = generateRubric(); + const scoringData: PerseusPlotterScoringData = { + correct: [15, 25, 5, 10, 10], + starting: [0, 0, 0, 0, 0], + }; - const userInput: PerseusPlotterUserInput = rubric.correct; + const userInput: PerseusPlotterUserInput = scoringData.correct; // Act - const result = scorePlotter(userInput, rubric); + const score = scorePlotter(userInput, scoringData); // Assert - expect(result).toHaveBeenAnsweredCorrectly(); + expect(score).toHaveBeenAnsweredCorrectly(); }); it("can be answered incorrectly", () => { // Arrange - const rubric = generateRubric(); + const scoringData: PerseusPlotterScoringData = { + correct: [15, 25, 5, 10, 10], + starting: [0, 0, 0, 0, 0], + }; const userInput: PerseusPlotterUserInput = [8, 6, 7, 5, 3, 0, 9]; // Act - const result = scorePlotter(userInput, rubric); + const score = scorePlotter(userInput, scoringData); // Assert - expect(result).toHaveBeenAnsweredIncorrectly(); + expect(score).toHaveBeenAnsweredIncorrectly(); }); }); diff --git a/packages/perseus/src/widgets/plotter/score-plotter.ts b/packages/perseus/src/widgets/plotter/score-plotter.ts index 0af3432de6..41674387e9 100644 --- a/packages/perseus/src/widgets/plotter/score-plotter.ts +++ b/packages/perseus/src/widgets/plotter/score-plotter.ts @@ -1,8 +1,10 @@ import Util from "../../util"; +import validatePlotter from "./validate-plotter"; + import type {PerseusScore} from "../../types"; import type { - PerseusPlotterRubric, + PerseusPlotterScoringData, PerseusPlotterUserInput, } from "../../validation.types"; @@ -10,17 +12,15 @@ const {deepEq} = Util; function scorePlotter( userInput: PerseusPlotterUserInput, - rubric: PerseusPlotterRubric, + scoringData: PerseusPlotterScoringData, ): PerseusScore { - if (deepEq(userInput, rubric.starting)) { - return { - type: "invalid", - message: null, - }; + const validationError = validatePlotter(userInput, scoringData); + if (validationError) { + return validationError; } return { type: "points", - earned: deepEq(userInput, rubric.correct) ? 1 : 0, + earned: deepEq(userInput, scoringData.correct) ? 1 : 0, total: 1, message: null, }; diff --git a/packages/perseus/src/widgets/plotter/validate-plotter.test.ts b/packages/perseus/src/widgets/plotter/validate-plotter.test.ts new file mode 100644 index 0000000000..a4c298f351 --- /dev/null +++ b/packages/perseus/src/widgets/plotter/validate-plotter.test.ts @@ -0,0 +1,38 @@ +import validatePlotter from "./validate-plotter"; + +import type { + PerseusPlotterUserInput, + PerseusPlotterValidationData, +} from "../../validation.types"; + +describe("validatePlotter", () => { + it("is invalid if the start and end are the same", () => { + // Arrange + const validationData: PerseusPlotterValidationData = { + starting: [0, 0, 0, 0, 0], + }; + + const userInput: PerseusPlotterUserInput = validationData.starting; + + // Act + const validationError = validatePlotter(userInput, validationData); + + // Assert + expect(validationError).toHaveInvalidInput(); + }); + + it("returns null if the start and end are not the same and the user has modified the graph", () => { + // Arrange + const validationData: PerseusPlotterValidationData = { + starting: [0, 0, 0, 0, 0], + }; + + const userInput: PerseusPlotterUserInput = [0, 1, 2, 3, 4]; + + // Act + const validationError = validatePlotter(userInput, validationData); + + // Assert + expect(validationError).toBeNull(); + }); +}); diff --git a/packages/perseus/src/widgets/plotter/validate-plotter.ts b/packages/perseus/src/widgets/plotter/validate-plotter.ts new file mode 100644 index 0000000000..0904ba8f8e --- /dev/null +++ b/packages/perseus/src/widgets/plotter/validate-plotter.ts @@ -0,0 +1,30 @@ +import Util from "../../util"; + +import type {PerseusScore} from "../../types"; +import type { + PerseusPlotterUserInput, + PerseusPlotterValidationData, +} from "../../validation.types"; + +const {deepEq} = Util; + +/** + * Checks user input to confirm it is not the same as the starting values for the graph. + * This means the user has modified the graph, and the question can be scored. + * + * @see 'scorePlotter' for more details on scoring. + */ +function validatePlotter( + userInput: PerseusPlotterUserInput, + validationData: PerseusPlotterValidationData, +): Extract | null { + if (deepEq(userInput, validationData.starting)) { + return { + type: "invalid", + message: null, + }; + } + return null; +} + +export default validatePlotter;