Skip to content

Commit

Permalink
Plotter: Extract validation out of scoring (#1899)
Browse files Browse the repository at this point in the history
## Summary:
To complete server-side scoring, we are separating out validation logic from scoring logic. This PR separates that logic and associated tests for the plotter widget.

Issue: LEMS-2604

## Test plan:
- Confirm checks pass
- Confirm widget still works as expected

Author: Myranae

Reviewers: jeremywiebe, Myranae, handeyeco

Required Reviewers:

Approved By: jeremywiebe

Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1899
  • Loading branch information
Myranae authored Nov 26, 2024
1 parent 3a9b592 commit 2437ce6
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-fans-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Introduces a validation function for the plotter widget (extracted from the scoring function).
13 changes: 10 additions & 3 deletions packages/perseus/src/validation.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import type {
PerseusNumberLineWidgetOptions,
PerseusNumericInputAnswer,
PerseusOrdererWidgetOptions,
PerseusPlotterWidgetOptions,
PerseusRadioChoice,
PerseusGraphCorrectType,
} from "./perseus-types";
Expand Down Expand Up @@ -188,7 +187,15 @@ export type PerseusOrdererUserInput = {
current: ReadonlyArray<string>;
};

export type PerseusPlotterRubric = PerseusPlotterWidgetOptions;
export type PerseusPlotterScoringData = {
// The Y values that represent the correct answer expected
correct: ReadonlyArray<number>;
} & PerseusPlotterValidationData;

export type PerseusPlotterValidationData = {
// The Y values the graph should start with
starting: ReadonlyArray<number>;
};

export type PerseusPlotterUserInput = ReadonlyArray<number>;

Expand Down Expand Up @@ -236,7 +243,7 @@ export type Rubric =
| PerseusNumberLineRubric
| PerseusNumericInputRubric
| PerseusOrdererRubric
| PerseusPlotterRubric
| PerseusPlotterScoringData
| PerseusRadioRubric
| PerseusSorterRubric
| PerseusTableRubric;
Expand Down
4 changes: 2 additions & 2 deletions packages/perseus/src/widgets/plotter/plotter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<RenderProps, PerseusPlotterRubric> & {
type Props = WidgetProps<RenderProps, PerseusPlotterScoringData> & {
labelInterval: NonNullable<PerseusPlotterWidgetOptions["labelInterval"]>;
picSize: NonNullable<PerseusPlotterWidgetOptions["picSize"]>;
};
Expand Down
63 changes: 14 additions & 49 deletions packages/perseus/src/widgets/plotter/score-plotter.test.ts
Original file line number Diff line number Diff line change
@@ -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>,
): 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();
});
});
16 changes: 8 additions & 8 deletions packages/perseus/src/widgets/plotter/score-plotter.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import Util from "../../util";

import validatePlotter from "./validate-plotter";

import type {PerseusScore} from "../../types";
import type {
PerseusPlotterRubric,
PerseusPlotterScoringData,
PerseusPlotterUserInput,
} from "../../validation.types";

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,
};
Expand Down
38 changes: 38 additions & 0 deletions packages/perseus/src/widgets/plotter/validate-plotter.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
30 changes: 30 additions & 0 deletions packages/perseus/src/widgets/plotter/validate-plotter.ts
Original file line number Diff line number Diff line change
@@ -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<PerseusScore, {type: "invalid"}> | null {
if (deepEq(userInput, validationData.starting)) {
return {
type: "invalid",
message: null,
};
}
return null;
}

export default validatePlotter;

0 comments on commit 2437ce6

Please sign in to comment.