Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/giant-fireants-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus-core": major
"@khanacademy/perseus-score": minor
---

Add new validateUserInput function to perseus-score
22 changes: 2 additions & 20 deletions packages/perseus-core/src/validation.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {
PerseusGraphCorrectType,
MakeWidgetMap,
PerseusFreeResponseWidgetScoringCriterion,
PerseusRenderer,
} from "./data-schema";
import type {ErrorCode} from "./error-codes";
import type {Relationship} from "./types";
Expand Down Expand Up @@ -118,7 +119,7 @@ export type PerseusExpressionRubric = {
export type PerseusExpressionUserInput = string;

export type PerseusGroupRubric = PerseusGroupWidgetOptions;
export type PerseusGroupValidationData = {widgets: ValidationDataMap};
export type PerseusGroupValidationData = PerseusRenderer;
export type PerseusGroupUserInput = UserInputMap;

export type PerseusGradedGroupRubric = PerseusGradedGroupWidgetOptions;
Expand Down Expand Up @@ -378,25 +379,6 @@ export interface ValidationDataTypes {
plotter: PerseusPlotterValidationData;
}

/**
* A map of validation data, keyed by `widgetId`. This data is used to check if
* a question is answerable. This data represents the minimal intersection of
* data that's available in the client (widget options) and server (scoring
* data) and is represented by a group of types known as "validation data".
*
* NOTE: The value in this map is intentionally a subset of WidgetOptions<T>.
* By using the same shape (minus any unneeded data), we are able to pass a
* `PerseusWidgetsMap` or ` into any function that accepts a
* `ValidationDataMap` without any mutation of data.
*/
export type ValidationDataMap = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just didn't see the point of having this type and it was keeping me from reusing helpers between scoring and validation logic. Ideally in the future we could just have a AnswerfulPerseusItem and AnswerlessPerseusItem or something.

[Property in keyof ValidationDataTypes as `${Property} ${number}`]: {
type: Property;
static?: boolean;
options: ValidationDataTypes[Property];
};
};

/**
* A union type of all the different widget validation data types that exist.
*/
Expand Down
25 changes: 0 additions & 25 deletions packages/perseus-core/src/validation.typetest.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/perseus-score/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ export {
inputNumberAnswerTypes,
} from "./widgets/input-number/score-input-number";

export {scorePerseusItem, scoreWidgetsFunctional, flattenScores} from "./score";
export {emptyWidgetsFunctional} from "./validate";
export {scorePerseusItem, scoreWidgetsFunctional} from "./score";
export {default as flattenScores} from "./util/flatten-scores";
export {validateUserInput, emptyWidgetsFunctional} from "./validate";
export {default as hasEmptyDINERWidgets} from "./has-empty-diner-widgets";

export type {
Expand Down
120 changes: 4 additions & 116 deletions packages/perseus-score/src/score.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,124 +4,9 @@ import {
type UserInputMap,
} from "@khanacademy/perseus-core";

import {flattenScores, scorePerseusItem, scoreWidgetsFunctional} from "./score";
import {scorePerseusItem, scoreWidgetsFunctional} from "./score";
import {getExpressionWidget, getTestDropdownWidget} from "./util/test-helpers";

describe("flattenScores", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved

it("defaults to an empty score", () => {
const result = flattenScores({});

expect(result).toHaveBeenAnsweredCorrectly({shouldHavePoints: false});
expect(result).toEqual({
type: "points",
total: 0,
earned: 0,
message: null,
});
});

it("defaults to single score if there is only one", () => {
const result = flattenScores({
"radio 1": {
type: "points",
total: 1,
earned: 1,
message: null,
},
});

expect(result).toHaveBeenAnsweredCorrectly();
expect(result).toEqual({
type: "points",
total: 1,
earned: 1,
message: null,
});
});

it("returns an invalid score if any are invalid", () => {
const result = flattenScores({
"radio 1": {
type: "points",
total: 1,
earned: 1,
message: null,
},
"radio 2": {
type: "invalid",
message: null,
},
});

expect(result).toHaveInvalidInput();
expect(result).toEqual({
type: "invalid",
message: null,
});
});

it("tallies scores if multiple widgets have points", () => {
const result = flattenScores({
"radio 1": {
type: "points",
total: 1,
earned: 1,
message: null,
},
"radio 2": {
type: "points",
total: 1,
earned: 1,
message: null,
},
"radio 3": {
type: "points",
total: 1,
earned: 1,
message: null,
},
});

expect(result).toHaveBeenAnsweredCorrectly();
expect(result).toEqual({
type: "points",
total: 3,
earned: 3,
message: null,
});
});

it("doesn't count incorrect widgets", () => {
const result = flattenScores({
"radio 1": {
type: "points",
total: 1,
earned: 1,
message: null,
},
"radio 2": {
type: "points",
total: 1,
earned: 1,
message: null,
},
"radio 3": {
type: "points",
total: 1,
earned: 0,
message: null,
},
});

expect(result).toEqual({
type: "points",
total: 3,
earned: 2,
message: null,
});
});
});

describe("scoreWidgetsFunctional", () => {
it("returns an empty object when there's no widgets", () => {
// Arrange / Act
Expand Down Expand Up @@ -432,6 +317,7 @@ describe("scorePerseusItem", () => {
const score = scorePerseusItem(item, userInputMap, "en");

// Assert:
expect(score).toHaveInvalidInput();
expect(score).toEqual({type: "invalid", message: null});
});

Expand All @@ -454,6 +340,7 @@ describe("scorePerseusItem", () => {
);

// Assert
expect(score).toHaveInvalidInput();
expect(score).toEqual({type: "invalid", message: null});
});

Expand All @@ -478,6 +365,7 @@ describe("scorePerseusItem", () => {
);

// Assert
expect(score).toHaveBeenAnsweredCorrectly();
expect(score).toEqual({
type: "points",
total: 2,
Expand Down
Loading
Loading