@khanacademy/perseus 13.1.0 → 14.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 (43) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/components/svg-image.d.ts +10 -9
  3. package/dist/es/index.css +0 -7
  4. package/dist/es/index.js +37 -31
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +0 -7
  7. package/dist/index.js +61 -49
  8. package/dist/index.js.map +1 -1
  9. package/dist/renderer.d.ts +2 -2
  10. package/dist/widgets/matcher.d.ts +1 -1
  11. package/package.json +35 -35
  12. package/src/__stories__/hints-renderer.stories.tsx +41 -44
  13. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +9 -33
  14. package/src/__tests__/article-renderer.test.tsx +2 -3
  15. package/src/__tests__/renderer-api.test.tsx +35 -52
  16. package/src/components/graph.tsx +14 -8
  17. package/src/components/svg-image.tsx +13 -11
  18. package/src/mixins/provide-keypad.tsx +0 -1
  19. package/src/perseus-types.ts +8 -3
  20. package/src/renderer.tsx +2 -2
  21. package/src/styles/khan-exercise.css +0 -2
  22. package/src/styles/perseus-renderer.less +0 -7
  23. package/src/widgets/__stories__/random-widgets.stories.tsx +59 -0
  24. package/src/widgets/__stories__/test-keypad-context-wrapper.tsx +0 -1
  25. package/src/widgets/__testdata__/categorizer.testdata.ts +40 -0
  26. package/src/widgets/__testdata__/dropdown.testdata.ts +38 -0
  27. package/src/widgets/__testdata__/explanation.testdata.ts +30 -0
  28. package/src/widgets/__testdata__/expression.testdata.ts +82 -0
  29. package/src/widgets/__testdata__/interactive-graph-random.testdata.ts +269 -0
  30. package/src/widgets/__testdata__/radio.testdata.ts +63 -0
  31. package/src/widgets/__testdata__/randomizers.ts +51 -0
  32. package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +6 -22
  33. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt.test.ts.snap +3 -3
  34. package/src/widgets/__tests__/__snapshots__/graded-group-set.test.ts.snap +1 -1
  35. package/src/widgets/__tests__/__snapshots__/graded-group.test.ts.snap +1 -1
  36. package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +4 -4
  37. package/src/widgets/__tests__/expression-mobile.test.tsx +0 -1
  38. package/src/widgets/__tests__/matcher.test.tsx +1 -1
  39. package/src/widgets/__tests__/randomizers.test.ts +53 -0
  40. package/src/widgets/image.tsx +27 -20
  41. package/src/widgets/matcher.tsx +1 -1
  42. package/src/widgets/radio/choice.tsx +1 -0
  43. package/tsconfig-build.tsbuildinfo +1 -1
