Skip to content

Commit 36198bf

Browse files
authored
[Feature] Client Validation (#2031)
## Summary: This PR includes the following commits: - Swap out deprecated input-number with numeric-input in some tests (#1995) - SSS: Hook emptyWidgets() up to validation functions (#2000) - Add test to document empty expression can be a correct answer (#2003) - Remove unused rubric type for CS Program (#1997) - Remove unused rubric type for iFrame (#1996) - Refactor LabelImage to separate out answers from userInput into scoringData (#1965) - Label-image: Extract validation out of scoring (#2016) - Rename usages of rubric to scoringData (#2006) - SSS: Improve types for validation (#2002) ## Test plan: - Confirm all checks pass - Manual test widgets to confirm they act as expected by creating a webapp testing branch/PR - Confirm widgets all grade as expected Author: Myranae Reviewers: jeremywiebe, handeyeco Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (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), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x) Pull Request URL: #2031
2 parents 19332bb + cd0e130 commit 36198bf

File tree

102 files changed

+1644
-951
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+1644
-951
lines changed

.changeset/eight-squids-repair.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
"@khanacademy/perseus-core": patch
4+
"@khanacademy/perseus-editor": patch
5+
---
6+
7+
Type and test fixes for new MockWidget (isolating to be seen only in tests)

.changeset/few-rings-cover.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
---
4+
5+
Add and improve types for scoring and validation

.changeset/fifty-laws-hear.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
Remove unused CS Program rubric type

.changeset/many-penguins-hug.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@khanacademy/perseus": major
3+
"@khanacademy/perseus-editor": patch
4+
---
5+
6+
Refactor the LabelImage widget to separate out answers from userInput into scoringData

.changeset/nine-planes-relax.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
---
4+
5+
Fix some naming discrepancies related to validation and simplify Matcher ScoringData type

.changeset/pink-pumas-hug.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
---
4+
5+
Use empty widgets check in scoring function

.changeset/proud-ghosts-learn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
Remove unused iframe rubric type

.changeset/quiet-adults-look.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
---
4+
5+
Change empty widgets check in Renderer to depend only on data available (and not on scoring data)

.changeset/smooth-cheetahs-grin.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
---
4+
5+
Rename usages of rubric to scoringData

.changeset/spicy-cups-join.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
TESTS: swap input-number out of renderer tests as it is deprecated

.changeset/thirty-hornets-punch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
---
4+
5+
Introduces a validation function for the label-image widget (extracted from label-image scoring function).

docs/architecture.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ base Markdown syntax:
1111

1212
1. Widgets - Perseus can render custom widgets (in the form of React
1313
components) which conform to a special API that enables the user to
14-
interact with the widget and for the widget to check taht input for
15-
correctness against a rubric. Widgets are denoted using the following
16-
Markdown syntax: `[[☃️ widget-id ]]` (where `widget-id` represents a
17-
generated ID that is unique within the Perseus instance.
14+
interact with the widget and for the widget to check that input for
15+
correctness against a set of scoring data. Widgets are denoted using the
16+
following Markdown syntax: `[[☃️ widget-id ]]` (where `widget-id`
17+
represents a generated ID that is unique within the Perseus instance.
1818
1. Math - Perseus can also render beautiful math using MathJax. Math is
1919
denoted using an opening and close dollar sign (eg. `$y = mx + b$`).
2020

@@ -181,15 +181,15 @@ the widgets options type (ie. the type `T` wrapped in `WidgetOptions<T>` from
181181
In a few rare cases, this type is defined as the sum of RenderProps wrapped in
182182
`WidgetOptions`.
183183

184-
### `Rubric`
184+
### `Scoring Data`
185185

186186
This type defines the data that the scoring function needs in order to score
187187
the learner's guess (aka user input).
188188

189189
### `Props`
190190

191191
Finally, `Props` form the entire set of props that widget's component supports.
192-
Typically it is defined as `type Props = WidgetProps<RenderProps, Rubric>`. In
192+
Typically it is defined as `type Props = WidgetProps<RenderProps, ScoringData>`. In
193193
cases where there are `RenderProps` that are optional that are provided via
194194
`DefaultProps`, this `Props` type "redefines" these props as `myProp:
195195
NonNullable<ExternalProps["myProps"]>;`.

packages/perseus-core/src/data-schema.ts

+58-49
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,50 @@ export type Size = [width: number, height: number];
3737
export type CollinearTuple = [Vector2, Vector2];
3838
export type ShowSolutions = "all" | "selected" | "none";
3939

40+
/**
41+
* A utility type that constructs a widget map from a "registry interface".
42+
* The keys of the registry should be the widget type (aka, "categorizer" or
43+
* "radio", etc) and the value should be the option type stored in the value
44+
* of the map.
45+
*
46+
* You can think of this as a type that generates another type. We use
47+
* "registry interfaces" as a way to keep a set of widget types to their data
48+
* type in several places in Perseus. This type then allows us to generate a
49+
* map type that maps a widget id to its data type and keep strong typing by
50+
* widget id.
51+
*
52+
* For example, given a fictitious registry such as this:
53+
*
54+
* ```
55+
* interface DummyRegistry {
56+
* categorizer: { categories: ReadonlyArray<string> };
57+
* dropdown: { choices: ReadonlyArray<string> }:
58+
* }
59+
* ```
60+
*
61+
* If we create a DummyMap using this helper:
62+
*
63+
* ```
64+
* type DummyMap = MakeWidgetMap<DummyRegistry>;
65+
* ```
66+
*
67+
* We'll get a map that looks like this:
68+
*
69+
* ```
70+
* type DummyMap = {
71+
* `categorizer ${number}`: { categories: ReadonlyArray<string> };
72+
* `dropdown ${number}`: { choices: ReadonlyArray<string> };
73+
* }
74+
* ```
75+
*
76+
* We use interfaces for the registries so that they can be extended in cases
77+
* where the consuming app brings along their own widgets. Interfaces in
78+
* TypeScript are always open (ie. you can extend them) whereas types aren't.
79+
*/
80+
export type MakeWidgetMap<TRegistry> = {
81+
[Property in keyof TRegistry as `${Property & string} ${number}`]: TRegistry[Property];
82+
};
83+
4084
/**
4185
* Our core set of Perseus widgets.
4286
*
@@ -58,7 +102,7 @@ export type ShowSolutions = "all" | "selected" | "none";
58102
* `PerseusWidgets` with the one defined below.
59103
*
60104
* ```typescript
61-
* declare module "@khanacademy/perseus" {
105+
* declare module "@khanacademy/perseus-core" {
62106
* interface PerseusWidgetTypes {
63107
* // A new widget
64108
* "new-awesomeness": MyAwesomeNewWidget;
@@ -100,7 +144,6 @@ export interface PerseusWidgetTypes {
100144
matcher: MatcherWidget;
101145
matrix: MatrixWidget;
102146
measurer: MeasurerWidget;
103-
"mock-widget": MockWidget;
104147
"molecule-renderer": MoleculeRendererWidget;
105148
"number-line": NumberLineWidget;
106149
"numeric-input": NumericInputWidget;
@@ -135,9 +178,19 @@ export interface PerseusWidgetTypes {
135178
* @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
136179
* by augmenting the PerseusWidgetTypes with new widget types!
137180
*/
138-
export type PerseusWidgetsMap = {
139-
[Property in keyof PerseusWidgetTypes as `${Property} ${number}`]: PerseusWidgetTypes[Property];
140-
};
181+
export type PerseusWidgetsMap = MakeWidgetMap<PerseusWidgetTypes>;
182+
183+
/**
184+
* PerseusWidget is a union of all the different types of widget options that
185+
* Perseus knows about.
186+
*
187+
* Thanks to it being based on PerseusWidgetTypes interface, this union is
188+
* automatically extended to include widgets used in tests without those widget
189+
* option types seeping into our production types.
190+
*
191+
* @see MockWidget for an example
192+
*/
193+
export type PerseusWidget = PerseusWidgetTypes[keyof PerseusWidgetTypes];
141194

142195
/**
143196
* A "PerseusItem" is a classic Perseus item. It is rendered by the
@@ -304,8 +357,6 @@ export type MatrixWidget = WidgetOptions<'matrix', PerseusMatrixWidgetOptions>;
304357
// prettier-ignore
305358
export type MeasurerWidget = WidgetOptions<'measurer', PerseusMeasurerWidgetOptions>;
306359
// prettier-ignore
307-
export type MockWidget = WidgetOptions<'mock-widget', MockWidgetOptions>;
308-
// prettier-ignore
309360
export type NumberLineWidget = WidgetOptions<'number-line', PerseusNumberLineWidgetOptions>;
310361
// prettier-ignore
311362
export type NumericInputWidget = WidgetOptions<'numeric-input', PerseusNumericInputWidgetOptions>;
@@ -338,43 +389,6 @@ export type VideoWidget = WidgetOptions<'video', PerseusVideoWidgetOptions>;
338389
//prettier-ignore
339390
export type DeprecatedStandinWidget = WidgetOptions<'deprecated-standin', object>;
340391

341-
export type PerseusWidget =
342-
| CategorizerWidget
343-
| CSProgramWidget
344-
| DefinitionWidget
345-
| DropdownWidget
346-
| ExplanationWidget
347-
| ExpressionWidget
348-
| GradedGroupSetWidget
349-
| GradedGroupWidget
350-
| GrapherWidget
351-
| GroupWidget
352-
| IFrameWidget
353-
| ImageWidget
354-
| InputNumberWidget
355-
| InteractionWidget
356-
| InteractiveGraphWidget
357-
| LabelImageWidget
358-
| MatcherWidget
359-
| MatrixWidget
360-
| MeasurerWidget
361-
| MockWidget
362-
| MoleculeRendererWidget
363-
| NumberLineWidget
364-
| NumericInputWidget
365-
| OrdererWidget
366-
| PassageRefWidget
367-
| PassageWidget
368-
| PhetSimulationWidget
369-
| PlotterWidget
370-
| PythonProgramWidget
371-
| RadioWidget
372-
| RefTargetWidget
373-
| SorterWidget
374-
| TableWidget
375-
| VideoWidget
376-
| DeprecatedStandinWidget;
377-
378392
/**
379393
* A background image applied to various widgets.
380394
*/
@@ -1678,11 +1692,6 @@ export type PerseusVideoWidgetOptions = {
16781692
static?: boolean;
16791693
};
16801694

1681-
export type MockWidgetOptions = {
1682-
static?: boolean;
1683-
value: string;
1684-
};
1685-
16861695
export type PerseusInputNumberWidgetOptions = {
16871696
answerType?:
16881697
| "number"

packages/perseus-editor/src/widgets/__stories__/label-image-editor.stories.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as React from "react";
44

55
import LabelImageEditor from "../label-image-editor";
66

7-
import type {MarkerType} from "@khanacademy/perseus-core";
7+
import type {PerseusLabelImageWidgetOptions} from "@khanacademy/perseus-core";
88

99
type StoryArgs = Record<any, any>;
1010

@@ -29,7 +29,7 @@ type State = {
2929
imageUrl: string;
3030
imageWidth: number;
3131
imageHeight: number;
32-
markers: ReadonlyArray<MarkerType>;
32+
markers: PerseusLabelImageWidgetOptions["markers"];
3333
};
3434

3535
class WithState extends React.Component<Empty, State> {

packages/perseus-editor/src/widgets/label-image-editor.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Behavior from "./label-image/behavior";
1717
import QuestionMarkers from "./label-image/question-markers";
1818
import SelectImage from "./label-image/select-image";
1919

20-
import type {MarkerType} from "@khanacademy/perseus-core";
20+
import type {PerseusLabelImageWidgetOptions} from "@khanacademy/perseus-core";
2121

2222
type Props = {
2323
// List of answer choices to label question image with.
@@ -28,7 +28,7 @@ type Props = {
2828
imageWidth: number;
2929
imageHeight: number;
3030
// The list of label markers on the question image.
31-
markers: ReadonlyArray<MarkerType>;
31+
markers: PerseusLabelImageWidgetOptions["markers"];
3232
// Whether multiple answer choices may be selected for markers.
3333
multipleAnswers: boolean;
3434
// Whether to hide answer choices from user instructions.
@@ -176,9 +176,9 @@ class LabelImageEditor extends React.Component<Props> {
176176
this.props.onChange({choices});
177177
};
178178

179-
handleMarkersChange: (markers: ReadonlyArray<MarkerType>) => void = (
180-
markers: ReadonlyArray<MarkerType>,
181-
) => {
179+
handleMarkersChange: (
180+
markers: PerseusLabelImageWidgetOptions["markers"],
181+
) => void = (markers: PerseusLabelImageWidgetOptions["markers"]) => {
182182
this.props.onChange({markers});
183183
};
184184

packages/perseus-editor/src/widgets/label-image/__stories__/question-markers.stories.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from "react";
33

44
import QuestionMarkers from "../question-markers";
55

6-
import type {MarkerType} from "@khanacademy/perseus-core";
6+
import type {PerseusLabelImageWidgetOptions} from "@khanacademy/perseus-core";
77

88
type StoryArgs = Record<any, any>;
99

@@ -31,7 +31,7 @@ const Wrapper = (props) => (
3131
class WithState extends React.Component<
3232
Record<any, any>,
3333
{
34-
markers: ReadonlyArray<MarkerType>;
34+
markers: PerseusLabelImageWidgetOptions["markers"];
3535
}
3636
> {
3737
state = {

packages/perseus-editor/src/widgets/label-image/marker.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import Option, {OptionGroup} from "../../components/dropdown-option";
1414
import FormWrappedTextField from "../../components/form-wrapped-text-field";
1515
import {gray17, gray85, gray98} from "../../styles/global-colors";
1616

17-
import type {MarkerType} from "@khanacademy/perseus-core";
17+
import type {PerseusLabelImageWidgetOptions} from "@khanacademy/perseus-core";
1818

19-
type Props = MarkerType & {
19+
type Props = PerseusLabelImageWidgetOptions["markers"][number] & {
2020
// The list of possible answer choices.
21-
choices: ReadonlyArray<string>;
21+
choices: PerseusLabelImageWidgetOptions["choices"];
2222
// Callback for when any of the marker props are changed.
23-
onChange: (marker: MarkerType) => void;
23+
onChange: (
24+
marker: PerseusLabelImageWidgetOptions["markers"][number],
25+
) => void;
2426
// Callback to remove marker from the question image.
2527
onRemove: () => void;
2628
};

packages/perseus-editor/src/widgets/label-image/question-markers.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {gray17, gray68} from "../../styles/global-colors";
1111

1212
import Marker from "./marker";
1313

14-
import type {MarkerType} from "@khanacademy/perseus-core";
14+
import type {PerseusLabelImageWidgetOptions} from "@khanacademy/perseus-core";
1515

1616
type Props = {
1717
// The list of possible answers in a specific order.
@@ -21,9 +21,9 @@ type Props = {
2121
imageWidth: number;
2222
imageHeight: number;
2323
// The list of markers placed on the question image.
24-
markers: ReadonlyArray<MarkerType>;
24+
markers: PerseusLabelImageWidgetOptions["markers"];
2525
// Callback for when any of markers change.
26-
onChange: (markers: ReadonlyArray<MarkerType>) => void;
26+
onChange: (markers: PerseusLabelImageWidgetOptions["markers"]) => void;
2727
};
2828

2929
export default class QuestionMarkers extends React.Component<Props> {

packages/perseus-score/src/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ export type {Score} from "./util/answer-types";
33
export {default as ErrorCodes} from "./error-codes";
44
export type * from "./validation.types";
55
export {default as scoreCategorizer} from "./widgets/categorizer/score-categorizer";
6+
export {default as validateCategorizer} from "./widgets/categorizer/validate-categorizer";
67
export {default as scoreCSProgram} from "./widgets/cs-program/score-cs-program";
78
export {default as scoreDropdown} from "./widgets/dropdown/score-dropdown";
9+
export {default as validateDropdown} from "./widgets/dropdown/validate-dropdown";
810
export {default as scoreExpression} from "./widgets/expression/score-expression";
11+
export {default as validateExpression} from "./widgets/expression/validate-expression";
912
export {default as scoreGrapher} from "./widgets/grapher/score-grapher";
1013
export {default as scoreIframe} from "./widgets/iframe/score-iframe";
1114
export {default as scoreInteractiveGraph} from "./widgets/interactive-graph/score-interactive-graph";
@@ -15,13 +18,20 @@ export {
1518
} from "./widgets/label-image/score-label-image";
1619
export {default as scoreMatcher} from "./widgets/matcher/score-matcher";
1720
export {default as scoreMatrix} from "./widgets/matrix/score-matrix";
21+
export {default as validateMatrix} from "./widgets/matrix/validate-matrix";
1822
export {default as scoreNumberLine} from "./widgets/number-line/score-number-line";
23+
export {default as validateNumberLine} from "./widgets/number-line/validate-number-line";
1924
export {default as scoreNumericInput} from "./widgets/numeric-input/score-numeric-input";
2025
export {default as scoreOrderer} from "./widgets/orderer/score-orderer";
26+
export {default as validateOrderer} from "./widgets/orderer/validate-orderer";
2127
export {default as scorePlotter} from "./widgets/plotter/score-plotter";
28+
export {default as validatePlotter} from "./widgets/plotter/validate-plotter";
2229
export {default as scoreRadio} from "./widgets/radio/score-radio";
30+
export {default as validateRadio} from "./widgets/radio/validate-radio";
2331
export {default as scoreSorter} from "./widgets/sorter/score-sorter";
32+
export {default as validateSorter} from "./widgets/sorter/validate-sorter";
2433
export {default as scoreTable} from "./widgets/table/score-table";
34+
export {default as validateTable} from "./widgets/table/validate-table";
2535
export {
2636
default as scoreInputNumber,
2737
inputNumberAnswerTypes,

0 commit comments

Comments
 (0)