@khanacademy/perseus 8.0.0 → 10.0.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 (60) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/article-renderer.d.ts +2 -1
  3. package/dist/components/info-tip.d.ts +2 -1
  4. package/dist/components/input-with-examples.d.ts +1 -1
  5. package/dist/components/sortable.d.ts +1 -1
  6. package/dist/dependencies.d.ts +7 -1
  7. package/dist/es/index.css +3 -5
  8. package/dist/es/index.js +72 -45
  9. package/dist/es/index.js.map +1 -1
  10. package/dist/hint-renderer.d.ts +2 -1
  11. package/dist/hints-renderer.d.ts +2 -1
  12. package/dist/index.css +3 -5
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +72 -46
  15. package/dist/index.js.map +1 -1
  16. package/dist/interaction-tracker.d.ts +3 -3
  17. package/dist/item-renderer.d.ts +3 -1
  18. package/dist/multi-items/multi-renderer.d.ts +4 -2
  19. package/dist/perseus-api.d.ts +1 -1
  20. package/dist/renderer.d.ts +4 -9
  21. package/dist/server-item-renderer.d.ts +3 -1
  22. package/dist/types.d.ts +30 -15
  23. package/dist/widget-container.d.ts +2 -1
  24. package/dist/widgets/expression.d.ts +409 -12
  25. package/dist/widgets/graded-group.d.ts +2 -2
  26. package/dist/widgets/grapher.d.ts +2 -1
  27. package/dist/widgets/passage-ref.d.ts +2 -1
  28. package/dist/widgets/sequence.d.ts +2 -2
  29. package/package.json +3 -3
  30. package/src/__stories__/article-renderer.stories.tsx +35 -2
  31. package/src/__testdata__/article-renderer.testdata.ts +71 -0
  32. package/src/__tests__/item-renderer.test.tsx +12 -1
  33. package/src/__tests__/renderer.test.tsx +0 -2
  34. package/src/__tests__/server-item-renderer.test.tsx +11 -5
  35. package/src/article-renderer.tsx +38 -30
  36. package/src/components/info-tip.tsx +3 -4
  37. package/src/components/input-with-examples.tsx +1 -1
  38. package/src/components/sortable.tsx +1 -1
  39. package/src/dependencies.ts +16 -1
  40. package/src/hint-renderer.tsx +2 -1
  41. package/src/hints-renderer.tsx +2 -9
  42. package/src/index.ts +0 -1
  43. package/src/interaction-tracker.ts +3 -3
  44. package/src/item-renderer.tsx +57 -44
  45. package/src/multi-items/__tests__/multi-renderer.test.tsx +6 -1
  46. package/src/multi-items/multi-renderer.tsx +18 -11
  47. package/src/renderer.tsx +4 -4
  48. package/src/server-item-renderer.tsx +21 -17
  49. package/src/types.ts +50 -21
  50. package/src/widget-container.tsx +2 -1
  51. package/src/widgets/__stories__/definition.stories.tsx +8 -1
  52. package/src/widgets/__stories__/expression.stories.tsx +2 -2
  53. package/src/widgets/__tests__/{expression.test.ts → expression.test.tsx} +73 -8
  54. package/src/widgets/__tests__/renderQuestion.tsx +29 -29
  55. package/src/widgets/expression.tsx +77 -48
  56. package/src/widgets/graded-group.tsx +11 -2
  57. package/src/widgets/grapher.tsx +4 -12
  58. package/src/widgets/passage-ref.tsx +1 -1
  59. package/src/widgets/sequence.tsx +10 -2
  60. package/tsconfig-build.tsbuildinfo +1 -1
@@ -41,6 +41,7 @@ import {StyleSheet, css} from "aphrodite"; // eslint-disable-line import/no-extr
41
41
  import lens from "hubble"; // eslint-disable-line import/no-extraneous-dependencies
42
42
  import * as React from "react";
43
43
 
