Skip to content

Commit 1a75ca6

Browse files
authored
[Client Validation] Fixes after merging main (#2122)
## Summary: I recently merged `main` and the introduction of `MockWidget` caused some tests to break. I've fixed them in this PR and adjusted the MockWidget to be simpler (ie. not use KhanAnswerTypes) as I don't _think_ that's needed. I also fixed up some related types so that the MockWidget does not exist in our production Widget types, only in tests. The new `MockWidget` illustrates this "test-only" expansion of the widget types. Issue: LEMS-2561 ## Test plan: `yarn test` `yarn typecheck` Author: jeremywiebe Reviewers: jeremywiebe, handeyeco, benchristel, Myranae Required Reviewers: Approved By: handeyeco Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x) Pull Request URL: #2122
1 parent 5f79a67 commit 1a75ca6

File tree

14 files changed

+120
-89
lines changed

14 files changed

+120
-89
lines changed

.changeset/eight-squids-repair.md

Lines changed: 7 additions & 0 deletions
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)

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

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export type MakeWidgetMap<TRegistry> = {
102102
* `PerseusWidgets` with the one defined below.
103103
*
104104
* ```typescript
105-
* declare module "@khanacademy/perseus" {
105+
* declare module "@khanacademy/perseus-core" {
106106
* interface PerseusWidgetTypes {
107107
* // A new widget
108108
* "new-awesomeness": MyAwesomeNewWidget;
@@ -144,7 +144,6 @@ export interface PerseusWidgetTypes {
144144
matcher: MatcherWidget;
145145
matrix: MatrixWidget;
146146
measurer: MeasurerWidget;
147-
"mock-widget": MockWidget;
148147
"molecule-renderer": MoleculeRendererWidget;
149148
"number-line": NumberLineWidget;
150149
"numeric-input": NumericInputWidget;
@@ -181,6 +180,18 @@ export interface PerseusWidgetTypes {
181180
*/
182181
export type PerseusWidgetsMap = MakeWidgetMap<PerseusWidgetTypes>;
183182

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];
194+
184195
/**
185196
* A "PerseusItem" is a classic Perseus item. It is rendered by the
186197
* `ServerItemRenderer` and the layout is pre-set.
@@ -346,8 +357,6 @@ export type MatrixWidget = WidgetOptions<'matrix', PerseusMatrixWidgetOptions>;
346357
// prettier-ignore
347358
export type MeasurerWidget = WidgetOptions<'measurer', PerseusMeasurerWidgetOptions>;
348359
// prettier-ignore
349-
export type MockWidget = WidgetOptions<'mock-widget', MockWidgetOptions>;
350-
// prettier-ignore
351360
export type NumberLineWidget = WidgetOptions<'number-line', PerseusNumberLineWidgetOptions>;
352361
// prettier-ignore
353362
export type NumericInputWidget = WidgetOptions<'numeric-input', PerseusNumericInputWidgetOptions>;
@@ -380,43 +389,6 @@ export type VideoWidget = WidgetOptions<'video', PerseusVideoWidgetOptions>;
380389
//prettier-ignore
381390
export type DeprecatedStandinWidget = WidgetOptions<'deprecated-standin', object>;
382391

383-
export type PerseusWidget =
384-
| CategorizerWidget
385-
| CSProgramWidget
386-
| DefinitionWidget
387-
| DropdownWidget
388-
| ExplanationWidget
389-
| ExpressionWidget
390-
| GradedGroupSetWidget
391-
| GradedGroupWidget
392-
| GrapherWidget
393-
| GroupWidget
394-
| IFrameWidget
395-
| ImageWidget
396-
| InputNumberWidget
397-
| InteractionWidget
398-
| InteractiveGraphWidget
399-
| LabelImageWidget
400-
| MatcherWidget
401-
| MatrixWidget
402-
| MeasurerWidget
403-
| MockWidget
404-
| MoleculeRendererWidget
405-
| NumberLineWidget
406-
| NumericInputWidget
407-
| OrdererWidget
408-
| PassageRefWidget
409-
| PassageWidget
410-
| PhetSimulationWidget
411-
| PlotterWidget
412-
| PythonProgramWidget
413-
| RadioWidget
414-
| RefTargetWidget
415-
| SorterWidget
416-
| TableWidget
417-
| VideoWidget
418-
| DeprecatedStandinWidget;
419-
420392
/**
421393
* A background image applied to various widgets.
422394
*/
@@ -1720,11 +1692,6 @@ export type PerseusVideoWidgetOptions = {
17201692
static?: boolean;
17211693
};
17221694

1723-
export type MockWidgetOptions = {
1724-
static?: boolean;
1725-
value: string;
1726-
};
1727-
17281695
export type PerseusInputNumberWidgetOptions = {
17291696
answerType?:
17301697
| "number"

packages/perseus/src/__testdata__/renderer.testdata.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1+
import type {MockWidget} from "../widgets/mock-widgets/mock-widget-types";
12
import type {RenderProps} from "../widgets/radio";
23
import type {
34
DropdownWidget,
45
ExpressionWidget,
56
ImageWidget,
6-
NumericInputWidget,
7-
MockWidget,
87
PerseusRenderer,
98
} from "@khanacademy/perseus-core";
109

packages/perseus/src/__testdata__/server-item-renderer.testdata.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {
77
type ExpressionWidget,
88
type RadioWidget,
99
type NumericInputWidget,
10-
type MockWidget,
1110
} from "@khanacademy/perseus-core";
1211

12+
import type {MockWidget} from "../widgets/mock-widgets/mock-widget-types";
13+
1314
export const itemWithNumericInput: PerseusItem = {
1415
question: {
1516
content:
@@ -40,7 +41,7 @@ export const itemWithNumericInput: PerseusItem = {
4041
labelText: "What's the answer?",
4142
size: "normal",
4243
},
43-
} as NumericInputWidget,
44+
} satisfies NumericInputWidget,
4445
},
4546
},
4647
hints: [
@@ -64,7 +65,7 @@ export const itemWithMockWidget: PerseusItem = {
6465
options: {
6566
value: "3",
6667
},
67-
} as MockWidget,
68+
} satisfies MockWidget,
6869
},
6970
},
7071
hints: [
@@ -158,14 +159,14 @@ export const itemWithTwoMockWidgets: PerseusItem = {
158159
options: {
159160
value: "3",
160161
},
161-
} as MockWidget,
162+
} satisfies MockWidget,
162163
"mock-widget 2": {
163164
type: "mock-widget",
164165
graded: true,
165166
options: {
166167
value: "3",
167168
},
168-
} as MockWidget,
169+
} satisfies MockWidget,
169170
},
170171
},
171172
hints: [

packages/perseus/src/__tests__/renderer-api.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import mockWidget1Item from "./test-items/mock-widget-1-item";
2121
import mockWidget2Item from "./test-items/mock-widget-2-item";
2222
import tableItem from "./test-items/table-item";
2323

24-
import type {PerseusMockWidgetUserInput} from "../widgets/mock-widgets/mock-widget";
24+
import type {PerseusMockWidgetUserInput} from "../widgets/mock-widgets/mock-widget-types";
2525
import type {UserEvent} from "@testing-library/user-event";
2626

2727
const itemWidget = mockWidget1Item;

packages/perseus/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export const MafsGraphTypeFlags = [
213213
/**
214214
* APIOptions provides different ways to customize the behaviour of Perseus.
215215
*
216-
* @see APIOptionsWithDefaults
216+
* @see {@link APIOptionsWithDefaults}
217217
*/
218218
export type APIOptions = Readonly<{
219219
isArticle?: boolean;

packages/perseus/src/widget-ai-utils/mock-widget/mock-widget.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {registerWidget} from "../../widgets";
55
import {renderQuestion} from "../../widgets/__testutils__/renderQuestion";
66
import MockWidgetExport from "../../widgets/mock-widgets/mock-widget";
77

8-
import type {PerseusRenderer, MockWidget} from "@khanacademy/perseus-core";
8+
import type {MockWidget} from "../../widgets/mock-widgets/mock-widget-types";
9+
import type {PerseusRenderer} from "@khanacademy/perseus-core";
910
import type {UserEvent} from "@testing-library/user-event";
1011

1112
const question: PerseusRenderer = {
@@ -25,7 +26,7 @@ const question: PerseusRenderer = {
2526
value: "42",
2627
},
2728
alignment: "default",
28-
} as MockWidget,
29+
} satisfies MockWidget,
2930
},
3031
};
3132

packages/perseus/src/widget-ai-utils/mock-widget/prompt-utils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {getPromptJSON} from "./prompt-utils";
22

3-
import type {PerseusMockWidgetUserInput} from "../../widgets/mock-widgets/mock-widget";
3+
import type {PerseusMockWidgetUserInput} from "../../widgets/mock-widgets/mock-widget-types";
44

55
describe("InputNumber getPromptJSON", () => {
66
it("it returns JSON with the expected format and fields", () => {

packages/perseus/src/widget-ai-utils/mock-widget/prompt-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type {PerseusMockWidgetUserInput} from "../../widgets/mock-widgets/mock-widget";
21
import type mockWidget from "../../widgets/mock-widgets/mock-widget";
2+
import type {PerseusMockWidgetUserInput} from "../../widgets/mock-widgets/mock-widget-types";
33
import type React from "react";
44

55
export type MockWidgetPromptJSON = {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type {WidgetOptions} from "@khanacademy/perseus-core";
2+
3+
export type MockWidget = WidgetOptions<"mock-widget", MockWidgetOptions>;
4+
5+
export type MockWidgetOptions = {
6+
static?: boolean;
7+
value: string;
8+
};
9+
10+
export type PerseusMockWidgetRubric = {
11+
value: string;
12+
};
13+
14+
export type PerseusMockWidgetUserInput = {
15+
currentValue: string;
16+
};
17+
18+
// Extend the widget registries for testing
19+
// See @khanacademy/perseus-core's PerseusWidgetTypes for a full explanation.
20+
// Basically, we're extending the interface from that package so that our
21+
// testing code knows of the MockWidget. In production code, there's no
22+
// knowledge of the mock widget.
23+
declare module "@khanacademy/perseus-core" {
24+
export interface PerseusWidgetTypes {
25+
"mock-widget": MockWidget;
26+
}
27+
}

packages/perseus/src/widgets/mock-widgets/mock-widget.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,15 @@ import * as React from "react";
66
import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/mock-widget/prompt-utils";
77

88
import scoreMockWidget from "./score-mock-widget";
9+
import validateMockWidget from "./validate-mock-widget";
910

11+
import type {
12+
MockWidgetOptions,
13+
PerseusMockWidgetRubric,
14+
PerseusMockWidgetUserInput,
15+
} from "./mock-widget-types";
1016
import type {WidgetExports, WidgetProps, Widget, FocusPath} from "../../types";
1117
import type {MockWidgetPromptJSON} from "../../widget-ai-utils/mock-widget/prompt-utils";
12-
import type {MockWidgetOptions} from "@khanacademy/perseus-core";
13-
14-
export type PerseusMockWidgetRubric = {
15-
value: string;
16-
};
17-
18-
export type PerseusMockWidgetUserInput = {
19-
currentValue: string;
20-
};
2118

2219
type ExternalProps = WidgetProps<MockWidgetOptions, PerseusMockWidgetRubric>;
2320

@@ -41,7 +38,7 @@ type Props = ExternalProps & {
4138
*
4239
* You can register this widget for your tests by calling `registerWidget("mock-widget", MockWidget);`
4340
*/
44-
export class MockWidget extends React.Component<Props> implements Widget {
41+
class MockWidgetComponent extends React.Component<Props> implements Widget {
4542
static defaultProps: DefaultProps = {
4643
currentValue: "",
4744
};
@@ -93,7 +90,7 @@ export class MockWidget extends React.Component<Props> implements Widget {
9390
};
9491

9592
getUserInput(): PerseusMockWidgetUserInput {
96-
return MockWidget.getUserInputFromProps(this.props);
93+
return MockWidgetComponent.getUserInputFromProps(this.props);
9794
}
9895

9996
handleChange: (
@@ -131,9 +128,12 @@ const styles = StyleSheet.create({
131128
export default {
132129
name: "mock-widget",
133130
displayName: "Mock Widget",
134-
widget: MockWidget,
131+
widget: MockWidgetComponent,
135132
isLintable: true,
136133
// TODO(LEMS-2656): remove TS suppression
137134
// @ts-expect-error: Type 'UserInput' is not assignable to type 'MockWidget'.
138135
scorer: scoreMockWidget,
139-
} satisfies WidgetExports<typeof MockWidget>;
136+
// TODO(LEMS-2656): remove TS suppression
137+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMockWidgetUserInput'.
138+
validator: validateMockWidget,
139+
} satisfies WidgetExports<typeof MockWidgetComponent>;

packages/perseus/src/widgets/mock-widgets/score-mock-widget.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import {KhanAnswerTypes} from "@khanacademy/perseus-score";
1+
import validateMockWidget from "./validate-mock-widget";
22

33
import type {
44
PerseusMockWidgetUserInput,
55
PerseusMockWidgetRubric,
6-
} from "./mock-widget";
6+
} from "./mock-widget-types";
77
import type {PerseusStrings} from "../../strings";
88
import type {PerseusScore} from "@khanacademy/perseus";
99

@@ -12,25 +12,16 @@ function scoreMockWidget(
1212
rubric: PerseusMockWidgetRubric,
1313
strings: PerseusStrings,
1414
): PerseusScore {
15-
const stringValue = `${rubric.value}`;
16-
const val = KhanAnswerTypes.number.createValidatorFunctional(
17-
stringValue,
18-
strings,
19-
);
20-
21-
const result = val(userInput.currentValue);
22-
23-
if (result.empty) {
24-
return {
25-
type: "invalid",
26-
message: result.message,
27-
};
15+
const validationResult = validateMockWidget(userInput);
16+
if (validationResult != null) {
17+
return validationResult;
2818
}
19+
2920
return {
3021
type: "points",
31-
earned: result.correct ? 1 : 0,
22+
earned: userInput.currentValue === rubric.value ? 1 : 0,
3223
total: 1,
33-
message: result.message,
24+
message: "",
3425
};
3526
}
3627

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import validateMockWidget from "./validate-mock-widget";
2+
3+
import type {PerseusMockWidgetUserInput} from "./mock-widget-types";
4+
5+
describe("mock-widget", () => {
6+
it("should be invalid if no value provided", () => {
7+
const input: PerseusMockWidgetUserInput = {currentValue: ""};
8+
9+
const result = validateMockWidget(input);
10+
11+
expect(result).toHaveInvalidInput();
12+
});
13+
14+
it("should be valid if a value provided", () => {
15+
const input: PerseusMockWidgetUserInput = {currentValue: "a"};
16+
17+
const result = validateMockWidget(input);
18+
19+
expect(result).toBeNull();
20+
});
21+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type {PerseusMockWidgetUserInput} from "./mock-widget-types";
2+
import type {ValidationResult} from "../../types";
3+
4+
function validateMockWidget(
5+
userInput: PerseusMockWidgetUserInput,
6+
): ValidationResult {
7+
if (userInput.currentValue == null || userInput.currentValue === "") {
8+
return {
9+
type: "invalid",
10+
message: "",
11+
};
12+
}
13+
14+
return null;
15+
}
16+
17+
export default validateMockWidget;

0 commit comments

Comments
 (0)