Skip to content

Commit

Permalink
Orderer: Extract validation out of scoring (#1869)
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 separates that logic and updates associated tests.

Issue: LEMS-2603

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

Author: Myranae

Reviewers: jeremywiebe, handeyeco, Myranae

Required Reviewers:

Approved By: jeremywiebe, handeyeco

Checks: ✅ Publish npm snapshot (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), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (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), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (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), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1869
  • Loading branch information
Myranae authored Nov 26, 2024
1 parent 33ccbd2 commit f43edd4
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-radios-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Split out validation function for the `orderer` widget. This can be used to check if the user has ordered at least one option, confirming the question is ready to be scored.
43 changes: 37 additions & 6 deletions packages/perseus/src/widgets/orderer/score-orderer.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {question1} from "./orderer.testdata";
import {scoreOrderer} from "./score-orderer";
import * as OrdererValidator from "./validate-orderer";

import type {
PerseusOrdererRubric,
PerseusOrdererUserInput,
} from "../../validation.types";

describe("ordererValiator", () => {
describe("scoreOrderer", () => {
it("is correct when the userInput is in the same order and is the same length as the rubric's correctOption content items", () => {
// Arrange
const rubric: PerseusOrdererRubric =
Expand All @@ -21,7 +22,7 @@ describe("ordererValiator", () => {
// Act
const result = scoreOrderer(userInput, rubric);

// assert
// Assert
expect(result).toHaveBeenAnsweredCorrectly();
});

Expand All @@ -37,7 +38,7 @@ describe("ordererValiator", () => {
// Act
const result = scoreOrderer(userInput, rubric);

// assert
// Assert
expect(result).toHaveBeenAnsweredIncorrectly();
});

Expand All @@ -53,12 +54,41 @@ describe("ordererValiator", () => {
// Act
const result = scoreOrderer(userInput, rubric);

// assert
// Assert
expect(result).toHaveBeenAnsweredIncorrectly();
});

it("is invalid when the when the user has not started ordering the options and current is empty", () => {
it("should be correctly answerable if validation passes", () => {
// Arrange
const mockValidator = jest
.spyOn(OrdererValidator, "default")
.mockReturnValue(null);

const rubric: PerseusOrdererRubric =
question1.widgets["orderer 1"].options;

const userInput: PerseusOrdererUserInput = {
current: question1.widgets["orderer 1"].options.correctOptions.map(
(option) => option.content,
),
};
// Act
const result = scoreOrderer(userInput, rubric);

// Assert
expect(mockValidator).toHaveBeenCalledWith(userInput);
expect(result).toHaveBeenAnsweredCorrectly();
});

it("should return an invalid response if validation fails", () => {
// Arrange
const mockValidator = jest
.spyOn(OrdererValidator, "default")
.mockReturnValue({
type: "invalid",
message: null,
});

const rubric: PerseusOrdererRubric =
question1.widgets["orderer 1"].options;

Expand All @@ -69,7 +99,8 @@ describe("ordererValiator", () => {
// Act
const result = scoreOrderer(userInput, rubric);

// assert
// Assert
expect(mockValidator).toHaveBeenCalledWith(userInput);
expect(result).toHaveInvalidInput();
});
});
12 changes: 6 additions & 6 deletions packages/perseus/src/widgets/orderer/score-orderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import _ from "underscore";

import validateOrderer from "./validate-orderer";

import type {PerseusScore} from "../../types";
import type {
PerseusOrdererRubric,
Expand All @@ -10,16 +12,14 @@ export function scoreOrderer(
userInput: PerseusOrdererUserInput,
rubric: PerseusOrdererRubric,
): PerseusScore {
if (userInput.current.length === 0) {
return {
type: "invalid",
message: null,
};
const validateError = validateOrderer(userInput);
if (validateError) {
return validateError;
}

const correct = _.isEqual(
userInput.current,
_.pluck(rubric.correctOptions, "content"),
rubric.correctOptions.map((option) => option.content),
);

return {
Expand Down
31 changes: 31 additions & 0 deletions packages/perseus/src/widgets/orderer/validate-orderer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import validateOrderer from "./validate-orderer";

import type {PerseusOrdererUserInput} from "../../validation.types";

describe("validateOrderer", () => {
it("is invalid when the user has not started ordering the options and current is empty", () => {
// Arrange
const userInput: PerseusOrdererUserInput = {
current: [],
};

// Act
const result = validateOrderer(userInput);

// Assert
expect(result).toHaveInvalidInput();
});

it("is null when the user has started ordering the options and current has at least one option", () => {
// Arrange
const userInput: PerseusOrdererUserInput = {
current: ["$10.9$"],
};

// Act
const result = validateOrderer(userInput);

// Assert
expect(result).toBeNull();
});
});
23 changes: 23 additions & 0 deletions packages/perseus/src/widgets/orderer/validate-orderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {PerseusScore} from "../../types";
import type {PerseusOrdererUserInput} from "../../validation.types";

/**
* Checks user input from the orderer widget to see if the user has started
* ordering the options, making the widget scorable.
* @param userInput
* @see `scoreOrderer` for more details.
*/
function validateOrderer(
userInput: PerseusOrdererUserInput,
): Extract<PerseusScore, {type: "invalid"}> | null {
if (userInput.current.length === 0) {
return {
type: "invalid",
message: null,
};
}

return null;
}

export default validateOrderer;

0 comments on commit f43edd4

Please sign in to comment.