diff --git a/.changeset/quiet-adults-look.md b/.changeset/quiet-adults-look.md new file mode 100644 index 0000000000..c1d9f4aaa0 --- /dev/null +++ b/.changeset/quiet-adults-look.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Change empty widgets check in Renderer to depend only on data available (and not on scoring data) diff --git a/packages/perseus/src/__testdata__/renderer.testdata.ts b/packages/perseus/src/__testdata__/renderer.testdata.ts index b2b7b417c2..93a4e1e6b6 100644 --- a/packages/perseus/src/__testdata__/renderer.testdata.ts +++ b/packages/perseus/src/__testdata__/renderer.testdata.ts @@ -1,11 +1,29 @@ import type { DropdownWidget, + ExpressionWidget, ImageWidget, NumericInputWidget, PerseusRenderer, } from "../perseus-types"; import type {RenderProps} from "../widgets/radio"; +export const expressionWidget: ExpressionWidget = { + type: "expression", + options: { + answerForms: [ + { + considered: "correct", + form: true, + simplify: true, + value: "1.0", + }, + ], + buttonSets: ["basic"], + functions: [], + times: true, + }, +}; + export const dropdownWidget: DropdownWidget = { type: "dropdown", alignment: "default", @@ -96,6 +114,12 @@ export const question2: PerseusRenderer = { widgets: {"numeric-input 1": numericInputWidget}, }; +export const question3: PerseusRenderer = { + content: "Enter $1.0$ in the input field: [[\u2603 expression 1]]\n\n\n\n", + images: {}, + widgets: {"expression 1": expressionWidget}, +}; + export const definitionItem: PerseusRenderer = { content: "Mock widgets ==> [[\u2603 definition 1]] [[\u2603 definition 2]] [[\u2603 definition 3]]", diff --git a/packages/perseus/src/__tests__/renderer.test.tsx b/packages/perseus/src/__tests__/renderer.test.tsx index 13b2f07645..209f1f09bf 100644 --- a/packages/perseus/src/__tests__/renderer.test.tsx +++ b/packages/perseus/src/__tests__/renderer.test.tsx @@ -15,6 +15,7 @@ import { definitionItem, mockedRandomItem, mockedShuffledRadioProps, + question3, } from "../__testdata__/renderer.testdata"; import * as Dependencies from "../dependencies"; import {registerWidget} from "../widgets"; @@ -1605,35 +1606,36 @@ describe("renderer", () => { it("should return all empty widgets", async () => { // Arrange const {renderer} = renderQuestion({ - ...question2, + ...question3, content: - "Input 1: [[☃ numeric-input 1]]\n\n" + - "Input 2: [[☃ numeric-input 2]]", + "Input 1: [[☃ expression 1]]\n\n" + + "Input 2: [[☃ expression 2]]", widgets: { - ...question2.widgets, - "numeric-input 2": question2.widgets["numeric-input 1"], + ...question3.widgets, + "expression 2": question3.widgets["expression 1"], }, }); await userEvent.type(screen.getAllByRole("textbox")[0], "150"); + act(() => jest.runOnlyPendingTimers()); // Act const emptyWidgets = renderer.emptyWidgets(); // Assert - expect(emptyWidgets).toStrictEqual(["numeric-input 2"]); + expect(emptyWidgets).toStrictEqual(["expression 2"]); }); it("should not return static widgets even if empty", () => { // Arrange const {renderer} = renderQuestion({ - ...question2, + ...question3, content: - "Input 1: [[☃ numeric-input 1]]\n\n" + - "Input 2: [[☃ numeric-input 2]]", + "Input 1: [[☃ expression 1]]\n\n" + + "Input 2: [[☃ expression 2]]", widgets: { - ...question2.widgets, - "numeric-input 2": { - ...question2.widgets["numeric-input 1"], + ...question3.widgets, + "expression 2": { + ...question3.widgets["expression 1"], static: true, }, }, @@ -1643,7 +1645,7 @@ describe("renderer", () => { const emptyWidgets = renderer.emptyWidgets(); // Assert - expect(emptyWidgets).toStrictEqual(["numeric-input 1"]); + expect(emptyWidgets).toStrictEqual(["expression 1"]); }); it("should return widget ID for group with empty widget", () => { @@ -1663,7 +1665,7 @@ describe("renderer", () => { JSON.stringify(simpleGroupQuestion), ); simpleGroupQuestionCopy.widgets["group 1"].options.widgets[ - "numeric-input 1" + "expression 1" ].static = true; const {renderer} = renderQuestion(simpleGroupQuestionCopy); @@ -1678,6 +1680,7 @@ describe("renderer", () => { // Arrange const {renderer} = renderQuestion(simpleGroupQuestion); await userEvent.type(screen.getByRole("textbox"), "99"); + act(() => jest.runOnlyPendingTimers()); // Act const emptyWidgets = renderer.emptyWidgets(); diff --git a/packages/perseus/src/renderer-util.ts b/packages/perseus/src/renderer-util.ts index 6c05e18cf0..2a54a30a8c 100644 --- a/packages/perseus/src/renderer-util.ts +++ b/packages/perseus/src/renderer-util.ts @@ -1,12 +1,20 @@ import {mapObject} from "./interactive2/objective_"; import {scoreIsEmpty, flattenScores} from "./util/scoring"; import {getWidgetIdsFromContent} from "./widget-type-utils"; -import {getWidgetScorer, upgradeWidgetInfoToLatestVersion} from "./widgets"; +import { + getWidgetScorer, + getWidgetValidator, + upgradeWidgetInfoToLatestVersion, +} from "./widgets"; import type {PerseusRenderer, PerseusWidgetsMap} from "./perseus-types"; import type {PerseusStrings} from "./strings"; import type {PerseusScore} from "./types"; -import type {UserInput, UserInputMap} from "./validation.types"; +import type { + UserInput, + UserInputMap, + ValidationDataMap, +} from "./validation.types"; export function getUpgradedWidgetOptions( oldWidgetOptions: PerseusWidgetsMap, @@ -33,8 +41,13 @@ export function getUpgradedWidgetOptions( }); } +/** + * Checks the given user input to see if any answerable widgets have not been + * "filled in" (ie. if they're empty). Another way to think about this + * function is that its a check to see if we can score the provided input. + */ export function emptyWidgetsFunctional( - widgets: PerseusWidgetsMap, + widgets: ValidationDataMap, // This is a port of old code, I'm not sure why // we need widgetIds vs the keys of the widgets object widgetIds: Array, @@ -42,22 +55,17 @@ export function emptyWidgetsFunctional( strings: PerseusStrings, locale: string, ): ReadonlyArray { - const upgradedWidgets = getUpgradedWidgetOptions(widgets); - return widgetIds.filter((id) => { - const widget = upgradedWidgets[id]; - if (!widget || widget.static) { + const widget = widgets[id]; + if (!widget || widget.static === true) { // Static widgets shouldn't count as empty return false; } - const scorer = getWidgetScorer(widget.type); - const score = scorer?.( - userInputMap[id] as UserInput, - widget.options, - strings, - locale, - ); + const validator = getWidgetValidator(widget.type); + const userInput = userInputMap[id]; + const validationData = widget.options; + const score = validator?.(userInput, validationData, strings, locale); if (score) { return scoreIsEmpty(score); diff --git a/packages/perseus/src/types.ts b/packages/perseus/src/types.ts index 6d5d8970ca..af33410513 100644 --- a/packages/perseus/src/types.ts +++ b/packages/perseus/src/types.ts @@ -13,6 +13,7 @@ import type { UserInput, UserInputArray, UserInputMap, + ValidationData, } from "./validation.types"; import type {WidgetPromptJSON} from "./widget-ai-utils/prompt-types"; import type {KeypadAPI} from "@khanacademy/math-input"; @@ -578,6 +579,13 @@ export type WidgetTransform = ( export type ValidationResult = Extract | null; +export type WidgetValidatorFunction = ( + userInput: UserInput, + validationData: ValidationData, + strings: PerseusStrings, + locale: string, +) => ValidationResult; + export type WidgetScorerFunction = ( // The user data needed to score userInput: UserInput, @@ -632,6 +640,14 @@ export type WidgetExports< */ staticTransform?: WidgetTransform; // this is a function of some sort, + /** + * Validates the learner's guess to check if it's sufficient for scoring. + * Typically, this is basically an "emptiness" check, but for some widgets + * such as `interactive-graph` it is a check that the learner has made any + * edits (ie. the widget is not in it's origin state). + */ + validator?: WidgetValidatorFunction; + /** * A function that scores user input (the guess) for the widget. */ diff --git a/packages/perseus/src/validation.types.ts b/packages/perseus/src/validation.types.ts index 03f7139c7c..b3ca2cc08f 100644 --- a/packages/perseus/src/validation.types.ts +++ b/packages/perseus/src/validation.types.ts @@ -90,6 +90,7 @@ export type PerseusExpressionRubric = { export type PerseusExpressionUserInput = string; export type PerseusGroupRubric = PerseusGroupWidgetOptions; +export type PerseusGroupValidationData = {widgets: ValidationDataMap}; export type PerseusGroupUserInput = UserInputMap; export type PerseusGradedGroupRubric = PerseusGradedGroupWidgetOptions; @@ -264,6 +265,7 @@ export type UserInput = | PerseusDropdownUserInput | PerseusExpressionUserInput | PerseusGrapherUserInput + | PerseusGroupUserInput | PerseusIFrameUserInput | PerseusInputNumberUserInput | PerseusInteractiveGraphUserInput @@ -278,7 +280,7 @@ export type UserInput = | PerseusSorterUserInput | PerseusTableUserInput; -export type UserInputMap = {[widgetId: string]: UserInput | UserInputMap}; +export type UserInputMap = {[widgetId: string]: UserInput}; /** * deprecated prefer using UserInputMap @@ -286,3 +288,65 @@ export type UserInputMap = {[widgetId: string]: UserInput | UserInputMap}; export type UserInputArray = ReadonlyArray< UserInputArray | UserInput | null | undefined >; +export interface ValidationDataTypes { + categorizer: PerseusCategorizerValidationData; + // "cs-program": PerseusCSProgramValidationData; + // definition: PerseusDefinitionValidationData; + // dropdown: PerseusDropdownRubric; + // explanation: PerseusExplanationValidationData; + // expression: PerseusExpressionValidationData; + // grapher: PerseusGrapherValidationData; + // "graded-group-set": PerseusGradedGroupSetValidationData; + // "graded-group": PerseusGradedGroupValidationData; + group: PerseusGroupValidationData; + // iframe: PerseusIFrameValidationData; + // image: PerseusImageValidationData; + // "input-number": PerseusInputNumberValidationData; + // interaction: PerseusInteractionValidationData; + // "interactive-graph": PerseusInteractiveGraphValidationData; + // "label-image": PerseusLabelImageValidationData; + // matcher: PerseusMatcherValidationData; + // matrix: PerseusMatrixValidationData; + // measurer: PerseusMeasurerValidationData; + // "molecule-renderer": PerseusMoleculeRendererValidationData; + // "number-line": PerseusNumberLineValidationData; + // "numeric-input": PerseusNumericInputValidationData; + // orderer: PerseusOrdererValidationData; + // "passage-ref-target": PerseusRefTargetValidationData; + // "passage-ref": PerseusPassageRefValidationData; + // passage: PerseusPassageValidationData; + // "phet-simulation": PerseusPhetSimulationValidationData; + // "python-program": PerseusPythonProgramValidationData; + plotter: PerseusPlotterValidationData; + // radio: PerseusRadioValidationData; + // sorter: PerseusSorterValidationData; + // table: PerseusTableValidationData; + // video: PerseusVideoValidationData; + + // Deprecated widgets + // sequence: PerseusAutoCorrectValidationData; +} + +/** + * A map of validation data, keyed by `widgetId`. This data is used to check if + * a question is answerable. This data represents the minimal intersection of + * data that's available in the client (widget options) and server (scoring + * data) and is represented by a group of types known as "validation data". + * + * NOTE: The value in this map is intentionally a subset of WidgetOptions. + * By using the same shape (minus any unneeded data), we are able to pass a + * `PerseusWidgetsMap` or ` into any function that accepts a + * `ValidationDataMap` without any mutation of data. + */ +export type ValidationDataMap = { + [Property in keyof ValidationDataTypes as `${Property} ${number}`]: { + type: Property; + static?: boolean; + options: ValidationDataTypes[Property]; + }; +}; + +/** + * A union type of all the different widget validation data types that exist. + */ +export type ValidationData = ValidationDataTypes[keyof ValidationDataTypes]; diff --git a/packages/perseus/src/widgets.ts b/packages/perseus/src/widgets.ts index 11991cdd37..d88e09de69 100644 --- a/packages/perseus/src/widgets.ts +++ b/packages/perseus/src/widgets.ts @@ -12,6 +12,7 @@ import type { WidgetExports, WidgetTransform, WidgetScorerFunction, + WidgetValidatorFunction, } from "./types"; import type * as React from "react"; @@ -137,6 +138,12 @@ export const getWidgetExport = (name: string): WidgetExports | null => { return widgets[name] ?? null; }; +export const getWidgetValidator = ( + name: string, +): WidgetValidatorFunction | null => { + return widgets[name]?.validator ?? null; +}; + export const getWidgetScorer = (name: string): WidgetScorerFunction | null => { return widgets[name]?.scorer ?? null; }; diff --git a/packages/perseus/src/widgets/categorizer/categorizer.tsx b/packages/perseus/src/widgets/categorizer/categorizer.tsx index 27069da230..6bf02234e0 100644 --- a/packages/perseus/src/widgets/categorizer/categorizer.tsx +++ b/packages/perseus/src/widgets/categorizer/categorizer.tsx @@ -17,6 +17,7 @@ import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/categorizer/categorizer-ai-utils"; import scoreCategorizer from "./score-categorizer"; +import validateCategorizer from "./validate-categorizer"; import type {PerseusCategorizerWidgetOptions} from "../../perseus-types"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; @@ -328,4 +329,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'. scorer: scoreCategorizer, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'. + validator: validateCategorizer, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/dropdown/dropdown.tsx b/packages/perseus/src/widgets/dropdown/dropdown.tsx index 487ff0a364..4f2d384b3c 100644 --- a/packages/perseus/src/widgets/dropdown/dropdown.tsx +++ b/packages/perseus/src/widgets/dropdown/dropdown.tsx @@ -9,6 +9,7 @@ import {ApiOptions} from "../../perseus-api"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/dropdown/dropdown-ai-utils"; import scoreDropdown from "./score-dropdown"; +import validateDropdown from "./validate-dropdown"; import type {PerseusDropdownWidgetOptions} from "../../perseus-types"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; @@ -162,4 +163,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'. scorer: scoreDropdown, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'. + validator: validateDropdown, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx index e2b649062d..7c95cce897 100644 --- a/packages/perseus/src/widgets/expression/expression.tsx +++ b/packages/perseus/src/widgets/expression/expression.tsx @@ -20,6 +20,7 @@ import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/expression/ import getDecimalSeparator from "./get-decimal-separator"; import scoreExpression from "./score-expression"; +import validateExpression from "./validate-expression"; import type {DependenciesContext} from "../../dependencies"; import type {PerseusExpressionWidgetOptions} from "../../perseus-types"; @@ -558,6 +559,9 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'. scorer: scoreExpression, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'. + validator: validateExpression, // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'. diff --git a/packages/perseus/src/widgets/group/group.testdata.ts b/packages/perseus/src/widgets/group/group.testdata.ts index 5e709c3a9d..df0f2580d0 100644 --- a/packages/perseus/src/widgets/group/group.testdata.ts +++ b/packages/perseus/src/widgets/group/group.testdata.ts @@ -159,32 +159,24 @@ export const simpleGroupQuestion: PerseusRenderer = { "group 1": { graded: true, options: { - content: "[[☃ numeric-input 1]]", + content: "[[☃ expression 1]]", images: {}, widgets: { - "numeric-input 1": { - alignment: "default", - graded: true, + "expression 1": { + type: "expression", options: { - answers: [ + answerForms: [ { - maxError: null, - message: "", - simplify: "required", - status: "correct", - strict: false, - value: 230, + considered: "correct", + form: true, + simplify: true, + value: "1.0", }, ], - coefficient: false, - labelText: "value rounded to the nearest ten", - rightAlign: false, - size: "normal", - static: false, + buttonSets: ["basic"], + functions: [], + times: true, }, - static: false, - type: "numeric-input", - version: {major: 0, minor: 0}, }, }, }, diff --git a/packages/perseus/src/widgets/group/group.tsx b/packages/perseus/src/widgets/group/group.tsx index ec5ae760d0..882c4991e4 100644 --- a/packages/perseus/src/widgets/group/group.tsx +++ b/packages/perseus/src/widgets/group/group.tsx @@ -9,6 +9,7 @@ import Renderer from "../../renderer"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/group/group-ai-utils"; import scoreGroup from "./score-group"; +import validateGroup from "./validate-group"; import type {PerseusGroupWidgetOptions} from "../../perseus-types"; import type { @@ -205,8 +206,11 @@ export default { widget: Group, traverseChildWidgets: traverseChildWidgets, // TODO(LEMS-2656): remove TS suppression - // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'. + // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'. scorer: scoreGroup, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'. + validator: validateGroup, hidden: true, isLintable: true, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/group/validate-group.ts b/packages/perseus/src/widgets/group/validate-group.ts new file mode 100644 index 0000000000..39417d362c --- /dev/null +++ b/packages/perseus/src/widgets/group/validate-group.ts @@ -0,0 +1,31 @@ +import {emptyWidgetsFunctional} from "../../renderer-util"; + +import type {PerseusStrings} from "../../strings"; +import type {ValidationResult} from "../../types"; +import type { + PerseusGroupUserInput, + PerseusGroupValidationData, +} from "../../validation.types"; + +function validateGroup( + userInput: PerseusGroupUserInput, + validationData: PerseusGroupValidationData, + strings: PerseusStrings, + locale: string, +): ValidationResult { + const emptyWidgets = emptyWidgetsFunctional( + validationData.widgets, + Object.keys(validationData.widgets), + userInput, + strings, + locale, + ); + + if (emptyWidgets.length === 0) { + return null; + } + + return {type: "invalid", message: null}; +} + +export default validateGroup; diff --git a/packages/perseus/src/widgets/matrix/matrix.tsx b/packages/perseus/src/widgets/matrix/matrix.tsx index eb0dc3bd64..47773730b7 100644 --- a/packages/perseus/src/widgets/matrix/matrix.tsx +++ b/packages/perseus/src/widgets/matrix/matrix.tsx @@ -16,6 +16,7 @@ import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/matrix/matrix-ai-utils"; import scoreMatrix from "./score-matrix"; +import validateMatrix from "./validate-matrix"; import type { PerseusMatrixWidgetAnswers, @@ -600,4 +601,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'. scorer: scoreMatrix, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'. + validator: validateMatrix, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/number-line/number-line.tsx b/packages/perseus/src/widgets/number-line/number-line.tsx index abfcf9c732..5da2f6ce74 100644 --- a/packages/perseus/src/widgets/number-line/number-line.tsx +++ b/packages/perseus/src/widgets/number-line/number-line.tsx @@ -15,6 +15,7 @@ import KhanMath from "../../util/math"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/number-line/number-line-ai-utils"; import scoreNumberLine from "./score-number-line"; +import validateNumberLine from "./validate-number-line"; import type {ChangeableProps} from "../../mixins/changeable"; import type {APIOptions, WidgetExports, FocusPath, Widget} from "../../types"; @@ -808,4 +809,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'. scorer: scoreNumberLine, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'. + validator: validateNumberLine, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/orderer/orderer.tsx b/packages/perseus/src/widgets/orderer/orderer.tsx index 6f297b0b4d..59e63d85c9 100644 --- a/packages/perseus/src/widgets/orderer/orderer.tsx +++ b/packages/perseus/src/widgets/orderer/orderer.tsx @@ -15,6 +15,7 @@ import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/orderer/orderer-ai-utils"; import {scoreOrderer} from "./score-orderer"; +import validateOrderer from "./validate-orderer"; import type {PerseusOrdererWidgetOptions} from "../../perseus-types"; import type {WidgetExports, WidgetProps, Widget} from "../../types"; @@ -785,4 +786,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput scorer: scoreOrderer, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput + validator: validateOrderer, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/plotter/plotter.tsx b/packages/perseus/src/widgets/plotter/plotter.tsx index 8e985698ac..3ae9ddc8df 100644 --- a/packages/perseus/src/widgets/plotter/plotter.tsx +++ b/packages/perseus/src/widgets/plotter/plotter.tsx @@ -14,6 +14,7 @@ import KhanMath from "../../util/math"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/plotter/plotter-ai-utils"; import scorePlotter from "./score-plotter"; +import validatePlotter from "./validate-plotter"; import type {PerseusPlotterWidgetOptions} from "../../perseus-types"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; @@ -1182,4 +1183,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput scorer: scorePlotter, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput + validator: validatePlotter, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/radio/radio.ts b/packages/perseus/src/widgets/radio/radio.ts index 528514e7c9..bc8d6e14be 100644 --- a/packages/perseus/src/widgets/radio/radio.ts +++ b/packages/perseus/src/widgets/radio/radio.ts @@ -4,6 +4,7 @@ import Util from "../../util"; import Radio from "./radio-component"; import scoreRadio from "./score-radio"; +import validateRadio from "./validate-radio"; import type {RenderProps, RadioChoiceWithMetadata} from "./radio-component"; import type {PerseusRadioWidgetOptions} from "../../perseus-types"; @@ -155,4 +156,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput scorer: scoreRadio, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput + validator: validateRadio, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/sorter/sorter.tsx b/packages/perseus/src/widgets/sorter/sorter.tsx index 6959507dce..7d0134b8d3 100644 --- a/packages/perseus/src/widgets/sorter/sorter.tsx +++ b/packages/perseus/src/widgets/sorter/sorter.tsx @@ -6,6 +6,7 @@ import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/sorter/sorter-ai-utils"; import scoreSorter from "./score-sorter"; +import validateSorter from "./validate-sorter"; import type {SortableOption} from "../../components/sortable"; import type {PerseusSorterWidgetOptions} from "../../perseus-types"; @@ -136,4 +137,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput scorer: scoreSorter, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput + validator: validateSorter, } satisfies WidgetExports; diff --git a/packages/perseus/src/widgets/table/table.tsx b/packages/perseus/src/widgets/table/table.tsx index 6340cce3b2..31fb289837 100644 --- a/packages/perseus/src/widgets/table/table.tsx +++ b/packages/perseus/src/widgets/table/table.tsx @@ -11,6 +11,7 @@ import Renderer from "../../renderer"; import Util from "../../util"; import scoreTable from "./score-table"; +import validateTable from "./validate-table"; import type {ChangeableProps} from "../../mixins/changeable"; import type {PerseusTableWidgetOptions} from "../../perseus-types"; @@ -327,4 +328,7 @@ export default { // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput scorer: scoreTable, + // TODO(LEMS-2656): remove TS suppression + // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput + validator: validateTable, } satisfies WidgetExports;