From 567ed2ea3fa7630975a131948077336a4935105e Mon Sep 17 00:00:00 2001 From: Sarah Third Date: Tue, 31 Mar 2026 17:05:45 -0700 Subject: [PATCH] [LEMS-3971/vector-pr4] docs(changeset): Added ability to score Vector Interactive Graphs --- .changeset/lazy-bats-complain.md | 5 + .../score-interactive-graph.test.ts | 101 ++++++++++++++++++ .../score-interactive-graph.ts | 25 +++++ 3 files changed, 131 insertions(+) create mode 100644 .changeset/lazy-bats-complain.md diff --git a/.changeset/lazy-bats-complain.md b/.changeset/lazy-bats-complain.md new file mode 100644 index 00000000000..cc90879d13f --- /dev/null +++ b/.changeset/lazy-bats-complain.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus-score": minor +--- + +Added ability to score Vector Interactive Graphs diff --git a/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.test.ts b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.test.ts index db32ac5c7d3..3e9cdbade0a 100644 --- a/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.test.ts +++ b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.test.ts @@ -932,3 +932,104 @@ describe("InteractiveGraph scoring on a tangent question", () => { expect(result).toHaveBeenAnsweredCorrectly(); }); }); + +const vectorRubric: PerseusInteractiveGraphRubric = { + graph: {type: "vector"}, + correct: { + type: "vector", + coords: [ + [0, 0], + [3, 4], + ], + }, +}; + +describe("InteractiveGraph scoring on a vector question", () => { + it("marks the answer invalid if guess is undefined", () => { + // Arrange, Act + const result = scoreInteractiveGraph(undefined, vectorRubric); + + // Assert + expect(result).toHaveInvalidInput(); + }); + + it("marks the answer invalid if coords are missing", () => { + // Arrange + const guess: PerseusGraphType = {type: "vector"}; + + // Act + const result = scoreInteractiveGraph(guess, vectorRubric); + + // Assert + expect(result).toHaveInvalidInput(); + }); + + it("marks a correct answer as correct", () => { + // Arrange + const guess: PerseusGraphType = { + type: "vector", + coords: [ + [0, 0], + [3, 4], + ], + }; + + // Act + const result = scoreInteractiveGraph(guess, vectorRubric); + + // Assert + expect(result).toHaveBeenAnsweredCorrectly(); + }); + + it("marks a wrong tail as incorrect", () => { + // Arrange — correct tip but wrong tail + const guess: PerseusGraphType = { + type: "vector", + coords: [ + [1, 1], + [3, 4], + ], + }; + + // Act + const result = scoreInteractiveGraph(guess, vectorRubric); + + // Assert + expect(result).toHaveBeenAnsweredIncorrectly(); + }); + + it("marks a wrong tip as incorrect", () => { + // Arrange — correct tail but wrong tip + const guess: PerseusGraphType = { + type: "vector", + coords: [ + [0, 0], + [4, 5], + ], + }; + + // Act + const result = scoreInteractiveGraph(guess, vectorRubric); + + // Assert + expect(result).toHaveBeenAnsweredIncorrectly(); + }); + + it("marks swapped tail and tip as incorrect", () => { + // Arrange — coords are reversed (tip at tail position, tail at tip) + // Unlike ray, vector scoring is order-sensitive + const guess: PerseusGraphType = { + type: "vector", + coords: [ + [3, 4], + [0, 0], + ], + }; + + // Act + const result = scoreInteractiveGraph(guess, vectorRubric); + + // Assert + expect(result).toHaveBeenAnsweredIncorrectly(); + }); +}); diff --git a/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.ts b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.ts index 33fb1f152f7..df7db01d237 100644 --- a/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.ts +++ b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.ts @@ -395,6 +395,31 @@ function scoreInteractiveGraph( message: null, }; } + } else if ( + userInput.type === "vector" && + rubric.correct.type === "vector" && + userInput.coords != null && + rubric.correct.coords != null + ) { + // Vector scoring: both tail and tip must match exactly. + // Order matters — coords[0] is tail, coords[1] is tip. + if ( + approximateDeepEqual( + userInput.coords[0], + rubric.correct.coords[0], + ) && + approximateDeepEqual( + userInput.coords[1], + rubric.correct.coords[1], + ) + ) { + return { + type: "points", + earned: 1, + total: 1, + message: null, + }; + } } }