@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.
- package/CHANGELOG.md +33 -0
- package/dist/article-renderer.d.ts +2 -1
- package/dist/components/info-tip.d.ts +2 -1
- package/dist/components/input-with-examples.d.ts +1 -1
- package/dist/components/sortable.d.ts +1 -1
- package/dist/dependencies.d.ts +7 -1
- package/dist/es/index.css +3 -5
- package/dist/es/index.js +72 -45
- package/dist/es/index.js.map +1 -1
- package/dist/hint-renderer.d.ts +2 -1
- package/dist/hints-renderer.d.ts +2 -1
- package/dist/index.css +3 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +72 -46
- package/dist/index.js.map +1 -1
- package/dist/interaction-tracker.d.ts +3 -3
- package/dist/item-renderer.d.ts +3 -1
- package/dist/multi-items/multi-renderer.d.ts +4 -2
- package/dist/perseus-api.d.ts +1 -1
- package/dist/renderer.d.ts +4 -9
- package/dist/server-item-renderer.d.ts +3 -1
- package/dist/types.d.ts +30 -15
- package/dist/widget-container.d.ts +2 -1
- package/dist/widgets/expression.d.ts +409 -12
- package/dist/widgets/graded-group.d.ts +2 -2
- package/dist/widgets/grapher.d.ts +2 -1
- package/dist/widgets/passage-ref.d.ts +2 -1
- package/dist/widgets/sequence.d.ts +2 -2
- package/package.json +3 -3
- package/src/__stories__/article-renderer.stories.tsx +35 -2
- package/src/__testdata__/article-renderer.testdata.ts +71 -0
- package/src/__tests__/item-renderer.test.tsx +12 -1
- package/src/__tests__/renderer.test.tsx +0 -2
- package/src/__tests__/server-item-renderer.test.tsx +11 -5
- package/src/article-renderer.tsx +38 -30
- package/src/components/info-tip.tsx +3 -4
- package/src/components/input-with-examples.tsx +1 -1
- package/src/components/sortable.tsx +1 -1
- package/src/dependencies.ts +16 -1
- package/src/hint-renderer.tsx +2 -1
- package/src/hints-renderer.tsx +2 -9
- package/src/index.ts +0 -1
- package/src/interaction-tracker.ts +3 -3
- package/src/item-renderer.tsx +57 -44
- package/src/multi-items/__tests__/multi-renderer.test.tsx +6 -1
- package/src/multi-items/multi-renderer.tsx +18 -11
- package/src/renderer.tsx +4 -4
- package/src/server-item-renderer.tsx +21 -17
- package/src/types.ts +50 -21
- package/src/widget-container.tsx +2 -1
- package/src/widgets/__stories__/definition.stories.tsx +8 -1
- package/src/widgets/__stories__/expression.stories.tsx +2 -2
- package/src/widgets/__tests__/{expression.test.ts → expression.test.tsx} +73 -8
- package/src/widgets/__tests__/renderQuestion.tsx +29 -29
- package/src/widgets/expression.tsx +77 -48
- package/src/widgets/graded-group.tsx +11 -2
- package/src/widgets/grapher.tsx +4 -12
- package/src/widgets/passage-ref.tsx +1 -1
- package/src/widgets/sequence.tsx +10 -2
- 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 {
|
|
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 =
|
|
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
|
|
551
|
-
|
|
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
|
|
254
|
-
super(props
|
|
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
|
-
<
|
|
429
|
-
<div>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
</
|
|
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 {
|
|
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<
|
|
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?:
|
|
495
|
+
trackInteraction: (extraData?: TrackingExtraArgs) => void;
|
|
467
496
|
isLastUsedWidget: boolean;
|
|
468
497
|
// provided by widget-container.jsx#render()
|
|
469
498
|
linterContext: LinterContextProps;
|
package/src/widget-container.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
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 {
|
|
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 = (
|
|
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(
|
|
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(
|
|
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 = (
|
|
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(
|
|
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(
|
|
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?:
|
|
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?:
|
|
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
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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) {
|