@khanacademy/perseus 12.2.0 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/article-renderer.d.ts +3 -2
  3. package/dist/error-boundary.d.ts +1 -0
  4. package/dist/es/index.css +1 -66
  5. package/dist/es/index.js +86 -66
  6. package/dist/es/index.js.map +1 -1
  7. package/dist/index.css +1 -66
  8. package/dist/index.js +77 -54
  9. package/dist/index.js.map +1 -1
  10. package/dist/renderer.d.ts +4 -1
  11. package/dist/styles/constants.d.ts +2 -2
  12. package/dist/widget-container.d.ts +2 -0
  13. package/dist/widgets/expression.d.ts +5 -4
  14. package/package.json +8 -8
  15. package/src/__tests__/renderer-api.test.tsx +18 -23
  16. package/src/article-renderer.tsx +15 -13
  17. package/src/error-boundary.tsx +3 -0
  18. package/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +5 -5
  19. package/src/renderer.tsx +29 -27
  20. package/src/server-item-renderer.tsx +1 -0
  21. package/src/styles/constants.ts +4 -4
  22. package/src/styles/perseus-renderer.less +0 -10
  23. package/src/styles/variables.less +2 -2
  24. package/src/styles/variables.shared.less +2 -2
  25. package/src/styles/widgets/expression.less +0 -77
  26. package/src/styles/widgets/passage.less +1 -19
  27. package/src/widget-container.tsx +32 -10
  28. package/src/widgets/__stories__/label-image.stories.tsx +30 -0
  29. package/src/widgets/__testdata__/label-image.testdata.ts +63 -0
  30. package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +5 -5
  31. package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +28 -28
  32. package/src/widgets/__tests__/expression.test.tsx +12 -9
  33. package/src/widgets/__tests__/renderQuestion.tsx +30 -13
  34. package/src/widgets/expression.tsx +19 -25
  35. package/src/widgets/transformer.tsx +1 -2
  36. package/tsconfig-build.tsbuildinfo +1 -1
@@ -4,6 +4,8 @@ import classNames from "classnames";
4
4
  import * as React from "react";
5
5
  import ReactDOM from "react-dom";
6
6
 
7
+ import {DependenciesContext} from "./dependencies";
8
+ import ErrorBoundary from "./error-boundary";
7
9
  import {zIndexInteractiveComponent} from "./styles/constants";
8
10
  import {containerSizeClass, getClassFromWidth} from "./util/sizing-utils";
9
11
  import * as Widgets from "./widgets";
@@ -15,6 +17,7 @@ import type {LinterContextProps} from "@khanacademy/perseus-linter";
15
17
  type Props = {
16
18
  shouldHighlight: boolean;
17
19
  type: string; // widget type/name,
20
+ id: string; // widget id
18
21
  initialProps: WidgetProps<any, PerseusWidgetOptions>;
19
22
  linterContext: LinterContextProps;
20
23
  };
@@ -29,6 +32,8 @@ type State = {
29
32
  };
30
33
 
31
34
  class WidgetContainer extends React.Component<Props, State> {
35
+ widgetRef = React.createRef<React.ComponentType<any>>();
36
+
32
37
  static defaultProps: DefaultProps = {
33
38
  linterContext: linterContextDefault,
34
39
  };
@@ -132,14 +137,32 @@ class WidgetContainer extends React.Component<Props, State> {
132
137
  className={className}
133
138
  style={isStatic ? staticContainerStyles : {}}
134
139
  >
135
- <WidgetType
136
- {...this.state.widgetProps}
137
- linterContext={linterContext}
138
- containerSizeClass={this.state.sizeClass}
139
- // eslint-disable-next-line react/no-string-refs
140
- ref="widget"
141
- />
142
- {isStatic && <div style={staticOverlayStyles} />}
140
+ <DependenciesContext.Consumer>
141
+ {({analytics}) => (
142
+ <ErrorBoundary
143
+ metadata={{
144
+ widget_type: type,
145
+ widget_id: this.props.id,
146
+ }}
147
+ onError={(error) => {
148
+ analytics.onAnalyticsEvent({
149
+ type: "perseus:widget-rendering-error",
150
+ payload: {
151
+ widgetType: type,
152
+ },
153
+ });
154
+ }}
155
+ >
156
+ <WidgetType
157
+ {...this.state.widgetProps}
158
+ linterContext={linterContext}
159
+ containerSizeClass={this.state.sizeClass}
160
+ ref={this.widgetRef}
161
+ />
162
+ {isStatic && <div style={staticOverlayStyles} />}
163
+ </ErrorBoundary>
164
+ )}
165
+ </DependenciesContext.Consumer>
143
166
  </div>
144
167
  );
145
168
  }