44
+ import {DependenciesContext} from "../dependencies";
44
45
  import HintsRenderer from "../hints-renderer";
45
46
  import {Errors, Log} from "../logging/log";
46
47
  import Renderer from "../renderer";
@@ -50,21 +51,23 @@ import {itemToTree} from "./items";
50
51
  import {buildMapper} from "./trees";
51
52
 
52
53
  import type {Widget} from "../renderer";
53
- import type {APIOptions, FilterCriterion, PerseusScore} from "../types";
54
+ import type {
55
+ APIOptions,
56
+ FilterCriterion,
57
+ PerseusDependenciesV2,
58
+ PerseusScore,
59
+ } from "../types";
54
60
  import type {Item, ContentNode, HintNode, TagsNode} from "./item-types";
55
61
  import type {Shape, ArrayShape} from "./shape-types";
56
62
  import type {Tree} from "./tree-types";
57
63
  import type {TreeMapper, ContentMapper, HintMapper, Path} from "./trees";
64
+ import type {PropsFor} from "@khanacademy/wonder-blocks-core";
58
65
 
59
66
  type Hint = any; // TODO(mdr)
60
67
  type Score = any; // TODO(mdr)
61
68
  type SerializedState = any; // TODO(mdr)
62
69
 
63
- type RendererProps = JSX.LibraryManagedAttributes<
64
- typeof Renderer,
65
- // @ts-expect-error - TS2344 - Type 'typeof Renderer' does not satisfy the constraint 'keyof IntrinsicElements | JSXElementConstructor<any>'.
66
- React.ComponentProps<typeof Renderer>
67
- >;
70
+ type RendererProps = PropsFor<typeof Renderer>;
68
71
 
69
72
  type ContentRendererElement = React.ReactElement<any>;
70
73
  type HintRendererElement = React.ReactElement<any>;
@@ -109,6 +112,8 @@ type Props = {
109
112
  onInteractWithWidget?: (id: string) => void;
110
113
  apiOptions?: APIOptions;
111
114
  reviewMode?: boolean | null | undefined;
115
+
116
+ dependencies: PerseusDependenciesV2;
112
117
  };
