@khanacademy/perseus 11.4.0 → 11.5.1

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.
@@ -1,9 +1,8 @@
1
- import { KeypadType } from "@khanacademy/math-input";
2
1
  import * as React from "react";
3
2
  import { useDependencies } from "../dependencies";
4
3
  import type { PerseusExpressionWidgetOptions } from "../perseus-types";
5
4
  import type { PerseusScore, WidgetProps } from "../types";
6
- import type { Keys as Key } from "@khanacademy/math-input";
5
+ import type { Keys as Key, KeypadConfiguration } from "@khanacademy/math-input";
7
6
  type InputPath = ReadonlyArray<string>;
8
7
  type Rubric = PerseusExpressionWidgetOptions;
9
8
  type RenderProps = {
@@ -79,10 +78,7 @@ export declare class Expression extends React.Component<Props, ExpressionState>
79
78
  * to be included as keys on the keypad. These are scraped from the answer
80
79
  * forms.
81
80
  */
82
- declare const keypadConfigurationForProps: (widgetOptions: PerseusExpressionWidgetOptions) => {
83
- keypadType: KeypadType;
84
- extraKeys: string[];
85
- };
81
+ declare const keypadConfigurationForProps: (widgetOptions: PerseusExpressionWidgetOptions) => KeypadConfiguration;
86
82
  declare const _default: Readonly<{
87
83
  name: string;
88
84
  displayName: string;
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": "11.4.0",
6
+ "version": "11.5.1",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.1",
26
26
  "@khanacademy/kmath": "^0.1.2",
27
- "@khanacademy/math-input": "^12.1.1",
27
+ "@khanacademy/math-input": "^13.1.0",
28
28
  "@khanacademy/perseus-core": "1.1.0",
29
29
  "@khanacademy/perseus-linter": "^0.3.4",
30
30
  "@khanacademy/pure-markdown": "^0.2.6",
@@ -57,6 +57,7 @@
57
57
  "jquery": "^2.1.1",
58
58
  "katex": "^0.11.1",
59
59
  "lodash.debounce": "^4.0.8",
60
+ "mathquill": "git+https://git@github.com/Khan/mathquill.git#32d9f351aaa68537170b3120a52e99b8def3a2c3",
60
61
  "perseus-build-settings": "^0.2.0",
61
62
  "prop-types": "^15.6.1",
62
63
  "react": "16.14.0",
@@ -11,6 +11,7 @@ import expressionExport from "../expression";
11
11
  import TestKeypadContextWrapper from "./test-keypad-context-wrapper";
12
12
 
13
13
  import type {PerseusItem} from "../../perseus-types";
14
+ import type {Keys as Key} from "@khanacademy/math-input";
14
15
 
15
16
  type StoryArgs = {
16
17
  customKeypad: boolean;
@@ -34,8 +35,11 @@ const WrappedKeypadContext = (props: WrappedKeypadContextProps) => {
34
35
  return (
35
36
  <ItemRendererWithDebugUI
36
37
  item={props.item}
38
+ // Hardcoding the V2 Keypad to true as the Storybook Args
39
+ // were not working.
37
40
  apiOptions={{
38
41
  customKeypad: props.customKeypad,
42
+ useV2Keypad: true,
39
43
  }}
40
44
  />
41
45
  );
@@ -63,7 +67,7 @@ export const DesktopKitchenSink = (args: StoryArgs): React.ReactElement => {
63
67
 
64
68
  const keypadConfiguration = {
65
69
  keypadType: KeypadType.EXPRESSION,
66
- extraKeys: ["x", "y", "z"],
70
+ extraKeys: ["x", "y", "z"] as Key[],
67
71
  };
68
72
 
69
73
  return (
@@ -0,0 +1,346 @@
1
+ import "@testing-library/jest-dom";
2
+ import {KeypadContext, MobileKeypad} from "@khanacademy/math-input";
3
+ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
4
+ import {
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ within,
10
+ } from "@testing-library/react";
11
+ import userEvent from "@testing-library/user-event";
12
+ import MathQuill from "mathquill";
13
+ import * as React from "react";
14
+
15
+ import {
16
+ testDependencies,
17
+ testDependenciesV2,
18
+ } from "../../../../../testing/test-dependencies";
19
+ import * as Dependencies from "../../dependencies";
20
+ import WrappedServerItemRenderer from "../../server-item-renderer";
21
+ import {registerWidget} from "../../widgets";
22
+ import {expressionItem2} from "../__testdata__/expression.testdata";
23
+ import ExpressionExport from "../expression";
24
+
25
+ import type {KeypadAPI} from "@khanacademy/math-input";
26
+
27
+ const MQ = MathQuill.getInterface(2);
28
+
29
+ function RendererWithContext({item}) {
30
+ return (
31
+ <KeypadContext.Consumer>
32
+ {({keypadElement}) => {
33
+ return (
34
+ <WrappedServerItemRenderer
35
+ apiOptions={{
36
+ isMobile: true,
37
+ customKeypad: true,
38
+ useV2Keypad: true,
39
+ }}
40
+ item={item}
41
+ problemNum={0}
42
+ reviewMode={false}
43
+ dependencies={testDependenciesV2}
44
+ keypadElement={keypadElement}
45
+ />
46
+ );
47
+ }}
48
+ </KeypadContext.Consumer>
49
+ );
50
+ }
51
+
52
+ function KeypadWithContext() {
53
+ return (
54
+ <KeypadContext.Consumer>
55
+ {({setKeypadElement}) => {
56
+ return (
57
+ <MobileKeypad
58
+ onElementMounted={setKeypadElement}
59
+ onDismiss={() => {}}
60
+ onAnalyticsEvent={async () => {}}
61
+ useV2Keypad
62
+ />
63
+ );
64
+ }}
65
+ </KeypadContext.Consumer>
66
+ );
67
+ }
68
+
69
+ function ConnectedRenderer({item = expressionItem2}) {
70
+ const [keypadElement, setKeypadElement] =
71
+ React.useState<KeypadAPI | null>();
72
+ const [renderer, setRenderer] = React.useState<any>(null);
73
+ const [scrollableElement, setScrollableElement] =
74
+ React.useState<HTMLElement | null>();
75
+
76
+ return (
77
+ <RenderStateRoot>
78
+ <KeypadContext.Provider
79
+ value={{
80
+ setKeypadElement,
81
+ keypadElement,
82
+ setRenderer,
83
+ renderer,
84
+ setScrollableElement,
85
+ scrollableElement,
86
+ }}
87
+ >
88
+ <RendererWithContext item={item} />
89
+ <KeypadWithContext />
90
+ </KeypadContext.Provider>
91
+ </RenderStateRoot>
92
+ );
93
+ }
94
+
95
+ describe("expression mobile", () => {
96
+ beforeAll(() => {
97
+ registerWidget("expression", ExpressionExport);
98
+ });
99
+
100
+ beforeEach(() => {
101
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
102
+ testDependencies,
103
+ );
104
+ });
105
+
106
+ afterEach(() => {
107
+ // The Renderer uses a timer to wait for widgets to complete rendering.
108
+ // If we don't spin the timers here, then the timer fires in the test
109
+ // _after_ and breaks it because we do setState() in the callback,
110
+ // and by that point the component has been unmounted.
111
+ jest.runOnlyPendingTimers();
112
+ });
113
+
114
+ it("renders input", () => {
115
+ render(<ConnectedRenderer />);
116
+
117
+ expect(
118
+ screen.getByLabelText(
119
+ "Math input box Tap with one or two fingers to open keyboard",
120
+ ),
121
+ ).toBeInTheDocument();
122
+ });
123
+
124
+ it("shows the keypad after input interaction", async () => {
125
+ render(<ConnectedRenderer />);
126
+
127
+ const input = screen.getByLabelText(
128
+ "Math input box Tap with one or two fingers to open keyboard",
129
+ );
130
+
131
+ fireEvent.touchStart(input);
132
+
133
+ // waiting because `visibility` is animated
134
+ await waitFor(() =>
135
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
136
+ );
137
+ });
138
+
139
+ it("is possible to use the keypad", async () => {
140
+ render(<ConnectedRenderer />);
141
+
142
+ const input = screen.getByLabelText(
143
+ "Math input box Tap with one or two fingers to open keyboard",
144
+ );
145
+
146
+ fireEvent.touchStart(input);
147
+
148
+ // waiting because `visibility` is animated
149
+ await waitFor(() =>
150
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
151
+ );
152
+
153
+ userEvent.click(screen.getByRole("button", {name: "1"}));
154
+
155
+ // MathQuill is problematic,
156
+ // this is the only way I know how to test the "input"
157
+ const mathquillInput =
158
+ // eslint-disable-next-line testing-library/no-node-access
159
+ document.getElementsByClassName("mq-root-block")[0] as HTMLElement;
160
+ const span1 = within(mathquillInput).getByText("1");
161
+
162
+ expect(span1).toBeVisible();
163
+ });
164
+
165
+ it("is possible to type many numbers", async () => {
166
+ render(<ConnectedRenderer />);
167
+
168
+ const input = screen.getByLabelText(
169
+ "Math input box Tap with one or two fingers to open keyboard",
170
+ );
171
+
172
+ fireEvent.touchStart(input);
173
+
174
+ // waiting because `visibility` is animated
175
+ await waitFor(() =>
176
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
177
+ );
178
+
179
+ const testNumbers = [8, 6, 7, 5, 3, 0, 9];
180
+ testNumbers.forEach((num) => {
181
+ userEvent.click(screen.getByRole("button", {name: `${num}`}));
182
+ });
183
+
184
+ // MathQuill is problematic,
185
+ // this is the only way I know how to test the "input"
186
+ const mathquillInput =
187
+ // eslint-disable-next-line testing-library/no-node-access
188
+ document.getElementsByClassName("mq-root-block")[0] as HTMLElement;
189
+
190
+ testNumbers.forEach((num) => {
191
+ const span = within(mathquillInput).getByText(`${num}`);
192
+
193
+ expect(span).toBeVisible();
194
+ });
195
+ });
196
+
197
+ it("can handle symbols", async () => {
198
+ render(<ConnectedRenderer />);
199
+
200
+ const input = screen.getByLabelText(
201
+ "Math input box Tap with one or two fingers to open keyboard",
202
+ );
203
+
204
+ fireEvent.touchStart(input);
205
+
206
+ // waiting because `visibility` is animated
207
+ await waitFor(() =>
208
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
209
+ );
210
+
211
+ userEvent.click(screen.getByRole("button", {name: "4"}));
212
+ userEvent.click(screen.getByRole("button", {name: "2"}));
213
+ userEvent.click(screen.getByRole("button", {name: "Percent"}));
214
+
215
+ // MathQuill is problematic,
216
+ // this is the only way I know how to test the "input"
217
+ const mathquillInput =
218
+ // eslint-disable-next-line testing-library/no-node-access
219
+ document.getElementsByClassName("mq-root-block")[0] as HTMLElement;
220
+ const span4 = within(mathquillInput).getByText("4");
221
+ const span2 = within(mathquillInput).getByText("2");
222
+ const spanPercent = within(mathquillInput).getByText("%");
223
+
224
+ expect(span4).toBeVisible();
225
+ expect(span2).toBeVisible();
226
+ expect(spanPercent).toBeVisible();
227
+ });
228
+
229
+ it("can do arithmetic", async () => {
230
+ render(<ConnectedRenderer />);
231
+
232
+ const input = screen.getByLabelText(
233
+ "Math input box Tap with one or two fingers to open keyboard",
234
+ );
235
+
236
+ fireEvent.touchStart(input);
237
+
238
+ // waiting because `visibility` is animated
239
+ await waitFor(() =>
240
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
241
+ );
242
+
243
+ userEvent.click(screen.getByRole("button", {name: "4"}));
244
+ userEvent.click(screen.getByRole("button", {name: "Plus"}));
245
+ userEvent.click(screen.getByRole("button", {name: "2"}));
246
+
247
+ // MathQuill is problematic,
248
+ // this is the only way I know how to test the "input"
249
+ const mathquillInput =
250
+ // eslint-disable-next-line testing-library/no-node-access
251
+ document.getElementsByClassName("mq-root-block")[0] as HTMLElement;
252
+ const span4 = within(mathquillInput).getByText("4");
253
+ const spanPlus = within(mathquillInput).getByText("+");
254
+ const span2 = within(mathquillInput).getByText("2");
255
+
256
+ expect(span4).toBeVisible();
257
+ expect(spanPlus).toBeVisible();
258
+ expect(span2).toBeVisible();
259
+ });
260
+
261
+ it("can navigate to operators page", async () => {
262
+ render(<ConnectedRenderer />);
263
+
264
+ const input = screen.getByLabelText(
265
+ "Math input box Tap with one or two fingers to open keyboard",
266
+ );
267
+
268
+ fireEvent.touchStart(input);
269
+
270
+ // waiting because `visibility` is animated
271
+ await waitFor(() =>
272
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
273
+ );
274
+
275
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
276
+ expect(screen.getByRole("button", {name: "Equals sign"})).toBeVisible();
277
+ });
278
+
279
+ it("can navigate to geometry page", async () => {
280
+ render(<ConnectedRenderer />);
281
+
282
+ const input = screen.getByLabelText(
283
+ "Math input box Tap with one or two fingers to open keyboard",
284
+ );
285
+
286
+ fireEvent.touchStart(input);
287
+
288
+ // waiting because `visibility` is animated
289
+ await waitFor(() =>
290
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
291
+ );
292
+
293
+ userEvent.click(screen.getByRole("tab", {name: "Geometry"}));
294
+ expect(screen.getByRole("button", {name: "Sine"})).toBeVisible();
295
+ });
296
+
297
+ it("can navigate to extras page", async () => {
298
+ render(<ConnectedRenderer />);
299
+
300
+ const input = screen.getByLabelText(
301
+ "Math input box Tap with one or two fingers to open keyboard",
302
+ );
303
+
304
+ fireEvent.touchStart(input);
305
+
306
+ // waiting because `visibility` is animated
307
+ await waitFor(() =>
308
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
309
+ );
310
+
311
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
312
+ expect(screen.getByRole("button", {name: "x"})).toBeVisible();
313
+ });
314
+
315
+ it("can write a full equation", async () => {
316
+ render(<ConnectedRenderer />);
317
+
318
+ const input = screen.getByLabelText(
319
+ "Math input box Tap with one or two fingers to open keyboard",
320
+ );
321
+
322
+ fireEvent.touchStart(input);
323
+
324
+ // waiting because `visibility` is animated
325
+ await waitFor(() =>
326
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
327
+ );
328
+
329
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
330
+ userEvent.click(screen.getByRole("button", {name: "x"}));
331
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
332
+ userEvent.click(screen.getByRole("button", {name: "Equals sign"}));
333
+ userEvent.click(screen.getByRole("tab", {name: "Geometry"}));
334
+ userEvent.click(screen.getByRole("button", {name: "Sine"}));
335
+ userEvent.click(screen.getByRole("tab", {name: "Numbers"}));
336
+ userEvent.click(screen.getByRole("button", {name: "9"}));
337
+
338
+ // MathQuill is problematic,
339
+ // this is how to get the value of the input directly from MQ
340
+ const mathquillInstance =
341
+ // eslint-disable-next-line testing-library/no-node-access
342
+ MQ(document.getElementsByClassName("mq-editable-field")[0]);
343
+
344
+ expect(mathquillInstance.latex()).toBe("x=\\sin\\left(9\\right)");
345
+ });
346
+ });
@@ -29,7 +29,7 @@ import type {
29
29
  WidgetExports,
30
30
  WidgetProps,
31
31
  } from "../types";
32
- import type {Keys as Key} from "@khanacademy/math-input";
32
+ import type {Keys as Key, KeypadConfiguration} from "@khanacademy/math-input";
33
33
  import type {PropsFor} from "@khanacademy/wonder-blocks-core";
34
34
 
35
35
  type InputPath = ReadonlyArray<string>;
@@ -610,7 +610,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
610
610
  */
611
611
  const keypadConfigurationForProps = (
612
612
  widgetOptions: PerseusExpressionWidgetOptions,
613
- ) => {
613
+ ): KeypadConfiguration => {
614
614
  // Always use the Expression keypad, regardless of the button sets that have
615
615
  // been enabled.
616
616
  const keypadType = KeypadType.EXPRESSION;
@@ -642,20 +642,22 @@ const keypadConfigurationForProps = (
642
642
 
643
643
  // TODO(charlie): Alert the keypad as to which of these symbols should be
644
644
  // treated as functions.
645
- const extraVariables = Object.keys(uniqueExtraVariables);
646
- extraVariables.sort();
645
+ const extraVariables = Object.keys(
646
+ uniqueExtraVariables,
647
+ ).sort() as ReadonlyArray<Key>;
647
648
 
648
- const extraConstants = Object.keys(uniqueExtraConstants);
649
- extraConstants.sort();
649
+ const extraConstants = Object.keys(
650
+ uniqueExtraConstants,
651
+ ).sort() as ReadonlyArray<Key>;
650
652
 
651
- const extraKeys = [...extraVariables, ...extraConstants];
653
+ let extraKeys = [...extraVariables, ...extraConstants];
652
654
  if (!extraKeys.length) {
653
655
  // If there are no extra symbols available, we include Pi anyway, so
654
656
  // that the "extra symbols" button doesn't appear empty.
655
- extraKeys.push("PI");
657
+ extraKeys = ["PI"];
656
658
  }
657
659
 
658
- return {keypadType, extraKeys};
660
+ return {keypadType, extraKeys, times: widgetOptions.times};
659
661
  };
660
662
 
661
663
  const propUpgrades = {