@@ -163,8 +186,7 @@ class WidgetContainer extends React.Component<Props, State> {
163
186
  }
164
187
 
165
188
  getWidget: () => any = () => {
166
- // eslint-disable-next-line react/no-string-refs
167
- return this.refs.widget;
189
+ return this.widgetRef.current;
168
190
  };
169
191
 
170
192
  replaceWidgetProps: (arg1: WidgetProps<any, PerseusWidgetOptions>) => void =
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+
3
+ import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";
4
+ import {question} from "../__testdata__/label-image.testdata";
5
+
6
+ import type {APIOptions} from "../../types";
7
+
8
+ type StoryArgs = {
9
+ isMobile: boolean;
10
+ };
11
+
12
+ type ImageStory = {
13
+ title: string;
14
+ args: StoryArgs;
15
+ };
16
+
17
+ export const Question1 = (args: StoryArgs): React.ReactElement => {
18
+ const apiOptions: APIOptions = {
19
+ isMobile: args.isMobile,
20
+ };
21
+
22
+ return <RendererWithDebugUI question={question} apiOptions={apiOptions} />;
23
+ };
24
+
25
+ export default {
26
+ title: "Perseus/Widgets/Label Image",
27
+ args: {
28
+ isMobile: false,
29
+ },
30
+ } as ImageStory;
@@ -0,0 +1,63 @@
1
+ import type {PerseusRenderer} from "../../perseus-types";
2
+
3
+ export const question: PerseusRenderer = {
4
+ content:
5
+ "Carol created a chart and a bar graph to show how many of each type of vehicle were in her supermarket parking lot.\n\nVehicle Type | Number in the parking lot\n:- | :-: \nTrucks| $25$ \nVans | $5$ \nCars| $40$ \nSUVs | $10$ \n\n**Label each bar on the bar graph.**\n\n[[☃ label-image 1]]\n\n",
6
+ images: {
7
+ "web+graphie://ka-perseus-graphie.s3.amazonaws.com/1e28332fd2321975639ab50c9ce442e568c18421":
8
+ {
9
+ width: 341,
10
+ height: 310,
11
+ },
12
+ },
13
+ widgets: {
14
+ "label-image 1": {
15
+ type: "label-image",
16
+ alignment: "default",
17
+ static: false,
18
+ graded: true,
19
+ options: {
20
+ static: false,
21
+ choices: ["Trucks", "Vans", "Cars", "SUVs"],
22
+ imageAlt:
23
+ "A bar graph with four bar lines that shows the horizontal axis labeled Number in the parking lot and the vertical axis labeled Vehicle Type. The horizontal axis is labeled, from left to right: 0, 10, 20, 30, 40, and 50. The vertical axis has, from the bottom to the top, four unlabeled bar lines as follows: the first unlabeled bar line extends to the middle of 0 and 10, the second unlabeled bar line extends to 40, the third unlabeled bar line extends to the middle of 20 and 30, and fourth unlabeled bar line extends to 10.",
24
+ imageUrl:
25
+ "web+graphie://ka-perseus-graphie.s3.amazonaws.com/56c60c72e96cd353e4a8b5434506cd3a21e717af",
26
+ imageWidth: 415,
27
+ imageHeight: 314,
28
+ markers: [
29
+ {
30
+ answers: ["SUVs"],
31
+ label: "The fourth unlabeled bar line.",
32
+ x: 25,
33
+ y: 17.7,
34
+ },
35
+ {
36
+ answers: ["Trucks"],
37
+ label: "The third unlabeled bar line.",
38
+ x: 25,
39
+ y: 35.3,
40
+ },
41
+ {
42
+ answers: ["Cars"],
43
+ label: "The second unlabeled bar line.",
44
+ x: 25,
45
+ y: 53,
46
+ },
47
+ {
48
+ answers: ["Vans"],
49
+ label: "The first unlabeled bar line.",
50
+ x: 25,
51
+ y: 70.3,
52
+ },
53
+ ],
54
+ multipleAnswers: false,
55
+ hideChoicesFromInstructions: true,
56
+ },
57
+ version: {
58
+ major: 0,
59
+ minor: 0,
60
+ },
61
+ },
62
+ },
63
+ };
@@ -192,7 +192,7 @@ exports[`group widget should snapshot: initial render 1`] = `
192
192
  style="border-color: transparent; border-radius: 50%;"
193
193
  >
194
194
  <div
195
- class="circle_1tys2th"
195
+ class="circle_1w6i3h1"
196
196
  data-is-radio-icon="true"
197
197
  data-test-id="choice-icon__library-choice-icon"
198
198
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -307,7 +307,7 @@ exports[`group widget should snapshot: initial render 1`] = `
307
307
  style="border-color: transparent; border-radius: 50%;"
