@khanacademy/perseus 6.3.1 → 6.4.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.
@@ -28,7 +28,7 @@ declare const _default: {
28
28
  readonly bound: (range: any, snap: any, paddingPx: number) => (arg1: Coord, arg2: Coord) => Coord;
29
29
  };
30
30
  onMove: {
31
- readonly updatePoints: (coord: Coord, prevCoord: Coord) => undefined;
31
+ readonly updatePoints: (coord: Coord, prevCoord: Coord) => void;
32
32
  };
33
33
  onMoveEnd: {
34
34
  standard: null;
@@ -1,2 +1,2 @@
1
- declare const Movable: (graphie: any, options: any) => undefined;
1
+ declare const Movable: (graphie: any, options: any) => void;
2
2
  export default Movable;
@@ -140,11 +140,12 @@ export type PerseusExpressionWidgetOptions = {
140
140
  times: boolean;
141
141
  buttonsVisible?: "always" | "never" | "focused";
142
142
  };
143
+ export declare const PerseusExpressionAnswerFormConsidered: readonly ["correct", "wrong", "ungraded"];
143
144
  export type PerseusExpressionAnswerForm = {
144
145
  value: string;
145
146
  form: boolean;
146
147
  simplify: boolean;
147
- considered: string;
148
+ considered: typeof PerseusExpressionAnswerFormConsidered[number];
148
149
  key?: string;
149
150
  };
150
151
  export type PerseusGradedGroupWidgetOptions = {
@@ -258,11 +258,19 @@ export type PerseusExpressionWidgetOptions = {|
258
258
  times: boolean,
259
259
  buttonsVisible?: "always" | "never" | "focused",
260
260
  |};
261
+ declare export var PerseusExpressionAnswerFormConsidered: [
262
+ "correct",
263
+ "wrong",
264
+ "ungraded"
265
+ ];
261
266
  export type PerseusExpressionAnswerForm = {|
262
267
  value: string,
263
268
  form: boolean,
264
269
  simplify: boolean,
265
- considered: string,
270
+ considered: $ElementType<
271
+ typeof PerseusExpressionAnswerFormConsidered,
272
+ number
273
+ >,
266
274
  key?: string,
267
275
  |};
268
276
  export type PerseusGradedGroupWidgetOptions = {|
package/dist/types.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { ILogger } from "./logging/log";
4
4
  import type { Item } from "./multi-items/item-types";
5
5
  import type { PerseusWidget } from "./perseus-types";
6
6
  import type { SizeClass } from "./util/sizing-utils";
7
+ import type { SendEventFn } from "@khanacademy/perseus-core";
7
8
  import type { Result } from "@khanacademy/wonder-blocks-data";
8
9
  export type FocusPath = ReadonlyArray<string> | null | undefined;
9
10
  type State = any;
@@ -171,6 +172,7 @@ export type PerseusDependencies = {
171
172
  TeX: React.ComponentType<TeXProps>;
172
173
  staticUrl: StaticUrlFn;
173
174
  InitialRequestUrl: InitialRequestUrlInterface;
175
+ analytics: SendEventFn;
174
176
  useVideo(id: string, kind: VideoKind): Result<{
175
177
  video: VideoData | null | undefined;
176
178
  }>;
@@ -10,6 +10,7 @@ import type { ILogger } from "./logging/log";
10
10
  import type { Item } from "./multi-items/item-types";
11
11
  import type { PerseusWidget } from "./perseus-types";
12
12
  import type { SizeClass } from "./util/sizing-utils";
13
+ import type { SendEventFn } from "@khanacademy/perseus-core";
13
14
  import type { Result } from "@khanacademy/wonder-blocks-data";
14
15
  export type FocusPath = $ReadOnlyArray<string> | null | void;
15
16
  declare type State = any;
@@ -201,6 +202,7 @@ export type PerseusDependencies = {|
201
202
  TeX: React.ComponentType<TeXProps>,
202
203
  staticUrl: StaticUrlFn,
203
204
  InitialRequestUrl: InitialRequestUrlInterface,
205
+ analytics: SendEventFn,
204
206
  useVideo(
205
207
  id: string,
206
208
  kind: VideoKind
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core Perseus API (includes renderers and widgets)",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "6.3.1",
6
+ "version": "6.4.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -23,7 +23,8 @@
23
23
  "dependencies": {
24
24
  "@khanacademy/kas": "^0.3.1",
25
25
  "@khanacademy/kmath": "^0.1.1",
26
- "@khanacademy/math-input": "^4.3.0",
26
+ "@khanacademy/math-input": "^5.0.0",
27
+ "@khanacademy/perseus-core": "0.0.1",
27
28
  "@khanacademy/perseus-linter": "^0.3.1",
28
29
  "@khanacademy/pure-markdown": "^0.2.3",
29
30
  "@khanacademy/simple-markdown": "^0.9.3"
@@ -66,7 +66,6 @@ describe("renderer", () => {
66
66
  // _after_ and breaks it because we do setState() in the callback,
67
67
  // and by that point the component has been unmounted.
68
68
  jest.runOnlyPendingTimers();
69
- jest.clearAllMocks();
70
69
  });
71
70
 
72
71
  describe("snapshots", () => {
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export {default as KhanMath} from "./util/math";
65
65
  export {default as LoadingContext} from "./loading-context";
66
66
  export {default as mediaQueries} from "./styles/media-queries";
67
67
  export {default as PerseusMarkdown} from "./perseus-markdown";
68
+ export {PerseusExpressionAnswerFormConsidered} from "./perseus-types";
68
69
  export {traverse} from "./traversal";
69
70
  export {isItemRenderableByVersion} from "./renderability";
70
71
  export {violatingWidgets} from "./a11y";
@@ -7,7 +7,6 @@ import _ from "underscore";
7
7
  import Movable from "../movable";
8
8
 
9
9
  const createMock = function (): any {
10
- // @ts-expect-error [FEI-5003] - TS2350 - Only a void function can be called with the 'new' keyword.
11
10
  const movable = new Movable(null, {
12
11
  mouseTarget: null,
13
12
  });
@@ -363,8 +363,7 @@ const constraints = {
363
363
  constraints.standard = null;
364
364
 
365
365
  const onMove = {
366
- // @ts-expect-error [FEI-5003] - TS2355 - A function whose declared type is neither 'void' nor 'any' must return a value.
367
- updatePoints: function (coord: Coord, prevCoord: Coord): undefined {
366
+ updatePoints: function (coord: Coord, prevCoord: Coord): void {
368
367
  const actualDelta = kvector.subtract(coord, prevCoord);
369
368
  // @ts-expect-error [FEI-5003] - TS2339 - Property 'state' does not exist on type '{ readonly updatePoints: (coord: Coord, prevCoord: Coord) => undefined; }'.
370
369
  _.each(this.state.points, function (point) {
@@ -49,8 +49,7 @@ const DEFAULT_STATE = {
49
49
  mouseTarget: null,
50
50
  } as const;
51
51
 
52
- // @ts-expect-error [FEI-5003] - TS2355 - A function whose declared type is neither 'void' nor 'any' must return a value.
53
- const Movable = function (graphie: any, options: any): undefined {
52
+ const Movable = function (graphie: any, options: any): void {
54
53
  // @ts-expect-error [FEI-5003] - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
55
54
  _.extend(this, {
56
55
  graphie: graphie,
@@ -7,19 +7,16 @@ import KhanColors from "./util/colors";
7
7
  const Interactive2 = {
8
8
  MovablePoint: MovablePoint,
9
9
  addMovablePoint: function (graphie: any, options: any): any {
10
- // @ts-expect-error [FEI-5003] - TS2350 - Only a void function can be called with the 'new' keyword.
11
10
  const movable = new Movable(graphie, {});
12
11
  return new MovablePoint(graphie, movable, options);
13
12
  },
14
13
  MovableLine: MovableLine,
15
14
  addMovableLine: function (graphie: any, options: any): any {
16
- // @ts-expect-error [FEI-5003] - TS2350 - Only a void function can be called with the 'new' keyword.
17
15
  const movable = new Movable(graphie, {});
18
16
  return new MovableLine(graphie, movable, options);
19
17
  },
20
18
  MovablePolygon: MovablePolygon,
21
19
  addMovablePolygon: function (graphie: any, options: any): any {
22
- // @ts-expect-error [FEI-5003] - TS2350 - Only a void function can be called with the 'new' keyword.
23
20
  const movable = new Movable(graphie, {});
24
21
  return new MovablePolygon(graphie, movable, options);
25
22
  },
@@ -327,6 +327,12 @@ export type PerseusExpressionWidgetOptions = {
327
327
  buttonsVisible?: "always" | "never" | "focused";
328
328
  };
329
329
 
330
+ export const PerseusExpressionAnswerFormConsidered = [
331
+ "correct",
332
+ "wrong",
333
+ "ungraded",
334
+ ] as const;
335
+
330
336
  export type PerseusExpressionAnswerForm = {
331
337
  // The Katex form of the expression. e.g. "x\\cdot3=y"
332
338
  value: string;
@@ -335,7 +341,7 @@ export type PerseusExpressionAnswerForm = {
335
341
  // The answer expression must be fully expanded and simplified
336
342
  simplify: boolean;
337
343
  // Whether the form is considered "correct", "wrong", or "ungraded"
338
- considered: string;
344
+ considered: typeof PerseusExpressionAnswerFormConsidered[number];
339
345
  // A key to identify the answer form in a list
340
346
  // NOTE: perseus-format.js says this is required even though it isn't necessary.
341
347
  key?: string;
package/src/renderer.tsx CHANGED
@@ -1770,7 +1770,7 @@ class Renderer extends React.Component<Props, State> {
1770
1770
  return widgetIsGraded && !widgetIsStatic;
1771
1771
  });
1772
1772
 
1773
- const widgetScores: Record<string, any> = {};
1773
+ const widgetScores: Record<string, PerseusScore> = {};
1774
1774
  _.each(gradedWidgetIds, (id) => {
1775
1775
  const props = widgetProps[id];
1776
1776
  const widget = this.getWidgetInstance(id);
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@ import type {ILogger} from "./logging/log";
5
5
  import type {Item} from "./multi-items/item-types";
6
6
  import type {PerseusWidget} from "./perseus-types";
7
7
  import type {SizeClass} from "./util/sizing-utils";
8
+ import type {SendEventFn} from "@khanacademy/perseus-core";
8
9
  import type {Result} from "@khanacademy/wonder-blocks-data";
9
10
 
10
11
  export type FocusPath = ReadonlyArray<string> | null | undefined;
@@ -128,6 +129,7 @@ export type Path = ReadonlyArray<string>;
128
129
 
129
130
  type StubTagEditorType = any; // from "./components/stub-tag-editor";
130
131
 
132
+ // APIOptions provides different ways to customize the behaviour of Perseus.
131
133
  export type APIOptions = Readonly<{
132
134
  isArticle?: boolean;
133
135
  // This should actually be required since renderer.jsx sets defaults for
@@ -299,16 +301,26 @@ export type VideoKind = "YOUTUBE_ID" | "READABLE_ID";
299
301
  // An object for dependency injection, to allow different clients
300
302
  // to provide different methods for logging, translation, network
301
303
  // requests, etc.
304
+ //
305
+ // NOTE: You should avoid adding new dependencies here as this type was added
306
+ // as a quick fix to get around the fact that some of the dependencies Perseus
307
+ // needs are used in places where neither `APIOptions` nor a React Context
308
+ // could be used. Aim to shrink the footprint of PerseusDependencies and try to
309
+ // use alternative methods where possible.
302
310
  export type PerseusDependencies = {
303
311
  // JIPT
304
312
  JIPT: JIPT;
305
313
  graphieMovablesJiptLabels: JiptLabelStore;
306
314
  svgImageJiptLabels: JiptLabelStore;
307
315
  rendererTranslationComponents: JiptTranslationComponents;
316
+
308
317
  TeX: React.ComponentType<TeXProps>;
318
+
309
319
  //misc
310
320
  staticUrl: StaticUrlFn;
311
321
  InitialRequestUrl: InitialRequestUrlInterface;
322
+ analytics: SendEventFn;
323
+
312
324
  // video widget
313
325
  // This is used as a hook to fetch data about a video which is used to
314
326
  // add a link to the video transcript. The return value conforms to
@@ -320,7 +332,9 @@ export type PerseusDependencies = {
320
332
  ): Result<{
321
333
  video: VideoData | null | undefined;
322
334
  }>;
335
+
323
336
  Log: ILogger;
337
+
324
338
  // RequestInfo
325
339
  isDevServer: boolean;
326
340
  kaLocale: string;
@@ -71,7 +71,7 @@ export const expressionItem3Options: PerseusExpressionWidgetOptions = {
71
71
  value: "x+1",
72
72
  },
73
73
  {
74
- considered: "incorrect",
74
+ considered: "wrong",
75
75
  form: false,
76
76
  simplify: false,
77
77
  value: "y+1",
@@ -5,7 +5,7 @@ import "@testing-library/jest-dom";
5
5
 
6
6
  import {testDependencies} from "../../../../../testing/test-dependencies";
7
7
  import * as Dependencies from "../../dependencies";
8
- import {registerAllWidgetsForTesting} from "../../util/register-all-widgets-for-testing";
8
+ import {PerseusItem} from "../../perseus-types";
9
9
  import {
10
10
  expressionItem2,
11
11
  expressionItem3,
@@ -15,47 +15,73 @@ import {Expression} from "../expression";
15
15
 
16
16
  import {renderQuestion} from "./renderQuestion";
17
17
 
18
- type Item = any;
19
-
20
- const assertComplete = (itemData: Item, input, isCorrect: boolean) => {
18
+ const assertComplete = (itemData: PerseusItem, input, isCorrect: boolean) => {
21
19
  const {renderer} = renderQuestion(itemData.question);
22
20
  userEvent.type(screen.getByRole("textbox"), input);
23
21
  const [_, score] = renderer.guessAndScore();
22
+
24
23
  expect(score).toMatchObject({
25
24
  type: "points",
26
25
  earned: isCorrect ? 1 : 0,
27
26
  });
28
27
  };
29
28
 
30
- const assertCorrect = (itemData: Item, input) =>
29
+ const assertCorrect = (itemData: PerseusItem, input) => {
31
30
  assertComplete(itemData, input, true);
32
- const assertIncorrect = (itemData: Item, input: string) =>
31
+
32
+ expect(testDependencies.analytics).toHaveBeenCalledWith({
33
+ type: "perseus:expression-evaluated",
34
+ payload: {
35
+ virtualKeypadVersion: "PERSEUS_MATH_INPUT",
36
+ result: "correct",
37
+ },
38
+ });
39
+ };
40
+
41
+ const assertIncorrect = (itemData: PerseusItem, input: string) => {
33
42
  assertComplete(itemData, input, false);
34
43
 
44
+ expect(testDependencies.analytics).toHaveBeenCalledWith({
45
+ type: "perseus:expression-evaluated",
46
+ payload: {
47
+ virtualKeypadVersion: "PERSEUS_MATH_INPUT",
48
+ result: "incorrect",
49
+ },
50
+ });
51
+ };
52
+
35
53
  // TODO: actually Assert that message is being set on the score object.
36
- const assertInvalid = (itemData: Item, input, message?: string) => {
54
+ const assertInvalid = (itemData: PerseusItem, input, message?: string) => {
37
55
  const {renderer} = renderQuestion(itemData.question);
38
56
  if (input.length) {
39
57
  userEvent.type(screen.getByRole("textbox"), input);
40
58
  }
41
59
  const [_, score] = renderer.guessAndScore();
42
60
  expect(score).toMatchObject({type: "invalid"});
61
+
62
+ expect(testDependencies.analytics).toHaveBeenCalledWith({
63
+ type: "perseus:expression-evaluated",
64
+ payload: {
65
+ virtualKeypadVersion: "PERSEUS_MATH_INPUT",
66
+ result: "invalid",
67
+ },
68
+ });
43
69
  };
44
70
 
45
71
  describe("Expression Widget", function () {
46
72
  beforeEach(() => {
73
+ jest.spyOn(testDependencies, "analytics");
47
74
  jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
48
75
  testDependencies,
49
76
  );
50
- registerAllWidgetsForTesting();
51
77
  });
52
78
 
53
79
  describe("grading", function () {
54
- it("should not grade a thing that doesn't parse", function () {
80
+ it("should not grade a thing that doesn't parse", () => {
55
81
  assertInvalid(expressionItem2, "+++");
56
82
  });
57
83
 
58
- it("should not grade a thing that is empty", function () {
84
+ it("should not grade a thing that is empty", () => {
59
85
  assertInvalid(expressionItem2, "");
60
86
  });
61
87
  });
@@ -1,5 +1,4 @@
1
1
  import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
2
- // eslint-disable-next-line import/no-extraneous-dependencies
3
2
  import {render} from "@testing-library/react";
4
3
  import * as React from "react";
5
4
 
@@ -12,6 +12,7 @@ import Tooltip, {
12
12
  HorizontalDirection,
13
13
  VerticalDirection,
14
14
  } from "../components/tooltip";
15
+ import {getDependencies} from "../dependencies";
15
16
  import {iconExclamationSign} from "../icon-paths";
16
17
  import {Errors as PerseusErrors, Log} from "../logging/log";
17
18
  import * as Changeable from "../mixins/changeable";
@@ -24,6 +25,18 @@ import type {
24
25
  } from "../perseus-types";
25
26
  import type {PerseusScore, WidgetExports, WidgetProps} from "../types";
26
27
 
28
+ const sendExpressionEvaluatedEvent = (
29
+ result: "correct" | "incorrect" | "invalid",
30
+ ) => {
31
+ getDependencies().analytics({
32
+ type: "perseus:expression-evaluated",
33
+ payload: {
34
+ virtualKeypadVersion: "PERSEUS_MATH_INPUT", // STOPSHIP: Or do we push that onto host application?
35
+ result,
36
+ },
37
+ });
38
+ };
39
+
27
40
  type InputPath = ReadonlyArray<string>;
28
41
 
29
42
  const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
@@ -198,6 +211,8 @@ export class Expression extends React.Component<Props, ExpressionState> {
198
211
  // we did, whether it's considered correct, incorrect, or ungraded
199
212
  if (!matchingAnswerForm) {
200
213
  if (firstUngradedResult) {
214
+ sendExpressionEvaluatedEvent("invalid");
215
+
201
216
  // While we didn't directly match with any answer form, we
202
217
  // did at some point get an "ungraded" validation result,
203
218
  // which might indicate e.g. a mismatch in variable casing.
@@ -212,6 +227,8 @@ export class Expression extends React.Component<Props, ExpressionState> {
212
227
  };
213
228
  }
214
229
  if (allEmpty) {
230
+ sendExpressionEvaluatedEvent("invalid");
231
+
215
232
  // If everything graded as empty, it's invalid.
216
233
  return {
217
234
  type: "invalid",
@@ -220,6 +237,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
220
237
  }
221
238
  // We fell through all the possibilities and we're not empty,
222
239
  // so the answer is considered incorrect.
240
+ sendExpressionEvaluatedEvent("incorrect");
223
241
  return {
224
242
  type: "points",
225
243
  earned: 0,
@@ -234,6 +252,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
234
252
  userInput,
235
253
  matchMessage,
236
254
  );
255
+ sendExpressionEvaluatedEvent("invalid");
237
256
  return {
238
257
  type: "invalid",
239
258
  message: apiResult === false ? null : matchMessage,
@@ -245,6 +264,12 @@ export class Expression extends React.Component<Props, ExpressionState> {
245
264
  // TODO(eater): Seems silly to translate result to this
246
265
  // invalid/points thing and immediately translate it back in
247
266
  // ItemRenderer.scoreInput()
267
+ sendExpressionEvaluatedEvent(
268
+ matchingAnswerForm.considered === "correct"
269
+ ? "correct"
270
+ : "incorrect",
271
+ );
272
+
248
273
  return {
249
274
  type: "points",
250
275
  earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
@@ -503,16 +503,14 @@ class Matrix extends React.Component<Props, State> {
503
503
  value,
504
504
  cb,
505
505
  ) => {
506
- const answers = _.map(this.props.answers, _.clone);
506
+ const answers = this.props.answers.map((answer) => [...answer]);
507
507
  if (!answers[row]) {
508
508
  answers[row] = [];
509
509
  }
510
- // @ts-expect-error [FEI-5003] - TS2571 - Object is of type 'unknown'.
511
510
  answers[row][column] = value;
512
- // @ts-expect-error [FEI-5003] - TS2769 - No overload matches this call.
513
511
  this.props.onChange(
514
512
  {
515
- answers: answers,
513
+ answers,
516
514
  },
517
515
  cb,
518
516
  );
@@ -17,6 +17,7 @@
17
17
  {"path": "../kas/tsconfig-build.json"},
18
18
  {"path": "../kmath/tsconfig-build.json"},
19
19
  {"path": "../math-input/tsconfig-build.json"},
20
+ {"path": "../perseus-core/tsconfig-build.json"},
20
21
  {"path": "../perseus-linter/tsconfig-build.json"},
21
22
  {"path": "../pure-markdown/tsconfig-build.json"},
22
23
  {"path": "../simple-markdown/tsconfig-build.json"},