Skip to content

Commit

Permalink
Label-image: Extract validation out of scoring (#2016)
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 updates associated tests.

Issue: LEMS-2609

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

Author: Myranae

Reviewers: Myranae, handeyeco, jeremywiebe

Required Reviewers:

Approved By: handeyeco

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

Pull Request URL: #2016
  • Loading branch information
Myranae authored Dec 18, 2024
1 parent 0f2bec3 commit 55ad836
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 76 deletions.
5 changes: 5 additions & 0 deletions .changeset/thirty-hornets-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Introduces a validation function for the label-image widget (extracted from label-image scoring function).
63 changes: 0 additions & 63 deletions packages/perseus/src/widgets/label-image/score-label-image.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import scoreLabelImage, {scoreMarker} from "./score-label-image";

import type {
PerseusLabelImageScoringData,
PerseusLabelImageUserInput,
} from "../../validation.types";

describe("scoreMarker", function () {
it("should score correct for empty marker with no user answers", function () {
const score = scoreMarker([], []);
Expand Down Expand Up @@ -59,64 +54,6 @@ describe("scoreMarker", function () {
});

describe("scoreLabelImage", function () {
it("should not grade non-interacted widget", function () {
const userInput: PerseusLabelImageUserInput = {
markers: [{label: "England"}, {label: "Germany"}, {label: "Italy"}],
} as const;

const scoringData: PerseusLabelImageScoringData = {
markers: [
{
label: "England",
answers: ["Mini", "Morris Minor", "Reliant Robin"],
},
{
label: "Germany",
answers: ["BMW", "Volkswagen", "Porsche"],
},
{
label: "Italy",
answers: ["Lamborghini", "Fiat", "Ferrari"],
},
],
} as const;

const score = scoreLabelImage(userInput, scoringData);

expect(score).toHaveInvalidInput();
});

it("should not grade widget with not all markers answered", function () {
const userInput = {
markers: [
{label: "England", selected: ["Fiat"]},
{label: "Germany", selected: ["Lamborghini"]},
{label: "Italy"},
],
} as const;

const scoringData = {
markers: [
{
label: "England",
answers: [],
},
{
label: "Germany",
answers: ["BMW", "Volkswagen", "Porsche"],
},
{
label: "Italy",
answers: ["Lamborghini", "Fiat", "Ferrari"],
},
],
} as const;

const score = scoreLabelImage(userInput, scoringData);

expect(score).toHaveInvalidInput();
});

it("should grade as incorrect for widget with no answers for markers", function () {
const userInput = {
markers: [
Expand Down
20 changes: 7 additions & 13 deletions packages/perseus/src/widgets/label-image/score-label-image.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import validateLabelImage from "./validate-label-image";

import type {PerseusScore} from "../../types";
import type {
PerseusLabelImageScoringData,
Expand Down Expand Up @@ -44,7 +46,11 @@ function scoreLabelImage(
userInput: PerseusLabelImageUserInput,
scoringData: PerseusLabelImageScoringData,
): PerseusScore {
let numAnswered = 0;
const validationError = validateLabelImage(userInput);
if (validationError) {
return validationError;
}

let numCorrect = 0;

for (let i = 0; i < userInput.markers.length; i++) {
Expand All @@ -53,23 +59,11 @@ function scoreLabelImage(
scoringData.markers[i].answers,
);

if (score.hasAnswers) {
numAnswered++;
}

if (score.isCorrect) {
numCorrect++;
}
}

// We expect all question markers to be answered before grading.
if (numAnswered !== userInput.markers.length) {
return {
type: "invalid",
message: null,
};
}

return {
type: "points",
// Markers with no expected answers are graded as correct if user
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import validateLabelImage from "./validate-label-image";

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

describe("scoreLabelImage", () => {
it("should not grade non-interacted widget", function () {
const userInput: PerseusLabelImageUserInput = {
markers: [{label: "England"}, {label: "Germany"}, {label: "Italy"}],
} as const;

const validationError = validateLabelImage(userInput);

expect(validationError).toHaveInvalidInput();
});

it("should not grade widget with not all markers answered", function () {
const userInput = {
markers: [
{label: "England", selected: ["Fiat"]},
{label: "Germany", selected: ["Lamborghini"]},
{label: "Italy"},
],
} as const;

const validationError = validateLabelImage(userInput);

expect(validationError).toHaveInvalidInput();
});
});
25 changes: 25 additions & 0 deletions packages/perseus/src/widgets/label-image/validate-label-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {PerseusScore} from "../../types";
import type {PerseusLabelImageUserInput} from "../../validation.types";

function validateLabelImage(
userInput: PerseusLabelImageUserInput,
): Extract<PerseusScore, {type: "invalid"}> | null {
let numAnswered = 0;
for (let i = 0; i < userInput.markers.length; i++) {
const userSelection = userInput.markers[i].selected;
if (userSelection && userSelection.length > 0) {
numAnswered++;
}
}
// We expect all question markers to be answered before grading.
if (numAnswered !== userInput.markers.length) {
return {
type: "invalid",
message: null,
};
}

return null;
}

export default validateLabelImage;

0 comments on commit 55ad836

Please sign in to comment.