308
308
  >
309
309
  <div
310
- class="circle_1tys2th"
310
+ class="circle_1w6i3h1"
311
311
  data-is-radio-icon="true"
312
312
  data-test-id="choice-icon__library-choice-icon"
313
313
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -422,7 +422,7 @@ exports[`group widget should snapshot: initial render 1`] = `
422
422
  style="border-color: transparent; border-radius: 50%;"
423
423
  >
424
424
  <div
425
- class="circle_1tys2th"
425
+ class="circle_1w6i3h1"
426
426
  data-is-radio-icon="true"
427
427
  data-test-id="choice-icon__library-choice-icon"
428
428
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -537,7 +537,7 @@ exports[`group widget should snapshot: initial render 1`] = `
537
537
  style="border-color: transparent; border-radius: 50%;"
538
538
  >
539
539
  <div
540
- class="circle_1tys2th"
540
+ class="circle_1w6i3h1"
541
541
  data-is-radio-icon="true"
542
542
  data-test-id="choice-icon__library-choice-icon"
543
543
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -652,7 +652,7 @@ exports[`group widget should snapshot: initial render 1`] = `
652
652
  style="border-color: transparent; border-radius: 50%;"
653
653
  >
654
654
  <div
655
- class="circle_1tys2th"
655
+ class="circle_1w6i3h1"
656
656
  data-is-radio-icon="true"
657
657
  data-test-id="choice-icon__library-choice-icon"
658
658
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -127,7 +127,7 @@ exports[`multi-choice question should snapshot the same when invalid: invalid st
127
127
  style="border-color: transparent; border-radius: 5px;"
128
128
  >
129
129
  <div
130
- class="circle_1tys2th"
130
+ class="circle_1w6i3h1"
131
131
  data-is-radio-icon="true"
132
132
  data-test-id="choice-icon__library-choice-icon"
133
133
  style="border-color: #1865f2; color: rgb(255, 255, 255); border-radius: 3px; background-color: rgb(24, 101, 242);"
