From 8aaf2967088e55e6907ef4b01411d6e9579b4677 Mon Sep 17 00:00:00 2001 From: Tamara <60857422+Myranae@users.noreply.github.com> Date: Wed, 6 Nov 2024 09:22:22 -0600 Subject: [PATCH] Refine Interactive Graph Rubric type (#1818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: As part of the Server Side Scoring project, this refines the Interactive Graph widget's rubric to only what is used during the validation project. It updates the typing of correct to a union of correct types for all types of graphs. Initial iteration: https://github.com/Khan/perseus/pull/1813 Issue: LEMS-2473 ## Test plan: - Confirm all checks pass - Confirm graphs of all types still work as expected Author: Myranae Reviewers: Myranae, benchristel, jeremywiebe, handeyeco Required Reviewers: Approved By: handeyeco, benchristel 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), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1818 --- .changeset/fast-crabs-switch.md | 5 + packages/perseus/src/perseus-types.ts | 71 +++++ packages/perseus/src/validation.types.ts | 3 +- .../interactive-graph-validator.test.ts | 242 +++++++++++------- .../interactive-graph-validator.ts | 19 +- 5 files changed, 227 insertions(+), 113 deletions(-) create mode 100644 .changeset/fast-crabs-switch.md diff --git a/.changeset/fast-crabs-switch.md b/.changeset/fast-crabs-switch.md new file mode 100644 index 0000000000..a461f1597c --- /dev/null +++ b/.changeset/fast-crabs-switch.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Refine Interactive Graph's Rubric type diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index 3b50bc88ad..3fd2b991e4 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -909,6 +909,77 @@ export type PerseusGraphTypeRay = { startCoords?: CollinearTuple; } & PerseusGraphTypeCommon; +type AngleGraphCorrect = { + type: "angle"; + allowReflexAngles: boolean; + match: "congruent"; + coords: [Coord, Coord, Coord]; +}; + +type CircleGraphCorrect = { + type: "circle"; + center: Coord; + radius: number; +}; + +type LinearGraphCorrect = { + type: "linear"; + coords: CollinearTuple; +}; + +type LinearSystemGraphCorrect = { + type: "linear-system"; + coords: [CollinearTuple, CollinearTuple]; +}; + +type NoneGraphCorrect = { + type: "none"; +}; + +type PointGraphCorrect = { + type: "point"; + coords: ReadonlyArray; +}; + +type PolygonGraphCorrect = { + type: "polygon"; + match: "similar" | "congruent" | "approx"; + coords: ReadonlyArray; +}; + +type QuadraticGraphCorrect = { + type: "quadratic"; + coords: [Coord, Coord, Coord]; +}; + +type SegmentGraphCorrect = { + type: "segment"; + coords: CollinearTuple[]; +}; + +type SinusoidGraphCorrect = { + type: "sinusoid"; + coords: CollinearTuple; +}; + +type RayGraphCorrect = { + type: "ray"; + coords: CollinearTuple; +}; + +export type PerseusGraphCorrectType = + | AngleGraphCorrect + | CircleGraphCorrect + | LinearGraphCorrect + | LinearSystemGraphCorrect + | NoneGraphCorrect + | PointGraphCorrect + | PolygonGraphCorrect + | QuadraticGraphCorrect + | RayGraphCorrect + | SegmentGraphCorrect + | SinusoidGraphCorrect; + export type PerseusLabelImageWidgetOptions = { // Translatable Text; Tex representation of choices choices: ReadonlyArray; diff --git a/packages/perseus/src/validation.types.ts b/packages/perseus/src/validation.types.ts index 74ca2da9be..c45e41e306 100644 --- a/packages/perseus/src/validation.types.ts +++ b/packages/perseus/src/validation.types.ts @@ -14,6 +14,7 @@ import type { PerseusPlotterWidgetOptions, PerseusRadioChoice, PerseusTableWidgetOptions, + PerseusGraphCorrectType, } from "./perseus-types"; import type {InteractiveMarkerType} from "./widgets/label-image/types"; import type {Relationship} from "./widgets/number-line/number-line"; @@ -97,7 +98,7 @@ export type PerseusInputNumberUserInput = { export type PerseusInteractiveGraphRubric = { // TODO(LEMS-2344): make the type of `correct` more specific - correct: PerseusGraphType; + correct: PerseusGraphCorrectType; graph: PerseusGraphType; }; diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.test.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.test.ts index 3e71c5a327..5c9b7004a1 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.test.ts @@ -6,24 +6,24 @@ import interactiveGraphValidator from "./interactive-graph-validator"; import type {PerseusGraphType} from "../../perseus-types"; import type {PerseusInteractiveGraphRubric} from "../../validation.types"; -import type {Coord} from "@khanacademy/perseus"; - -function createRubric(graph: PerseusGraphType): PerseusInteractiveGraphRubric { - return {graph, correct: graph}; -} describe("InteractiveGraph.validate on a segment question", () => { it("marks the answer invalid if guess.coords is missing", () => { const guess: PerseusGraphType = {type: "segment"}; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "segment", - coords: [ - [ - [0, 0], - [1, 1], + const rubric: PerseusInteractiveGraphRubric = { + graph: { + type: "segment", + }, + correct: { + type: "segment", + coords: [ + [ + [0, 0], + [1, 1], + ], ], - ], - }); + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -40,15 +40,19 @@ describe("InteractiveGraph.validate on a segment question", () => { ], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "segment", - coords: [ - [ - [0, 0], - [1, 1], + + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "segment"}, + correct: { + type: "segment", + coords: [ + [ + [0, 0], + [1, 1], + ], ], - ], - }); + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -65,15 +69,19 @@ describe("InteractiveGraph.validate on a segment question", () => { ], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "segment", - coords: [ - [ - [0, 0], - [1, 1], + + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "segment"}, + correct: { + type: "segment", + coords: [ + [ + [0, 0], + [1, 1], + ], ], - ], - }); + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -90,15 +98,18 @@ describe("InteractiveGraph.validate on a segment question", () => { ], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "segment", - coords: [ - [ - [0, 0], - [1, 1], + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "segment"}, + correct: { + type: "segment", + coords: [ + [ + [0, 0], + [1, 1], + ], ], - ], - }); + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -115,15 +126,19 @@ describe("InteractiveGraph.validate on a segment question", () => { ], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "segment", - coords: [ - [ - [0, 0], - [1, 1], + + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "segment"}, + correct: { + type: "segment", + coords: [ + [ + [0, 0], + [1, 1], + ], ], - ], - }); + }, + }; interactiveGraphValidator(guess, rubric); @@ -145,15 +160,18 @@ describe("InteractiveGraph.validate on a segment question", () => { ], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "segment", - coords: [ - [ - [1, 1], - [0, 0], + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "segment"}, + correct: { + type: "segment", + coords: [ + [ + [1, 1], + [0, 0], + ], ], - ], - }); + }, + }; interactiveGraphValidator(guess, rubric); @@ -172,14 +190,19 @@ describe("InteractiveGraph.validate on a segment question", () => { describe("InteractiveGraph.validate on an angle question", () => { it("marks the answer invalid if guess.coords is missing", () => { const guess: PerseusGraphType = {type: "angle"}; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "angle", - coords: [ - [1, 1], - [0, 0], - [-1, -1], - ] as [Coord, Coord, Coord], - }); + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "angle"}, + correct: { + type: "angle", + coords: [ + [1, 1], + [0, 0], + [-1, -1], + ], + allowReflexAngles: false, + match: "congruent", + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -190,10 +213,13 @@ describe("InteractiveGraph.validate on an angle question", () => { describe("InteractiveGraph.validate on a point question", () => { it("marks the answer invalid if guess.coords is missing", () => { const guess: PerseusGraphType = {type: "point"}; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "point", - coords: [[0, 0]], - }); + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "point"}, + correct: { + type: "point", + coords: [[0, 0]], + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -207,9 +233,16 @@ describe("InteractiveGraph.validate on a point question", () => { type: "point", coords: [[0, 0]], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "point", - }); + + const rubric: PerseusInteractiveGraphRubric = { + graph: { + type: "point", + }, + // @ts-expect-error: Testing exception for invalid rubric + correct: { + type: "point", + }, + }; expect(() => interactiveGraphValidator(guess, rubric)).toThrowError(); }); @@ -219,10 +252,13 @@ describe("InteractiveGraph.validate on a point question", () => { type: "point", coords: [[9, 9]], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "point", - coords: [[0, 0]], - }); + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "point"}, + correct: { + type: "point", + coords: [[0, 0]], + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -234,10 +270,13 @@ describe("InteractiveGraph.validate on a point question", () => { type: "point", coords: [[7, 8]], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "point", - coords: [[7, 8]], - }); + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "point"}, + correct: { + type: "point", + coords: [[7, 8]], + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -252,13 +291,16 @@ describe("InteractiveGraph.validate on a point question", () => { [5, 6], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "point", - coords: [ - [5, 6], - [7, 8], - ], - }); + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "point"}, + correct: { + type: "point", + coords: [ + [5, 6], + [7, 8], + ], + }, + }; const result = interactiveGraphValidator(guess, rubric); @@ -273,13 +315,16 @@ describe("InteractiveGraph.validate on a point question", () => { [5, 6], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "point", - coords: [ - [5, 6], - [7, 8], - ], - }); + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "point"}, + correct: { + type: "point", + coords: [ + [5, 6], + [7, 8], + ], + }, + }; const guessClone = clone(guess); @@ -296,13 +341,16 @@ describe("InteractiveGraph.validate on a point question", () => { [5, 6], ], }; - const rubric: PerseusInteractiveGraphRubric = createRubric({ - type: "point", - coords: [ - [5, 6], - [7, 8], - ], - }); + const rubric: PerseusInteractiveGraphRubric = { + graph: {type: "point"}, + correct: { + type: "point", + coords: [ + [5, 6], + [7, 8], + ], + }, + }; const rubricClone = clone(rubric); diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.ts index 2a5de73e52..581be76d8e 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-validator.ts @@ -14,7 +14,6 @@ import { import {getClockwiseAngle} from "./math/angles"; -import type {Coord} from "../../interactive2/types"; import type {PerseusScore} from "../../types"; import type { PerseusInteractiveGraphRubric, @@ -60,9 +59,7 @@ function interactiveGraphValidator( // If both of the guess points are on the correct line, it's // correct. if ( - // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. collinear(correct[0], correct[1], guess[0]) && - // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. collinear(correct[0], correct[1], guess[1]) ) { return { @@ -78,9 +75,7 @@ function interactiveGraphValidator( userInput.coords != null ) { const guess = userInput.coords; - const correct = rubric.correct.coords as ReadonlyArray< - ReadonlyArray - >; + const correct = rubric.correct.coords; if ( (collinear(correct[0][0], correct[0][1], guess[0][0]) && @@ -107,7 +102,6 @@ function interactiveGraphValidator( // If the parabola coefficients match, it's correct. const guessCoeffs = getQuadraticCoefficients(userInput.coords); const correctCoeffs = getQuadraticCoefficients( - // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'. rubric.correct.coords, ); if (deepEq(guessCoeffs, correctCoeffs)) { @@ -125,7 +119,6 @@ function interactiveGraphValidator( ) { const guessCoeffs = getSinusoidCoefficients(userInput.coords); const correctCoeffs = getSinusoidCoefficients( - // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'. rubric.correct.coords, ); @@ -187,9 +180,8 @@ function interactiveGraphValidator( rubric.correct.type === "polygon" && userInput.coords != null ) { - const guess: Array = userInput.coords?.slice(); - // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'. - const correct: Array = rubric.correct.coords?.slice(); + const guess = userInput.coords.slice(); + const correct = rubric.correct.coords.slice(); let match; if (rubric.correct.match === "similar") { @@ -219,9 +211,8 @@ function interactiveGraphValidator( userInput.coords != null ) { let guess = Util.deepClone(userInput.coords); - let correct = Util.deepClone(rubric.correct?.coords); + let correct = Util.deepClone(rubric.correct.coords); guess = _.invoke(guess, "sort").sort(); - // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection'. correct = _.invoke(correct, "sort").sort(); if (deepEq(guess, correct)) { return { @@ -239,9 +230,7 @@ function interactiveGraphValidator( const guess = userInput.coords; const correct = rubric.correct.coords; if ( - // @ts-expect-error - TS2532 - Object is possibly 'undefined'. deepEq(guess[0], correct[0]) && - // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. collinear(correct[0], correct[1], guess[1]) ) { return {