Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plotter: Extract validation out of scoring #1899

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -40,7 +40,6 @@ import type {
PerseusNumberLineWidgetOptions,
PerseusNumericInputAnswer,
PerseusOrdererWidgetOptions,
PerseusPlotterWidgetOptions,
PerseusRadioChoice,
PerseusGraphCorrectType,
} from "./perseus-types";
Expand Down Expand Up @@ -185,7 +184,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 @@ -233,7 +240,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;