@@ -242,7 +242,7 @@ exports[`multi-choice question should snapshot the same when invalid: invalid st
242
242
  style="border-color: transparent; border-radius: 5px;"
243
243
  >
244
244
  <div
245
- class="circle_1tys2th"
245
+ class="circle_1w6i3h1"
246
246
  data-is-radio-icon="true"
247
247
  data-test-id="choice-icon__library-choice-icon"
248
248
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 3px;"
@@ -357,7 +357,7 @@ exports[`multi-choice question should snapshot the same when invalid: invalid st
357
357
  style="border-color: transparent; border-radius: 5px;"
358
358
  >
359
359
  <div
360
- class="circle_1tys2th"
360
+ class="circle_1w6i3h1"
361
361
  data-is-radio-icon="true"
362
362
  data-test-id="choice-icon__library-choice-icon"
363
363
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 3px;"
@@ -463,7 +463,7 @@ exports[`multi-choice question should snapshot the same when invalid: invalid st
463
463
  style="border-color: transparent; border-radius: 5px;"
464
464
  >
465
465
  <div
466
- class="circle_1tys2th"
466
+ class="circle_1w6i3h1"
467
467
  data-is-radio-icon="true"
468
468
  data-test-id="choice-icon__library-choice-icon"
469
469
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 3px;"
@@ -700,7 +700,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
700
700
  style="border-color: transparent; border-radius: 50%;"
701
701
  >
702
702
  <div
703
- class="circle_1tys2th"
703
+ class="circle_1w6i3h1"
704
704
  data-is-radio-icon="true"
705
705
  data-test-id="choice-icon__library-choice-icon"
706
706
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -828,7 +828,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
828
828
  style="border-color: transparent; border-radius: 50%;"
829
829
  >
830
830
  <div
831
- class="circle_1tys2th"
831
+ class="circle_1w6i3h1"
832
832
  data-is-radio-icon="true"
833
833
  data-test-id="choice-icon__library-choice-icon"
834
834
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -943,7 +943,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
943
943
  style="border-color: transparent; border-radius: 50%;"
944
944
  >
945
945
  <div
946
- class="circle_1tys2th"
946
+ class="circle_1w6i3h1"
947
947
  data-is-radio-icon="true"
948
948
  data-test-id="choice-icon__library-choice-icon"
949
949
  style="border-color: #1865f2; color: rgb(255, 255, 255); border-radius: 24px; background-color: rgb(24, 101, 242);"
@@ -1061,7 +1061,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
1061
1061
  style="border-color: transparent; border-radius: 50%;"
1062
1062
  >
1063
1063
  <div
1064
- class="circle_1tys2th"
1064
+ class="circle_1w6i3h1"
1065
1065
  data-is-radio-icon="true"
1066
1066
  data-test-id="choice-icon__library-choice-icon"
1067
1067
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -1272,7 +1272,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
1272
1272
  style="border-color: transparent; border-radius: 50%;"
1273
1273
  >
1274
1274
  <div
1275
- class="circle_1tys2th"
1275
+ class="circle_1w6i3h1"
1276
1276
  data-is-radio-icon="true"
1277
1277
  data-test-id="choice-icon__library-choice-icon"
1278
1278
  style="border-color: #1865f2; color: rgb(255, 255, 255); border-radius: 24px; background-color: rgb(24, 101, 242);"
@@ -1400,7 +1400,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
1400
1400
  style="border-color: transparent; border-radius: 50%;"
1401
1401
  >
1402
1402
  <div
1403
- class="circle_1tys2th"
1403
+ class="circle_1w6i3h1"
1404
1404
  data-is-radio-icon="true"
1405
1405
  data-test-id="choice-icon__library-choice-icon"
1406
1406
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -1515,7 +1515,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
1515
1515
  style="border-color: transparent; border-radius: 50%;"
1516
1516
  >
1517
1517
  <div
1518
- class="circle_1tys2th"
1518
+ class="circle_1w6i3h1"
1519
1519
  data-is-radio-icon="true"
1520
1520
  data-test-id="choice-icon__library-choice-icon"
1521
1521
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -1633,7 +1633,7 @@ exports[`single-choice question reviewMode: false should snapshot the same with
1633
1633
  style="border-color: transparent; border-radius: 50%;"
1634
1634
  >
1635
1635
  <div
1636
- class="circle_1tys2th"
1636
+ class="circle_1w6i3h1"
1637
1637
  data-is-radio-icon="true"
1638
1638
  data-test-id="choice-icon__library-choice-icon"
1639
1639
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -1844,7 +1844,7 @@ exports[`single-choice question reviewMode: false should snapshot the same: firs
1844
1844
  style="border-color: transparent; border-radius: 50%;"
1845
1845
  >
1846
1846
  <div
1847
- class="circle_1tys2th"
1847
+ class="circle_1w6i3h1"
1848
1848
  data-is-radio-icon="true"
1849
1849
  data-test-id="choice-icon__library-choice-icon"
1850
1850
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -1972,7 +1972,7 @@ exports[`single-choice question reviewMode: false should snapshot the same: firs
1972
1972
  style="border-color: transparent; border-radius: 50%;"
1973
1973
  >
1974
1974
  <div
1975
- class="circle_1tys2th"
1975
+ class="circle_1w6i3h1"
1976
1976
  data-is-radio-icon="true"
1977
1977
  data-test-id="choice-icon__library-choice-icon"
1978
1978
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -2087,7 +2087,7 @@ exports[`single-choice question reviewMode: false should snapshot the same: firs
2087
2087
  style="border-color: transparent; border-radius: 50%;"
2088
2088
  >
2089
2089
  <div
2090
- class="circle_1tys2th"
2090
+ class="circle_1w6i3h1"
2091
2091
  data-is-radio-icon="true"
2092
2092
  data-test-id="choice-icon__library-choice-icon"
2093
2093
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -2205,7 +2205,7 @@ exports[`single-choice question reviewMode: false should snapshot the same: firs
2205
2205
  style="border-color: transparent; border-radius: 50%;"
2206
2206
  >
2207
2207
  <div
2208
- class="circle_1tys2th"
2208
+ class="circle_1w6i3h1"
2209
2209
  data-is-radio-icon="true"
2210
2210
  data-test-id="choice-icon__library-choice-icon"
2211
2211
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -2416,7 +2416,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with c
2416
2416
  style="border-color: transparent; border-radius: 50%;"
2417
2417
  >
2418
2418
  <div
2419
- class="circle_1tys2th"
2419
+ class="circle_1w6i3h1"
2420
2420
  data-is-radio-icon="true"
2421
2421
  data-test-id="choice-icon__library-choice-icon"
2422
2422
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -2544,7 +2544,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with c
2544
2544
  style="border-color: transparent; border-radius: 50%;"
2545
2545
  >
2546
2546
  <div
2547
- class="circle_1tys2th"
2547
+ class="circle_1w6i3h1"
2548
2548
  data-is-radio-icon="true"
2549
2549
  data-test-id="choice-icon__library-choice-icon"
2550
2550
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -2659,7 +2659,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with c
2659
2659
  style="border-color: transparent; border-radius: 50%;"
2660
2660
  >
2661
2661
  <div
2662
- class="circle_1tys2th"
2662
+ class="circle_1w6i3h1"
2663
2663
  data-is-radio-icon="true"
2664
2664
  data-test-id="choice-icon__library-choice-icon"
2665
2665
  style="border-color: #1865f2; color: rgb(255, 255, 255); border-radius: 24px; background-color: rgb(24, 101, 242);"
@@ -2777,7 +2777,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with c
2777
2777
  style="border-color: transparent; border-radius: 50%;"
2778
2778
  >
2779
2779
  <div
2780
- class="circle_1tys2th"
2780
+ class="circle_1w6i3h1"
2781
2781
  data-is-radio-icon="true"
2782
2782
  data-test-id="choice-icon__library-choice-icon"
2783
2783
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -2988,7 +2988,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with i
2988
2988
  style="border-color: transparent; border-radius: 50%;"
2989
2989
  >
2990
2990
  <div
2991
- class="circle_1tys2th"
2991
+ class="circle_1w6i3h1"
2992
2992
  data-is-radio-icon="true"
2993
2993
  data-test-id="choice-icon__library-choice-icon"
2994
2994
  style="border-color: #1865f2; color: rgb(255, 255, 255); border-radius: 24px; background-color: rgb(24, 101, 242);"
@@ -3116,7 +3116,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with i
3116
3116
  style="border-color: transparent; border-radius: 50%;"
3117
3117
  >
3118
3118
  <div
3119
- class="circle_1tys2th"
3119
+ class="circle_1w6i3h1"
3120
3120
  data-is-radio-icon="true"
3121
3121
  data-test-id="choice-icon__library-choice-icon"
3122
3122
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -3231,7 +3231,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with i
3231
3231
  style="border-color: transparent; border-radius: 50%;"
3232
3232
  >
3233
3233
  <div
3234
- class="circle_1tys2th"
3234
+ class="circle_1w6i3h1"
3235
3235
  data-is-radio-icon="true"
3236
3236
  data-test-id="choice-icon__library-choice-icon"
3237
3237
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -3349,7 +3349,7 @@ exports[`single-choice question reviewMode: true should snapshot the same with i
3349
3349
  style="border-color: transparent; border-radius: 50%;"
3350
3350
  >
3351
3351
  <div
3352
- class="circle_1tys2th"
3352
+ class="circle_1w6i3h1"
3353
3353
  data-is-radio-icon="true"
3354
3354
  data-test-id="choice-icon__library-choice-icon"
3355
3355
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -3560,7 +3560,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
3560
3560
  style="border-color: transparent; border-radius: 50%;"
3561
3561
  >
3562
3562
  <div
3563
- class="circle_1tys2th-o_O-circleIncorrect_gxqnr9"
3563
+ class="circle_1w6i3h1-o_O-circleIncorrect_gxqnr9"
3564
3564
  data-is-radio-icon="true"
3565
3565
  data-test-id="choice-icon__library-choice-icon"
3566
3566
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -3769,7 +3769,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
3769
3769
  style="border-color: transparent; border-radius: 50%;"
3770
3770
  >
3771
3771
  <div
3772
- class="circle_1tys2th-o_O-circleIncorrect_gxqnr9"
3772
+ class="circle_1w6i3h1-o_O-circleIncorrect_gxqnr9"
3773
3773
  data-is-radio-icon="true"
3774
3774
  data-test-id="choice-icon__library-choice-icon"
3775
3775
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -3949,7 +3949,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
3949
3949
  style="border-color: transparent; border-radius: 50%;"
3950
3950
  >
3951
3951
  <div
3952
- class="circle_1tys2th-o_O-circleCorrect_ifnw95"
3952
+ class="circle_1w6i3h1-o_O-circleCorrect_ifnw95"
3953
3953
  data-is-radio-icon="true"
3954
3954
  data-test-id="choice-icon__library-choice-icon"
3955
3955
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -4127,7 +4127,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
4127
4127
  style="border-color: transparent; border-radius: 50%;"
4128
4128
  >
4129
4129
  <div
4130
- class="circle_1tys2th-o_O-circleIncorrect_gxqnr9"
4130
+ class="circle_1w6i3h1-o_O-circleIncorrect_gxqnr9"
4131
4131
  data-is-radio-icon="true"
4132
4132
  data-test-id="choice-icon__library-choice-icon"
4133
4133
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -71,14 +71,6 @@ const assertInvalid = (
71
71
  }
72
72
  const [_, score] = renderer.guessAndScore();
73
73
  expect(score).toMatchObject({type: "invalid"});
74
-
75
- expect(testDependenciesV2.analytics.onAnalyticsEvent).toHaveBeenCalledWith({
76
- type: "perseus:expression-evaluated",
77
- payload: {
78
- result: "invalid",
79
- virtualKeypadVersion: "PERSEUS_MATH_INPUT",
80
- },
81
- });
82
74
  };
83
75
 
84
76
  describe("Expression Widget", function () {
@@ -87,6 +79,9 @@ describe("Expression Widget", function () {
87
79
  jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
88
80
  testDependencies,
89
81
  );
82
+ jest.spyOn(Dependencies, "useDependencies").mockReturnValue(
83
+ testDependenciesV2,
84
+ );
90
85
  });
91
86
 
92
87
  describe("grading", function () {
@@ -208,12 +203,20 @@ describe("Expression Widget", function () {
208
203
  // widget did anything useful or that the keypad worked. We
209
204
  // just want to make sure the code that derives which
210
205
  // keypad version it detected is correct.
211
- result: "invalid",
206
+ result: "correct",
212
207
  virtualKeypadVersion,
213
208
  },
214
209
  });
215
210
  };
216
211
 
212
+ beforeEach(() => {
213
+ jest.spyOn(Expression, "validate").mockReturnValue({
214
+ type: "points",
215
+ earned: 1,
216
+ total: 1,
217
+ });
218
+ });
219
+
217
220
  it("should set the virtual keypad version to REACT_NATIVE_KEYPAD when nativeKeypadProxy is provided", () => {
218
221
  assertKeypadVersion(
219
222
  {nativeKeypadProxy: jest.fn()},
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
 
5
5
  // eslint-disable-next-line import/no-relative-packages
6
6
  import {testDependenciesV2} from "../../../../../testing/test-dependencies";
7
- import {DependenciesContext} from "../../dependencies";
7
+ import {DependenciesContext, useDependencies} from "../../dependencies";
8
8
  import * as Perseus from "../../index";
9
9
  import {registerAllWidgetsForTesting} from "../../util/register-all-widgets-for-testing";
10
10
 
@@ -33,14 +33,11 @@ export const renderQuestion = (
33
33
  const {container, rerender, unmount} = render(
34
34
  <RenderStateRoot>
35
35
  <DependenciesContext.Provider value={testDependenciesV2}>
36
- <Perseus.Renderer
36
+ <Renderer
37
37
  ref={(node) => (renderer = node)}
38
- content={question.content}
39
- images={question.images}
40
- widgets={question.widgets}
41
- problemNum={0}
38
+ question={question as any}
42
39
  apiOptions={apiOptions}
43
- {...extraProps}
40
+ extraProps={extraProps}
44
41
  />
45
42
  </DependenciesContext.Provider>
46
43
  </RenderStateRoot>,
@@ -55,14 +52,11 @@ export const renderQuestion = (
55
52
  rerender(
56
53
  <RenderStateRoot>
57
54
  <DependenciesContext.Provider value={testDependenciesV2}>
58
- <Perseus.Renderer
55
+ <Renderer
59
56
  ref={(node) => (renderer = node)}
60
- content={question.content}
61
- images={question.images}
62
- widgets={question.widgets}
63
- problemNum={0}
57
+ question={question}
64
58
  apiOptions={apiOptions}
65
- {...extraProps}
59
+ extraProps={extraProps}
66
60
  />
67
61
  </DependenciesContext.Provider>
68
62
  </RenderStateRoot>,
@@ -74,3 +68,26 @@ export const renderQuestion = (
74
68
 
75
69
  return {container, renderer, rerender: renderAgain, unmount};
76
70
  };
71
+
72
+ const Renderer = React.forwardRef<
73
+ Perseus.Renderer,
74
+ {
75
+ question: PerseusRenderer;
76
+ apiOptions: APIOptions;
77
+ extraProps?: PropsFor<typeof Perseus.Renderer>;
78
+ }
79
+ >((props, ref) => {
80
+ const dependencies = useDependencies();
81
+ return (
82
+ <Perseus.Renderer
83
+ ref={ref}
84
+ content={props.question.content}
85
+ images={props.question.images}
86
+ widgets={props.question.widgets}
87
+ problemNum={0}
88
+ apiOptions={props.apiOptions}
89
+ {...props.extraProps}
90
+ {...dependencies}
91
+ />
92
+ );
93
+ });
@@ -369,25 +369,31 @@ export class Expression extends React.Component<Props, ExpressionState> {
369
369
  };
370
370
 
371
371
  simpleValidate: (
372
- rubric: Rubric,
372
+ rubric: Rubric & {scoring?: boolean},
373
373
  onInputError: OnInputErrorFunctionType,
374
- ) => PerseusScore = (rubric, onInputError) => {
375
- onInputError = onInputError || function () {};
376
- const result = Expression.validate(
374
+ ) => PerseusScore = ({scoring, ...rubric}, onInputError) => {
375
+ const score = Expression.validate(
377
376
  this.getUserInput(),
378
377
  rubric,
379
- onInputError,
378
+ onInputError || function () {},
380
379
  );
381
380
 
382
- this.sendExpressionEvaluatedEvent(
383
- result.type === "invalid"
384
- ? "invalid"
385
- : result.earned === result.total
386
- ? "correct"
387
- : "incorrect",
388
- );
381
+ // "scoring" is a flag that indicates when we are checking answers.
382
+ // otherwise, we may just be checking validity after changes.
383
+ if (scoring && score.type !== "invalid") {
384
+ this.props.analytics?.onAnalyticsEvent({
385
+ type: "perseus:expression-evaluated",
386
+ payload: {
387
+ result:
388
+ score.earned === score.total ? "correct" : "incorrect",
389
+ virtualKeypadVersion: deriveKeypadVersion(
390
+ this.props.apiOptions,
391
+ ),
392
+ },
393
+ });
394
+ }
389
395
 
390
- return result;
396
+ return score;
391
397
  };
392
398
 
393
399
  getUserInput: () => string = () => {
@@ -494,18 +500,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
494
500
  );
495
501
  };
496
502
 
497
- sendExpressionEvaluatedEvent(result: "correct" | "incorrect" | "invalid") {
498
- this.props.analytics?.onAnalyticsEvent({
499
- type: "perseus:expression-evaluated",
500
- payload: {
501
- result,
502
- virtualKeypadVersion: deriveKeypadVersion(
503
- this.props.apiOptions,
504
- ),
505
- },
506
- });
507
- }
508
-
509
503
  render():
510
504
  | React.ReactNode
511
505
  | React.ReactElement<React.ComponentProps<"div">> {