113
118
  type State = {
114
119
  // We cache functions to generate renderers and refs in `rendererDataTree`,
@@ -306,7 +311,6 @@ class MultiRenderer extends React.Component<Props, State> {
306
311
  makeRenderer: () => (
307
312
  <HintsRenderer
308
313
  {...this._getRendererProps()}
309
- // @ts-expect-error - TS2769 - No overload matches this call.
310
314
  findExternalWidgets={findExternalWidgets}
311
315
  hints={[hint]}
312
316
  />
@@ -506,7 +510,6 @@ class MultiRenderer extends React.Component<Props, State> {
506
510
  (renderers as any).firstN = (n: any) => (
507
511
  <HintsRenderer
508
512
  {...this._getRendererProps()}
509
- // @ts-expect-error - TS2769 - No overload matches this call.
510
513
  findExternalWidgets={
511
514
  hintRendererDatas[0]
512
515
  ? hintRendererDatas[0].findExternalWidgets
@@ -547,9 +550,13 @@ class MultiRenderer extends React.Component<Props, State> {
547
550
 
548
551
  // Pass the renderer tree to the `children` function, which will
549
552
  // determine the actual content of this component.
550
- return this.props.children({
551
- renderers: this._getRenderers(),
552
- });
553
+ return (
554
+ <DependenciesContext.Provider value={this.props.dependencies}>
555
+ {this.props.children({
556
+ renderers: this._getRenderers(),
557
+ })}
558
+ </DependenciesContext.Provider>
559
+ );
553
560
  }
554
561
  }
555
562
 
package/src/renderer.tsx CHANGED
@@ -35,11 +35,11 @@ import type {
35
35
  APIOptionsWithDefaults,
36
36
  FilterCriterion,
37
37
  FocusPath,
38
- LinterContextProps,
39
38
  PerseusScore,
40
39
  WidgetInfo,
41
40
  WidgetProps,
42
41
  } from "./types";
42
+ import type {LinterContextProps} from "@khanacademy/perseus-linter";
43
43
 
44
44
  import "./styles/perseus-renderer.less";
45
45
 
@@ -213,7 +213,7 @@ class Renderer extends React.Component<Props, State> {
213
213
  _foundTextNodes: boolean;
214
214
  // @ts-expect-error - TS2564 - Property '_interactionTrackers' has no initializer and is not definitely assigned in the constructor.
215
215
  _interactionTrackers: {
216
- [id: string]: InteractionTracker;
216
+ [id: string]: InteractionTracker<any>;
217
217
  };
218
218
  // @ts-expect-error - TS2564 - Property '_isMounted' has no initializer and is not definitely assigned in the constructor.
219
219
  _isMounted: boolean;
@@ -250,8 +250,8 @@ class Renderer extends React.Component<Props, State> {
250
250
  linterContext: PerseusLinter.linterContextDefault,
251
251
  };
252
252
 
253
- constructor(props: Props, context: Context) {
254
- super(props, context);
253
+ constructor(props: Props) {
254
+ super(props);
255
255
  this._translationLinter = new TranslationLinter();
256
256
 
257
257
  this.state = {
@@ -12,6 +12,7 @@ import * as React from "react";
12
12
  import _ from "underscore";
13
13
 
14
14
  import AssetContext from "./asset-context";
15
+ import {DependenciesContext} from "./dependencies";
15
16
  import HintsRenderer from "./hints-renderer";
16
17
  import Objective from "./interactive2/objective_";
17
18
  import LoadingContext from "./loading-context";
@@ -20,7 +21,7 @@ import Renderer from "./renderer";
20
21
  import Util from "./util";
21
22
 
22
23
  import type {KeypadProps} from "./mixins/provide-keypad";
23
- import type {APIOptions, FocusPath} from "./types";
24
+ import type {APIOptions, FocusPath, PerseusDependenciesV2} from "./types";
24
25
  import type {RendererInterface, KEScore} from "@khanacademy/perseus-core";
25
26
 
26
27
  const {mapObject} = Objective;
@@ -37,6 +38,8 @@ type OwnProps = // These props are used by the ProvideKeypad mixin.
37
38
  reviewMode?: boolean;
38
39
  // from KeypadContext
39
40
  keypadElement?: any | null | undefined;
41
+
42
+ dependencies: PerseusDependenciesV2;
40
43
  };
41
44
 
42
45
  type HOCProps = {
@@ -418,29 +421,30 @@ export class ServerItemRenderer
418
421
  <HintsRenderer
419
422
  hints={this.props.item.hints}
420
423
  hintsVisible={this.props.hintsVisible}
421
- // @ts-expect-error - TS2769 - No overload matches this call.
422
424
  apiOptions={apiOptions}
423
425
  ref={(elem) => (this.hintsRenderer = elem)}
424
426
  />
425
427
  );
426
428
 
427
429
  return (
428
- <div>
429
- <div>{questionRenderer}</div>
430
- <div
431
- className={
432
- // Avoid adding any horizontal padding when applying the
433
- // mobile hint styles, which are flush to the left.
434
- // NOTE(charlie): We may still want to apply this
435
- // padding for desktop exercises.
436
- apiOptions.isMobile
437
- ? undefined
438
- : css(styles.hintsContainer)
439
- }
440
- >
441
- {hintsRenderer}
430
+ <DependenciesContext.Provider value={this.props.dependencies}>
431
+ <div>
432
+ <div>{questionRenderer}</div>
433
+ <div
434
+ className={
435
+ // Avoid adding any horizontal padding when applying the
436
+ // mobile hint styles, which are flush to the left.
437
+ // NOTE(charlie): We may still want to apply this
438
+ // padding for desktop exercises.
439
+ apiOptions.isMobile
440
+ ? undefined
441
+ : css(styles.hintsContainer)
442
+ }
443
+ >
444
+ {hintsRenderer}
445
+ </div>
442
446
  </div>
443
- </div>
447
+ </DependenciesContext.Provider>
444
448
  );
445
449
  }
446
450
  }
package/src/types.ts CHANGED
@@ -4,12 +4,16 @@ import type {Item} from "./multi-items/item-types";
4
4
  import type {PerseusWidget} from "./perseus-types";
5
5
  import type {SizeClass} from "./util/sizing-utils";
6
6
  import type {KeypadAPI} from "@khanacademy/math-input";
7
- import type {SendEventFn} from "@khanacademy/perseus-core";
7
+ import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
8
+ import type {LinterContextProps} from "@khanacademy/perseus-linter";
8
9
  import type {Result} from "@khanacademy/wonder-blocks-data";
9
10
  import type * as React from "react";
10
11
 
11
12
  export type FocusPath = ReadonlyArray<string> | null | undefined;
12
13
 
14
+ // TODO(FEI-5054): Figure out how to get global .d.ts files working with monorepos
15
+ type Empty = Record<never, never>;
16
+
13
17
  export type Dimensions = {
14
18
  width?: number;
15
19
  height?: number;
@@ -109,6 +113,20 @@ export type Path = ReadonlyArray<string>;
109
113
 
110
114
  type StubTagEditorType = any; // from "./components/stub-tag-editor";
111
115
 
116
+ type TrackInteractionArgs = {
117
+ // The widget type that this interaction originates from
118
+ type: string;
119
+ // The widget id that this interaction originates from
120
+ id: string;
121
+
122
+ correct?: boolean;
123
+
124
+ // Tracking args are all optional here because we don't know which
125
+ // widgets originated the call, and thus can't know what extra
126
+ // arguments will be included!
127
+ } & Partial<TrackingGradedGroupExtraArguments> &
128
+ Partial<TrackingSequenceExtraArguments>;
129
+
112
130
  // APIOptions provides different ways to customize the behaviour of Perseus.
113
131
  export type APIOptions = Readonly<{
114
132
  isArticle?: boolean;
@@ -162,15 +180,7 @@ export type APIOptions = Readonly<{
162
180
  // A function that is called when the user has interacted with a widget. It
163
181
  // also includes any extra parameters that the originating widget provided.
164
182
  // This is used for keeping track of widget interactions.
165
- trackInteraction?: (
166
- args: {
167
- // The widget type that this interaction originates from
168
- type: string;
169
- // The widget id that this interaction originates from
170
- id: string;
171
- correct?: boolean;
172
- } & Record<string, unknown>,
173
- ) => void;
183
+ trackInteraction?: (args: TrackInteractionArgs) => void;
174
184
  // A boolean that indicates whether or not a custom keypad is
175
185
  // being used. For mobile web this will be the ProvidedKeypad
176
186
  // component. In this situation we use the MathInput component
@@ -310,7 +320,6 @@ export type PerseusDependencies = {
310
320
  //misc
311
321
  staticUrl: StaticUrlFn;
312
322
  InitialRequestUrl: InitialRequestUrlInterface;
313
- analytics: SendEventFn;
314
323
 
315
324
  // video widget
316
325
  // This is used as a hook to fetch data about a video which is used to
@@ -332,6 +341,18 @@ export type PerseusDependencies = {
332
341
  isMobile: boolean;
333
342
  };
334
343
 
344
+ /**
345
+ * The modern iteration of Perseus Depedndencies. These dependencies are
346
+ * provided to Perseus through its entrypoints (for example:
347
+ * ServerItemRenderer) and then attached to the DependenciesContext so they are
348
+ * available anywhere down the React render tree.
349
+ *
350
+ * Prefer using this type over `PerseusDependencies` when possible.
351
+ */
352
+ export type PerseusDependenciesV2 = {
353
+ analytics: {onAnalyticsEvent: AnalyticsEventHandlerFn};
354
+ };
355
+
335
356
  export type APIOptionsWithDefaults = Readonly<
336
357
  APIOptions & {
337
358
  GroupMetadataEditor: NonNullable<APIOptions["GroupMetadataEditor"]>;
@@ -355,20 +376,22 @@ export type APIOptionsWithDefaults = Readonly<
355
376
  }
356
377
  >;
357
378
 
358
- export type LinterContextProps = {
359
- contentType: string;
360
- highlightLint: boolean;
361
- paths: ReadonlyArray<string>;
362
- stack: ReadonlyArray<string>;
363
- // additional properties can be added to the context by widgets
364
- };
365
-
366
379
  export type Tracking =
367
380
  // Track interactions once
368
381
  | ""
369
382
  // Track all interactions
370
383
  | "all";
371
384
 
385
+ // See graded-group widget
386
+ export type TrackingGradedGroupExtraArguments = {
387
+ status: "correct" | "incorrect" | "invalid";
388
+ };
389
+
390
+ // See sequence widget
391
+ export type TrackingSequenceExtraArguments = {
392
+ visible: number;
393
+ };
394
+
372
395
  export type Alignment =
373
396
  | "default"
374
397
  | "block"
@@ -443,7 +466,13 @@ export type FilterCriterion =
443
466
  ) => boolean);
444
467
 
445
468
  // NOTE: Rubric should always be the corresponding widget options type for the component.
446
- export type WidgetProps<RenderProps, Rubric> = RenderProps & {
469
+ export type WidgetProps<
470
+ RenderProps,
471
+ Rubric,
472
+ // Defines the arguments that can be passed to the `trackInteraction`
473
+ // function from APIOptions for this widget.
474
+ TrackingExtraArgs = Empty,
475
+ > = RenderProps & {
447
476
  // provided by renderer.jsx#getWidgetProps()
448
477
  widgetId: string;
449
478
  alignment: string | null | undefined;
@@ -463,7 +492,7 @@ export type WidgetProps<RenderProps, Rubric> = RenderProps & {
463
492
  // APIOptions. This provides the widget an easy way to notify the renderer
464
493
  // of an interaction. The Renderer then enriches the data provided with the
465
494
  // widget's id and type before calling APIOptions.trackInteraction.
466
- trackInteraction: (extraData?: any) => void;
495
+ trackInteraction: (extraData?: TrackingExtraArgs) => void;
467
496
  isLastUsedWidget: boolean;
468
497
  // provided by widget-container.jsx#render()
469
498
  linterContext: LinterContextProps;
@@ -9,7 +9,8 @@ import {containerSizeClass, getClassFromWidth} from "./util/sizing-utils";
9
9
  import * as Widgets from "./widgets";
10
10
 
11
11
  import type {PerseusWidgetOptions} from "./perseus-types";
12
- import type {LinterContextProps, WidgetProps} from "./types";
12
+ import type {WidgetProps} from "./types";
13
+ import type {LinterContextProps} from "@khanacademy/perseus-linter";
13
14
 
14
15
  type Props = {
15
16
  shouldHighlight: boolean;
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
 
3
3
  import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";
4
+ import {storybookDependenciesV2} from "../../../../../testing/test-dependencies";
4
5
  import ArticleRenderer from "../../article-renderer";
5
6
 
6
7
  export default {
@@ -103,5 +104,11 @@ export const MultipleDefinitions = (args: StoryArgs): React.ReactElement => {
103
104
  };
104
105
 
105
106
  export const ArticleDefintion = (args: StoryArgs): React.ReactElement => {
106
- return <ArticleRenderer json={article} useNewStyles />;
107
+ return (
108
+ <ArticleRenderer
109
+ json={article}
110
+ useNewStyles
111
+ dependencies={storybookDependenciesV2}
112
+ />
113
+ );
107
114
  };
@@ -1,4 +1,4 @@
1
- import {KeypadContext} from "@khanacademy/math-input";
1
+ import {KeypadContext, KeypadType} from "@khanacademy/math-input";
2
2
  import * as React from "react";
3
3
 
4
4
  import {ItemRendererWithDebugUI} from "../../../../../testing/item-renderer-with-debug-ui";
@@ -62,7 +62,7 @@ export const DesktopKitchenSink = (args: StoryArgs): React.ReactElement => {
62
62
  };
63
63
 
64
64
  const keypadConfiguration = {
65
- keypadType: "EXPRESSION",
65
+ keypadType: KeypadType.EXPRESSION,
66
66
  extraKeys: ["x", "y", "z"],
67
67
  };
68
68
 
@@ -3,7 +3,10 @@ import {screen, fireEvent} from "@testing-library/react";
3
3
  import userEvent from "@testing-library/user-event";
4
4
  import "@testing-library/jest-dom";
5
5
 
6
- import {testDependencies} from "../../../../../testing/test-dependencies";
6
+ import {
7
+ testDependencies,
8
+ testDependenciesV2,
9
+ } from "../../../../../testing/test-dependencies";
7
10
  import * as Dependencies from "../../dependencies";
8
11
  import {
9
12
  expressionItem2,
@@ -15,8 +18,13 @@ import {Expression} from "../expression";
15
18
  import {renderQuestion} from "./renderQuestion";
16
19
 
17
20
  import type {PerseusItem} from "../../perseus-types";
21
+ import type {APIOptions} from "../../types";
18
22
 
19
- const assertComplete = (itemData: PerseusItem, input, isCorrect: boolean) => {
23
+ const assertComplete = (
24
+ itemData: PerseusItem,
25
+ input: string,
26
+ isCorrect: boolean,
27
+ ) => {
20
28
  const {renderer} = renderQuestion(itemData.question);
21
29
  userEvent.type(screen.getByRole("textbox"), input);
22
30
  const [_, score] = renderer.guessAndScore();
@@ -27,13 +35,14 @@ const assertComplete = (itemData: PerseusItem, input, isCorrect: boolean) => {
27
35
  });
28
36
  };
29
37
 
30
- const assertCorrect = (itemData: PerseusItem, input) => {
38
+ const assertCorrect = (itemData: PerseusItem, input: string) => {
31
39
  assertComplete(itemData, input, true);
32
40
 
33
- expect(testDependencies.analytics).toHaveBeenCalledWith({
41
+ expect(testDependenciesV2.analytics.onAnalyticsEvent).toHaveBeenCalledWith({
34
42
  type: "perseus:expression-evaluated",
35
43
  payload: {
36
44
  result: "correct",
45
+ virtualKeypadVersion: "PERSEUS_MATH_INPUT",
37
46
  },
38
47
  });
39
48
  };
@@ -41,16 +50,21 @@ const assertCorrect = (itemData: PerseusItem, input) => {
41
50
  const assertIncorrect = (itemData: PerseusItem, input: string) => {
42
51
  assertComplete(itemData, input, false);
43
52
 
44
- expect(testDependencies.analytics).toHaveBeenCalledWith({
53
+ expect(testDependenciesV2.analytics.onAnalyticsEvent).toHaveBeenCalledWith({
45
54
  type: "perseus:expression-evaluated",
46
55
  payload: {
47
56
  result: "incorrect",
57
+ virtualKeypadVersion: "PERSEUS_MATH_INPUT",
48
58
  },
49
59
  });
50
60
  };
51
61
 
52
62
  // TODO: actually Assert that message is being set on the score object.
53
- const assertInvalid = (itemData: PerseusItem, input, message?: string) => {
63
+ const assertInvalid = (
64
+ itemData: PerseusItem,
65
+ input: string,
66
+ message?: string,
67
+ ) => {
54
68
  const {renderer} = renderQuestion(itemData.question);
55
69
  if (input.length) {
56
70
  userEvent.type(screen.getByRole("textbox"), input);
@@ -58,17 +72,18 @@ const assertInvalid = (itemData: PerseusItem, input, message?: string) => {
58
72
  const [_, score] = renderer.guessAndScore();
59
73
  expect(score).toMatchObject({type: "invalid"});
60
74
 
61
- expect(testDependencies.analytics).toHaveBeenCalledWith({
75
+ expect(testDependenciesV2.analytics.onAnalyticsEvent).toHaveBeenCalledWith({
62
76
  type: "perseus:expression-evaluated",
63
77
  payload: {
64
78
  result: "invalid",
79
+ virtualKeypadVersion: "PERSEUS_MATH_INPUT",
65
80
  },
66
81
  });
67
82
  };
68
83
 
69
84
  describe("Expression Widget", function () {
70
85
  beforeEach(() => {
71
- jest.spyOn(testDependencies, "analytics");
86
+ jest.spyOn(testDependenciesV2.analytics, "onAnalyticsEvent");
72
87
  jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
73
88
  testDependencies,
74
89
  );
@@ -171,6 +186,56 @@ describe("Expression Widget", function () {
171
186
  expect(err).toStrictEqual({message: null, type: "invalid"});
172
187
  });
173
188
  });
189
+
190
+ describe("analytics", () => {
191
+ const assertKeypadVersion = (
192
+ apiOptions: APIOptions,
193
+ virtualKeypadVersion: string,
194
+ ) => {
195
+ const {renderer} = renderQuestion(
196
+ expressionItem2.question,
197
+ apiOptions,
198
+ );
199
+
200
+ renderer.guessAndScore();
201
+
202
+ expect(
203
+ testDependenciesV2.analytics.onAnalyticsEvent,
204
+ ).toHaveBeenCalledWith({
205
+ type: "perseus:expression-evaluated",
206
+ payload: {
207
+ // We're not interested in validating that the expression
208
+ // widget did anything useful or that the keypad worked. We
209
+ // just want to make sure the code that derives which
210
+ // keypad version it detected is correct.
211
+ result: "invalid",
212
+ virtualKeypadVersion,
213
+ },
214
+ });
215
+ };
216
+
217
+ it("should set the virtual keypad version to REACT_NATIVE_KEYPAD when nativeKeypadProxy is provided", () => {
218
+ assertKeypadVersion(
219
+ {nativeKeypadProxy: jest.fn()},
220
+ "REACT_NATIVE_KEYPAD",
221
+ );
222
+ });
223
+
224
+ it("should set the virtual keypad version to MATH_INPUT_KEYPAD_V1 when customKeypad is set and useV2Keypad is unset", () => {
225
+ assertKeypadVersion({customKeypad: true}, "MATH_INPUT_KEYPAD_V1");
226
+ });
227
+
228
+ it("should set the virtual keypad version to MATH_INPUT_KEYPAD_V2 when customKeypad is set and useV2Keypad is true", () => {
229
+ assertKeypadVersion(
230
+ {customKeypad: true, useV2Keypad: true},
231
+ "MATH_INPUT_KEYPAD_V2",
232
+ );
233
+ });
234
+
235
+ it("should default the virtual keypad version to PERSEUS_MATH_INPUT", () => {
236
+ assertKeypadVersion(Object.freeze({}), "PERSEUS_MATH_INPUT");
237
+ });
238
+ });
174
239
  });
175
240
 
176
241
  describe("getOneCorrectAnswerFromRubric", () => {
@@ -2,48 +2,47 @@ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
2
2
  import {render} from "@testing-library/react";
3
3
  import * as React from "react";
4
4
 
5
+ // eslint-disable-next-line import/no-relative-packages
6
+ import {testDependenciesV2} from "../../../../../testing/test-dependencies";
7
+ import {DependenciesContext} from "../../dependencies";
5
8
  import * as Perseus from "../../index";
6
9
  import {registerAllWidgetsForTesting} from "../../util/register-all-widgets-for-testing";
7
10
 
8
11
  import type {PerseusRenderer} from "../../perseus-types";
9
12
  import type {APIOptions} from "../../types";
13
+ import type {PropsFor} from "@khanacademy/wonder-blocks-core";
10
14
 
11
15
  type RenderResult = ReturnType<typeof render>;
12
16
 
13
17
  export const renderQuestion = (
14
18
  question: PerseusRenderer,
15
19
  apiOptions: APIOptions = Object.freeze({}),
16
- extraProps?: JSX.LibraryManagedAttributes<
17
- typeof Perseus.Renderer,
18
- // @ts-expect-error - TS2344 - Type 'typeof Renderer' does not satisfy the constraint 'keyof IntrinsicElements | JSXElementConstructor<any>'.
19
- React.ComponentProps<typeof Perseus.Renderer>
20
- >,
20
+ extraProps?: PropsFor<typeof Perseus.Renderer>,
21
21
  ): {
22
22
  container: HTMLElement;
23
23
  renderer: Perseus.Renderer;
24
24
  rerender: (
25
25
  question: PerseusRenderer,
26
- extraProps?: JSX.LibraryManagedAttributes<
27
- typeof Perseus.Renderer,
28
- // @ts-expect-error - TS2344 - Type 'typeof Renderer' does not satisfy the constraint 'keyof IntrinsicElements | JSXElementConstructor<any>'.
29
- React.ComponentProps<typeof Perseus.Renderer>
30
- >,
26
+ extraProps?: PropsFor<typeof Perseus.Renderer>,
31
27
  ) => void;
32
28
  unmount: RenderResult["unmount"];
33
29
  } => {
34
30
  registerAllWidgetsForTesting();
31
+
35
32
  let renderer: Perseus.Renderer | null = null;
36
33
  const {container, rerender, unmount} = render(
37
34
  <RenderStateRoot>
38
- <Perseus.Renderer
39
- ref={(node) => (renderer = node)}
40
- content={question.content}
41
- images={question.images}
42
- widgets={question.widgets}
43
- problemNum={0}
44
- apiOptions={apiOptions}
45
- {...extraProps}
46
- />
35
+ <DependenciesContext.Provider value={testDependenciesV2}>
36
+ <Perseus.Renderer
37
+ ref={(node) => (renderer = node)}
38
+ content={question.content}
39
+ images={question.images}
40
+ widgets={question.widgets}
41
+ problemNum={0}
42
+ apiOptions={apiOptions}
43
+ {...extraProps}
44
+ />
45
+ </DependenciesContext.Provider>
47
46
  </RenderStateRoot>,
48
47
  );
49
48
  if (!renderer) {
@@ -51,20 +50,21 @@ export const renderQuestion = (
51
50
  }
52
51
  const renderAgain = (
53
52
  question: PerseusRenderer,
54
- // @ts-expect-error - TS2344 - Type 'typeof Renderer' does not satisfy the constraint 'keyof IntrinsicElements | JSXElementConstructor<any>'.
55
53
  extraProps: undefined | React.ComponentProps<typeof Perseus.Renderer>,
56
54
  ) => {
57
55
  rerender(
58
56
  <RenderStateRoot>
59
- <Perseus.Renderer
60
- ref={(node) => (renderer = node)}
61
- content={question.content}
62
- images={question.images}
63
- widgets={question.widgets}
64
- problemNum={0}
65
- apiOptions={apiOptions}
66
- {...extraProps}
67
- />
57
+ <DependenciesContext.Provider value={testDependenciesV2}>
58
+ <Perseus.Renderer
59
+ ref={(node) => (renderer = node)}
60
+ content={question.content}
61
+ images={question.images}
62
+ widgets={question.widgets}
63
+ problemNum={0}
64
+ apiOptions={apiOptions}
65
+ {...extraProps}
66
+ />
67
+ </DependenciesContext.Provider>
68
68
  </RenderStateRoot>,
69
69
  );
70
70
  if (!renderer) {