@@ -33,7 +33,7 @@ export type Widget = {
33
33
  blurInputPath?: (path: FocusPath) => null;
34
34
  focusInputPath?: (path: FocusPath) => null;
35
35
  getInputPaths?: () => ReadonlyArray<FocusPath>;
36
- setInputValue?: (path: FocusPath, newValue: string, focus: () => unknown) => void;
36
+ setInputValue?: (path: FocusPath, newValue: string, focus?: () => unknown) => void;
37
37
  getUserInput?: () => WidgetUserInput | null | undefined;
38
38
  simpleValidate?: (options?: any, onOutputError?: (widgetId: any, value: string, message?: string | null | undefined) => unknown | null | undefined) => PerseusScore;
39
39
  showRationalesForCurrentlySelectedChoices?: (options?: any) => void;
@@ -210,7 +210,7 @@ declare class Renderer extends React.Component<Props, State> {
210
210
  serialize: () => Record<any, any>;
211
211
  emptyWidgets: () => any;
212
212
  _setWidgetProps: SetWidgetPropsFn;
213
- setInputValue: (path: FocusPath, newValue: string, focus: () => unknown) => void;
213
+ setInputValue: (path: FocusPath, newValue: string, focus?: () => unknown) => void;
214
214
  /**
215
215
  * Returns an array of the widget `.getUserInput()` results
216
216
  */
@@ -6,7 +6,7 @@ type State = {
6
6
  rightHeight: number;
7
7
  texRendererLoaded: boolean;
8
8
  };
9
- declare class Matcher extends React.Component<any, any> {
9
+ export declare class Matcher extends React.Component<any, any> {
10
10
  static propTypes: {
11
11
  apiOptions: PropTypes.Validator<NonNullable<PropTypes.InferProps<{
12
12
  isArticle: PropTypes.Validator<boolean>;
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": "13.1.0",
6
+ "version": "14.0.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,29 +24,29 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.5",
26
26
  "@khanacademy/kmath": "^0.1.6",
27
- "@khanacademy/math-input": "^15.0.1",
27
+ "@khanacademy/math-input": "^16.0.0",
28
28
  "@khanacademy/perseus-core": "1.3.0",
29
29
  "@khanacademy/perseus-linter": "^0.3.8",
30
- "@khanacademy/pure-markdown": "^0.2.10",
31
- "@khanacademy/simple-markdown": "^0.10.4"
30
+ "@khanacademy/pure-markdown": "^0.2.11",
31
+ "@khanacademy/simple-markdown": "^0.11.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@khanacademy/wonder-blocks-button": "^4.1.3",
35
- "@khanacademy/wonder-blocks-clickable": "^4.0.0",
36
- "@khanacademy/wonder-blocks-color": "^2.0.1",
37
- "@khanacademy/wonder-blocks-core": "^6.0.0",
38
- "@khanacademy/wonder-blocks-data": "^12.0.0",
39
- "@khanacademy/wonder-blocks-dropdown": "^3.0.21",
40
- "@khanacademy/wonder-blocks-form": "^4.3.3",
34
+ "@khanacademy/wonder-blocks-button": "^6.2.2",
35
+ "@khanacademy/wonder-blocks-clickable": "^4.0.11",
36
+ "@khanacademy/wonder-blocks-color": "^3.0.0",
37
+ "@khanacademy/wonder-blocks-core": "^6.3.0",
38
+ "@khanacademy/wonder-blocks-data": "^13.0.5",
39
+ "@khanacademy/wonder-blocks-dropdown": "^3.1.8",
40
+ "@khanacademy/wonder-blocks-form": "^4.3.16",
41
41
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
42
- "@khanacademy/wonder-blocks-layout": "^2.0.17",
43
- "@khanacademy/wonder-blocks-link": "^5.0.0",
44
- "@khanacademy/wonder-blocks-modal": "^4.0.19",
45
- "@khanacademy/wonder-blocks-popover": "^3.0.0",
46
- "@khanacademy/wonder-blocks-progress-spinner": "^2.0.17",
47
- "@khanacademy/wonder-blocks-spacing": "^3.0.5",
48
- "@khanacademy/wonder-blocks-tooltip": "^2.1.2",
49
- "@khanacademy/wonder-blocks-typography": "^2.1.3",
42
+ "@khanacademy/wonder-blocks-layout": "^2.0.24",
43
+ "@khanacademy/wonder-blocks-link": "^6.0.2",
44
+ "@khanacademy/wonder-blocks-modal": "^4.0.35",
45
+ "@khanacademy/wonder-blocks-popover": "^3.0.16",
46
+ "@khanacademy/wonder-blocks-progress-spinner": "^2.0.24",
47
+ "@khanacademy/wonder-blocks-spacing": "^4.0.1",
48
+ "@khanacademy/wonder-blocks-tooltip": "^2.1.18",
49
+ "@khanacademy/wonder-blocks-typography": "^2.1.9",
50
50
  "@khanacademy/wonder-stuff-core": "^1.5.1",
51
51
  "@phosphor-icons/core": "^2.0.2",
52
52
  "aphrodite": "^1.2.5",
@@ -59,29 +59,29 @@
59
59
  "katex": "^0.11.1",
60
60
  "lodash.debounce": "^4.0.8",
61
61
  "mathquill": "git+https://git@github.com/Khan/mathquill.git#48410e80d760bbd5105544d4a4ab459a28dc2cbc",
62
- "perseus-build-settings": "^0.2.1",
62
+ "perseus-build-settings": "^0.3.0",
63
63
  "prop-types": "^15.6.1",
64
64
  "react": "16.14.0",
65
65
  "react-dom": "16.14.0",
66
66
  "underscore": "^1.4.4"
67
67
  },
68
68
  "peerDependencies": {
69
- "@khanacademy/wonder-blocks-button": "^4.1.3",
70
- "@khanacademy/wonder-blocks-clickable": "^4.0.0",
71
- "@khanacademy/wonder-blocks-color": "^2.0.1",
72
- "@khanacademy/wonder-blocks-core": "^6.0.0",
73
- "@khanacademy/wonder-blocks-data": "^12.0.0",
74
- "@khanacademy/wonder-blocks-dropdown": "^3.0.21",
75
- "@khanacademy/wonder-blocks-form": "^4.3.3",
69
+ "@khanacademy/wonder-blocks-button": "^6.2.2",
70
+ "@khanacademy/wonder-blocks-clickable": "^4.0.11",
71
+ "@khanacademy/wonder-blocks-color": "^3.0.0",
72
+ "@khanacademy/wonder-blocks-core": "^6.3.0",
73
+ "@khanacademy/wonder-blocks-data": "^13.0.5",
74
+ "@khanacademy/wonder-blocks-dropdown": "^3.1.8",
75
+ "@khanacademy/wonder-blocks-form": "^4.3.16",
76
76
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
77
- "@khanacademy/wonder-blocks-layout": "^2.0.17",
78
- "@khanacademy/wonder-blocks-link": "^5.0.0",
79
- "@khanacademy/wonder-blocks-modal": "^4.0.19",
80
- "@khanacademy/wonder-blocks-popover": "^3.0.0",
81
- "@khanacademy/wonder-blocks-progress-spinner": "^2.0.17",
82
- "@khanacademy/wonder-blocks-spacing": "^3.0.5",
83
- "@khanacademy/wonder-blocks-tooltip": "^2.1.2",
84
- "@khanacademy/wonder-blocks-typography": "^2.1.3",
77
+ "@khanacademy/wonder-blocks-layout": "^2.0.24",
78
+ "@khanacademy/wonder-blocks-link": "^6.0.2",
79
+ "@khanacademy/wonder-blocks-modal": "^4.0.35",
80
+ "@khanacademy/wonder-blocks-popover": "^3.0.16",
81
+ "@khanacademy/wonder-blocks-progress-spinner": "^2.0.24",
82
+ "@khanacademy/wonder-blocks-spacing": "^4.0.1",
83
+ "@khanacademy/wonder-blocks-tooltip": "^2.1.18",
84
+ "@khanacademy/wonder-blocks-typography": "^2.1.9",
85
85
  "@khanacademy/wonder-stuff-core": "^1.5.1",
86
86
  "@phosphor-icons/core": "^2.0.2",
87
87
  "aphrodite": "^1.2.5",
@@ -3,56 +3,53 @@ import React from "react";
3
3
 
4
4
  import HintsRenderer from "../hints-renderer";
5
5
 
6
- type StoryArgs = {
7
- hints: ReadonlyArray<any>;
8
- hintsVisible: number;
9
- };
10
-
11
- type Story = {
12
- title: string;
13
- argTypes: any;
14
- };
6
+ import type {Meta, StoryObj} from "@storybook/react";
15
7
 
16
- export default {
8
+ const meta: Meta<typeof HintsRenderer> = {
17
9
  title: "Perseus/Renderers/Hints Renderer",
18
- argTypes: {
19
- hints: {
20
- control: "object",
21
- defaultValue: [
22
- {
23
- content: "this is hint 1",
24
- images: {},
25
- replace: false,
26
- widgets: {},
27
- },
28
- {
29
- content: "this is hint 2",
30
- images: {},
31
- replace: false,
32
- widgets: {},
33
- },
34
- {
35
- content: "this is hint 3",
36
- images: {},
37
- replace: false,
38
- widgets: {},
39
- },
40
- ],
10
+ component: HintsRenderer,
11
+ decorators: [
12
+ (Story) => {
13
+ return (
14
+ <View style={{left: 80}}>
15
+ <Story />
16
+ </View>
17
+ );
41
18
  },
19
+ ],
20
+ argTypes: {
42
21
  hintsVisible: {
43
- control: {type: "number", min: 0},
22
+ control: {min: 0},
44
23
  defaultValue: 3,
45
24
  },
46
25
  },
47
- } as Story;
26
+ };
27
+
28
+ export default meta;
48
29
 
49
- export const Interactive = (args: StoryArgs): any => {
50
- return (
51
- // Sorry for the hacks! The HintRenderer uses absolute positioning
52
- // for the "1 / 3" label that is rendered left of the hint. So we shift
53
- // everything over so we can see it.
54
- <View style={{left: 80}}>
55
- <HintsRenderer {...args} />
56
- </View>
57
- );
30
+ type Story = StoryObj<typeof HintsRenderer>;
31
+
32
+ export const Interactive: Story = {
33
+ args: {
34
+ hints: [
35
+ {
36
+ content: "this is hint 1",
37
+ images: {},
38
+ replace: false,
39
+ widgets: {},
40
+ },
41
+ {
42
+ content: "this is hint 2",
43
+ images: {},
44
+ replace: false,
45
+ widgets: {},
46
+ },
47
+ {
48
+ content: "this is hint 3",
49
+ images: {},
50
+ replace: false,
51
+ widgets: {},
52
+ },
53
+ ],
54
+ },
58
55
  };
@@ -367,18 +367,10 @@ exports[`renderer snapshots correct answer: correct answer 1`] = `
367
367
  >
368
368
  less than or equal to
369
369
  </span>
370
- <svg
370
+ <span
371
371
  aria-hidden="true"
372
- class="svg_aeiopc-o_O-caret_ntkmq"
373
- height="16"
374
- viewBox="0 0 16 16"
375
- width="16"
376
- >
377
- <path
378
- d="M8 8.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 1.414-1.414L8 8.586z"
379
- fill="rgba(33,36,44,0.64)"
380
- />
381
- </svg>
372
+ class="svg_aeiopc-o_O-caret_ntkmq-o_O-inlineStyles_13xtep5"
373
+ />
382
374
  </button>
383
375
  </div>
384
376
  </div>
@@ -440,18 +432,10 @@ exports[`renderer snapshots incorrect answer: incorrect answer 1`] = `
440
432
  >
441
433
  greater than or equal to
442
434
  </span>
443
- <svg
435
+ <span
444
436
  aria-hidden="true"
445
- class="svg_aeiopc-o_O-caret_ntkmq"
446
- height="16"
447
- viewBox="0 0 16 16"
448
- width="16"
449
- >
450
- <path
451
- d="M8 8.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 1.414-1.414L8 8.586z"
452
- fill="rgba(33,36,44,0.64)"
453
- />
454
- </svg>
437
+ class="svg_aeiopc-o_O-caret_ntkmq-o_O-inlineStyles_13xtep5"
438
+ />
455
439
  </button>
456
440
  </div>
457
441
  </div>
@@ -513,18 +497,10 @@ exports[`renderer snapshots initial render: initial render 1`] = `
513
497
  >
514
498
  greater/less than or equal to
515
499
  </span>
516
- <svg
500
+ <span
517
501
  aria-hidden="true"
518
- class="svg_aeiopc-o_O-caret_ntkmq"
519
- height="16"
520
- viewBox="0 0 16 16"
521
- width="16"
522
- >
523
- <path
524
- d="M8 8.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 1.414-1.414L8 8.586z"
525
- fill="rgba(33,36,44,0.64)"
526
- />
527
- </svg>
502
+ class="svg_aeiopc-o_O-caret_ntkmq-o_O-inlineStyles_13xtep5"
503
+ />
528
504
  </button>
529
505
  </div>
530
506
  </div>
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  StatefulKeypadContextProvider,
3
3
  KeypadContext,
4
+ MobileKeypad,
4
5
  } from "@khanacademy/math-input";
5
6
  import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
6
7
  import {screen, render, fireEvent, waitFor} from "@testing-library/react";
@@ -11,7 +12,6 @@ import {
11
12
  testDependencies,
12
13
  testDependenciesV2,
13
14
  } from "../../../../testing/test-dependencies";
14
- import KeypadSwitch from "../../../math-input/src/components/keypad-switch";
15
15
  import {articleWithExpression} from "../__testdata__/article-renderer.testdata";
16
16
  import ArticleRenderer from "../article-renderer";
17
17
  import * as Dependencies from "../dependencies";
@@ -24,11 +24,10 @@ function KeypadWithContext() {
24
24
  <KeypadContext.Consumer>
25
25
  {({setKeypadElement}) => {
26
26
  return (
27
- <KeypadSwitch
27
+ <MobileKeypad
28
28
  onElementMounted={setKeypadElement}
29
29
  onDismiss={() => {}}
30
30
  onAnalyticsEvent={async () => {}}
31
- useV2Keypad
32
31
  />
33
32
  );
34
33
  }}
@@ -1,26 +1,22 @@
1
1
  import {describe, beforeEach, it} from "@jest/globals";
2
- import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
3
- import {mount} from "enzyme"; // eslint-disable-line no-restricted-imports
4
2
  import $ from "jquery";
5
3
  import * as React from "react";
6
4
  import ReactDOM from "react-dom";
7
5
  import TestUtils from "react-dom/test-utils";
8
6
  import _ from "underscore";
9
- import "jest-enzyme";
10
7
 
11
8
  import {testDependencies} from "../../../../testing/test-dependencies";
12
9
  import * as Dependencies from "../dependencies";
13
10
  import {ClassNames} from "../perseus-api";
14
11
  import Renderer from "../renderer";
15
12
  import {registerAllWidgetsForTesting} from "../util/register-all-widgets-for-testing";
13
+ import {renderQuestion} from "../widgets/__tests__/renderQuestion";
16
14
 
17
15
  import imageItem from "./test-items/image-item";
18
16
  import inputNumber1Item from "./test-items/input-number-1-item";
19
17
  import inputNumber2Item from "./test-items/input-number-2-item";
20
18
  import tableItem from "./test-items/table-item";
21
19
 
22
- import type {APIOptions} from "../types";
23
-
24
20
  const itemWidget = inputNumber1Item;
25
21
 
26
22
  // Returns a promise that will resolve shortly after the end of this
@@ -33,26 +29,6 @@ const delayedPromise = (value: undefined) => {
33
29
  return deferred.promise();
34
30
  };
35
31
 
36
- const renderQuestionArea = function (item, apiOptions?: APIOptions): any {
37
- const wrapper = mount(
38
- <RenderStateRoot>
39
- <Dependencies.DependenciesContext.Provider
40
- value={{analytics: {onAnalyticsEvent: async () => undefined}}}
41
- >
42
- <Renderer
43
- content={item.question.content}
44
- images={item.question.images}
45
- widgets={item.question.widgets}
46
- problemNum={0}
47
- apiOptions={apiOptions}
48
- />
49
- </Dependencies.DependenciesContext.Provider>
50
- </RenderStateRoot>,
51
- {includeDefaultTestHarness: false},
52
- );
53
- return wrapper.childAt(0).instance();
54
- };
55
-
56
32
  describe("Perseus API", function () {
57
33
  beforeEach(() => {
58
34
  jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
@@ -63,34 +39,40 @@ describe("Perseus API", function () {
63
39
 
64
40
  describe("setInputValue", function () {
65
41
  it("should be able to produce a correctly graded value", function () {
66
- const renderer = renderQuestionArea(inputNumber1Item);
67
- renderer.setInputValue(["input-number 1"], "5");
68
- const score = renderer.guessAndScore()[1];
69
- expect(score.type).toBe("points");
70
- expect(score.earned).toBe(score.total);
42
+ // Arrange
43
+ const {renderer} = renderQuestion(inputNumber1Item.question);
44
+
45
+ // Act
46
+ renderer.setInputValue(["input-number 1"], "5", () => undefined);
47
+
48
+ // Assert
49
+ expect(renderer).toHaveBeenAnsweredCorrectly();
71
50
  });
72
51
 
73
52
  it("should be able to produce a wrong value", function () {
74
- const renderer = renderQuestionArea(inputNumber1Item);
75
- renderer.setInputValue(["input-number 1"], "3", () => {});
76
- const score = renderer.guessAndScore()[1];
77
- expect(score.type).toBe("points");
78
- expect(score.earned).toBe(0);
53
+ // Arrange
54
+ const {renderer} = renderQuestion(inputNumber1Item.question);
55
+
56
+ // Act
57
+ renderer.setInputValue(["input-number 1"], "3");
58
+
59
+ // Assert
60
+ expect(renderer).toHaveBeenAnsweredIncorrectly();
79
61
  });
80
62
 
81
63
  it("should be able to produce an empty score", function () {
82
- const renderer = renderQuestionArea(inputNumber1Item);
64
+ // Arrange
65
+ const {renderer} = renderQuestion(inputNumber1Item.question);
66
+
83
67
  renderer.setInputValue(["input-number 1"], "3");
84
- let score = renderer.guessAndScore()[1];
85
- expect(score.type).toBe("points");
86
- expect(score.earned).toBe(0);
68
+ expect(renderer).toHaveBeenAnsweredIncorrectly();
69
+
87
70
  renderer.setInputValue(["input-number 1"], "");
88
- score = renderer.guessAndScore()[1];
89
- expect(score.type).toBe("invalid");
71
+ expect(renderer).toHaveInvalidInput();
90
72
  });
91
73
 
92
74
  it("should be able to accept a callback", function (done) {
93
- const renderer = renderQuestionArea(inputNumber1Item);
75
+ const {renderer} = renderQuestion(inputNumber1Item.question);
94
76
  renderer.setInputValue(["input-number 1"], "3", function () {
95
77
  const guess = renderer.getUserInput()[0];
96
78
  expect(guess.currentValue).toBe("3");
@@ -102,13 +84,13 @@ describe("Perseus API", function () {
102
84
 
103
85
  describe("getInputPaths", function () {
104
86
  it("should be able to find all the input widgets", function () {
105
- const renderer = renderQuestionArea(inputNumber2Item);
87
+ const {renderer} = renderQuestion(inputNumber2Item.question);
106
88
  const numPaths = renderer.getInputPaths().length;
107
89
  expect(numPaths).toBe(2);
108
90
  });
109
91
 
110
92
  it("should be able to find all inputs within widgets", function () {
111
- const renderer = renderQuestionArea(tableItem);
93
+ const {renderer} = renderQuestion(tableItem.question);
112
94
  const numPaths = renderer.getInputPaths().length;
113
95
  expect(numPaths).toBe(8);
114
96
  });
@@ -116,7 +98,7 @@ describe("Perseus API", function () {
116
98
 
117
99
  describe("getDOMNodeForPath", function () {
118
100
  it("should find one DOM node per <input>", function () {
119
- const renderer = renderQuestionArea(inputNumber2Item);
101
+ const {renderer} = renderQuestion(inputNumber2Item.question);
120
102
  const inputPaths = renderer.getInputPaths();
121
103
  const allInputs = TestUtils.scryRenderedDOMComponentsWithTag(
122
104
  renderer,
@@ -126,13 +108,14 @@ describe("Perseus API", function () {
126
108
  });
127
109
 
128
110
  it("should find the right DOM nodes for the <input>s", function () {
129
- const renderer = renderQuestionArea(inputNumber2Item);
111
+ const {renderer} = renderQuestion(inputNumber2Item.question);
130
112
  const inputPaths = renderer.getInputPaths();
131
113
  const allInputs = TestUtils.scryRenderedDOMComponentsWithTag(
132
114
  renderer,
133
115
  "input",
134
116
  );
135
117
  _.each(inputPaths, (inputPath, i) => {
118
+ // @ts-expect-error - TS2769 - No overload matches this call.
136
119
  const $node = $(renderer.getDOMNodeForPath(inputPath));
137
120
  // @ts-expect-error - TS2769 - No overload matches this call.
138
121
  const $input = $(ReactDOM.findDOMNode(allInputs[i]));
@@ -145,13 +128,13 @@ describe("Perseus API", function () {
145
128
  describe("onInputError", function () {
146
129
  it("should call a callback when grading an empty input-number", function () {
147
130
  let wasCalled;
148
- const renderer = renderQuestionArea(inputNumber1Item, {
131
+ const {renderer} = renderQuestion(inputNumber1Item.question, {
149
132
  onInputError: function (widgetId) {
150
133
  wasCalled = true;
151
134
  },
152
135
  });
153
- const score = renderer.guessAndScore()[1];
154
- expect(score.type).toBe("invalid");
136
+
137
+ expect(renderer).toHaveInvalidInput();
155
138
  expect(wasCalled).toBe(true);
156
139
  });
157
140
  });
@@ -164,7 +147,7 @@ describe("Perseus API", function () {
164
147
  // version
165
148
  expect(ClassNames.FOCUSED).toBe("perseus-focused");
166
149
 
167
- const renderer = renderQuestionArea(inputNumber1Item);
150
+ const {renderer} = renderQuestion(inputNumber1Item.question);
168
151
 
169
152
  const input =
170
153
  // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'querySelector' does not exist on type 'Element | Text'.
@@ -191,7 +174,7 @@ describe("Perseus API", function () {
191
174
  let callCount = 0;
192
175
  let newFocusResult;
193
176
  let oldFocusResult;
194
- const renderer = renderQuestionArea(inputNumber1Item, {
177
+ const {renderer} = renderQuestion(inputNumber1Item.question, {
195
178
  onFocusChange: function (newFocus, oldFocus) {
196
179
  callCount++;
197
180
  newFocusResult = newFocus;
@@ -235,7 +218,7 @@ describe("Perseus API", function () {
235
218
  let callCount = 0;
236
219
  let newFocusResult;
237
220
  let oldFocusResult;
238
- const renderer = renderQuestionArea(inputNumber2Item, {
221
+ const {renderer} = renderQuestion(inputNumber2Item.question, {
239
222
  onFocusChange: function (newFocus, oldFocus) {
240
223
  callCount++;
241
224
  newFocusResult = newFocus;
@@ -5,6 +5,7 @@ import * as React from "react";
5
5
  import ReactDOM from "react-dom";
6
6
  import _ from "underscore";
7
7
 
8
+ import AssetContext from "../asset-context";
8
9
  import {interactiveSizes} from "../styles/constants";
9
10
  import Util from "../util";
10
11
  import GraphUtils from "../util/graph-utils";
@@ -395,14 +396,19 @@ class Graph extends React.Component<Props> {
395
396
  if (imageData.url) {
396
397
  const scale = this.props.box[0] / interactiveSizes.defaultBoxSize;
397
398
  image = (
398
- // @ts-expect-error - TS2741 - Property 'alt' is missing in type '{ src: any; width: any; height: any; scale: number; responsive: false; }' but required in type 'Pick<Readonly<Props> & Readonly<{ children?: ReactNode; }>, "children" | "height" | "width" | "title" | "alt" | "trackInteraction" | "preloader" | "allowFullBleed" | "extraGraphie" | "overrideAriaHidden">'.
399
- <SvgImage
400
- src={imageData.url}
401
- width={imageData.width}
402
- height={imageData.height}
403
- scale={scale}
404
- responsive={false}
405
- />
399
+ <AssetContext.Consumer>
400
+ {({setAssetStatus}) => (
401
+ <SvgImage
402
+ // @ts-expect-error - TS2741 - Property 'alt' is missing in type '{ src: any; width: any; height: any; scale: number; responsive: false; }' but required in type 'Pick<Readonly<Props> & Readonly<{ children?: ReactNode; }>, "children" | "height" | "width" | "title" | "alt" | "trackInteraction" | "preloader" | "allowFullBleed" | "extraGraphie" | "overrideAriaHidden">'.
403
+ src={imageData.url}
404
+ width={imageData.width}
405
+ height={imageData.height}
406
+ scale={scale}
407
+ responsive={false}
408
+ setAssetStatus={setAssetStatus}
409
+ />
410
+ )}
411
+ </AssetContext.Consumer>
406
412
  );
407
413
  } else {
408
414
  image = null;
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
2
- /* eslint-disable no-useless-escape, react/no-unsafe */
2
+ /* eslint-disable react/no-unsafe */
3
3
  import {CircularSpinner} from "@khanacademy/wonder-blocks-progress-spinner";
4
4
  import classNames from "classnames";
5
5
  import $ from "jquery";
@@ -79,7 +79,7 @@ const doJSONP = function (url: string, options) {
79
79
  // system (with file://). We replace urls that start with `web+graphie`
80
80
  // in the perseus json with this `file+graphie` prefix to indicate that
81
81
  // they should have the `file://` protocol instead of `https://`.
82
- const svgLocalLabelsRegex = /^file\+graphie\:/;
82
+ const svgLocalLabelsRegex = /^file\+graphie:/;
83
83
  const hashRegex = /\/([^/]+)$/;
84
84
 
85
85
  function isImageProbablyPhotograph(imageUrl) {
@@ -180,6 +180,16 @@ type Props = {
180
180
  setAssetStatus: (assetKey: string, loaded: boolean) => void;
181
181
  };
182
182
 
183
+ type DefaultProps = {
184
+ constrainHeight: NonNullable<Props["constrainHeight"]>;
185
+ onUpdate: NonNullable<Props["onUpdate"]>;
186
+ responsive: NonNullable<Props["responsive"]>;
187
+ scale: NonNullable<Props["scale"]>;
188
+ setAssetStatus: NonNullable<Props["setAssetStatus"]>;
189
+ src: NonNullable<Props["src"]>;
190
+ zoomToFullSizeOnMobile: NonNullable<Props["zoomToFullSizeOnMobile"]>;
191
+ };
192
+
183
193
  type Label = {
184
194
  coordinates: ReadonlyArray<any>;
185
195
  content: string;
@@ -212,15 +222,7 @@ type State = {
212
222
  class SvgImage extends React.Component<Props, State> {
213
223
  _isMounted: boolean;
214
224
 
215
- static defaultProps: {
216
- constrainHeight: boolean;
217
- onUpdate: () => void;
218
- responsive: boolean;
219
- scale: number;
220
- setAssetStatus: (src: string, status: boolean) => void;
221
- src: string;
222
- zoomToFullSizeOnMobile: boolean;
223
- } = {
225
+ static defaultProps: DefaultProps = {
224
226
  constrainHeight: false,
225
227
  onUpdate: () => {},
226
228
  responsive: true,
@@ -102,7 +102,6 @@ const ProvideKeypad = {
102
102
  }}
103
103
  // @ts-expect-error - TS2339 - Property 'props' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'.
104
104
  style={_this.props.keypadStyle}
105
- useV2Keypad={apiOptions.useV2Keypad}
106
105
  onAnalyticsEvent={async () => {
107
106
  // Intentionally left empty. This component is
108
107
  // deprecated and so we don't want/need to instrument