@khanacademy/perseus-editor 2.12.0 → 2.13.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.
@@ -11,13 +11,9 @@ type DefaultProps = {
11
11
  buttonSets: Props["buttonSets"];
12
12
  functions: Props["functions"];
13
13
  };
14
- type State = {
15
- isTex: boolean;
16
- };
17
- declare class ExpressionEditor extends React.Component<Props, State> {
14
+ declare class ExpressionEditor extends React.Component<Props> {
18
15
  static widgetName: "expression";
19
16
  static defaultProps: DefaultProps;
20
- constructor(props: Props);
21
17
  change(...args: any[]): any;
22
18
  render(): React.ReactNode;
23
19
  serialize: () => any;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Perseus editors",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "2.12.0",
6
+ "version": "2.13.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.6",
26
26
  "@khanacademy/kmath": "^0.1.7",
27
- "@khanacademy/perseus": "^15.0.0",
27
+ "@khanacademy/perseus": "^17.0.0",
28
28
  "@khanacademy/perseus-core": "1.4.0"
29
29
  },
30
30
  "devDependencies": {
@@ -1,5 +1,6 @@
1
1
  import {Dependencies} from "@khanacademy/perseus";
2
2
  import {render, screen} from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
3
4
  import * as React from "react";
4
5
 
5
6
  import "@testing-library/jest-dom";
@@ -53,4 +54,350 @@ describe("expression-editor", () => {
53
54
 
54
55
  expect(await screen.findByText(/π/)).toBeInTheDocument();
55
56
  });
57
+
58
+ it("should toggle multiplication checkbox", () => {
59
+ const onChangeMock = jest.fn();
60
+
61
+ render(<ExpressionEditor onChange={onChangeMock} />);
62
+
63
+ userEvent.click(
64
+ screen.getByRole("checkbox", {
65
+ name: "Use × for rendering multiplication instead of a center dot.",
66
+ }),
67
+ );
68
+
69
+ expect(onChangeMock).toBeCalledWith({times: true});
70
+ });
71
+
72
+ it("should be possible to change function variables", () => {
73
+ const onChangeMock = jest.fn();
74
+
75
+ render(<ExpressionEditor onChange={onChangeMock} functions={[]} />);
76
+
77
+ const input = screen.getByRole("textbox", {
78
+ name: "Function variables:",
79
+ });
80
+
81
+ userEvent.type(input, "x");
82
+
83
+ expect(onChangeMock).toBeCalledWith({functions: ["x"]});
84
+ });
85
+
86
+ it("should toggle division checkbox", () => {
87
+ const onChangeMock = jest.fn();
88
+
89
+ render(<ExpressionEditor onChange={onChangeMock} />);
90
+
91
+ userEvent.click(
92
+ screen.getByRole("checkbox", {
93
+ name: "show \\div button",
94
+ }),
95
+ );
96
+
97
+ expect(onChangeMock).toBeCalledWith(
98
+ {
99
+ answerForms: [],
100
+ buttonSets: ["basic+div"],
101
+ functions: ["f", "g", "h"],
102
+ times: false,
103
+ },
104
+ undefined,
105
+ );
106
+ });
107
+
108
+ it("should toggle trig checkbox", () => {
109
+ const onChangeMock = jest.fn();
110
+
111
+ render(<ExpressionEditor onChange={onChangeMock} />);
112
+
113
+ userEvent.click(
114
+ screen.getByRole("checkbox", {
115
+ name: "trig",
116
+ }),
117
+ );
118
+
119
+ expect(onChangeMock).toBeCalledWith({
120
+ buttonSets: ["basic", "trig"],
121
+ });
122
+ });
123
+
124
+ it("should toggle prealgebra checkbox", () => {
125
+ const onChangeMock = jest.fn();
126
+
127
+ render(<ExpressionEditor onChange={onChangeMock} />);
128
+
129
+ userEvent.click(
130
+ screen.getByRole("checkbox", {
131
+ name: "prealgebra",
132
+ }),
133
+ );
134
+
135
+ expect(onChangeMock).toBeCalledWith({
136
+ buttonSets: ["basic", "prealgebra"],
137
+ });
138
+ });
139
+
140
+ it("should toggle prealgebra checkbox", () => {
141
+ const onChangeMock = jest.fn();
142
+
143
+ render(<ExpressionEditor onChange={onChangeMock} />);
144
+
145
+ userEvent.click(
146
+ screen.getByRole("checkbox", {
147
+ name: "logarithms",
148
+ }),
149
+ );
150
+
151
+ expect(onChangeMock).toBeCalledWith({
152
+ buttonSets: ["basic", "logarithms"],
153
+ });
154
+ });
155
+
156
+ it("should toggle prealgebra checkbox", () => {
157
+ const onChangeMock = jest.fn();
158
+
159
+ render(<ExpressionEditor onChange={onChangeMock} />);
160
+
161
+ userEvent.click(
162
+ screen.getByRole("checkbox", {
163
+ name: "basic relations",
164
+ }),
165
+ );
166
+
167
+ expect(onChangeMock).toBeCalledWith({
168
+ buttonSets: ["basic", "basic relations"],
169
+ });
170
+ });
171
+
172
+ it("should toggle prealgebra checkbox", () => {
173
+ const onChangeMock = jest.fn();
174
+
175
+ render(<ExpressionEditor onChange={onChangeMock} />);
176
+
177
+ userEvent.click(
178
+ screen.getByRole("checkbox", {
179
+ name: "advanced relations",
180
+ }),
181
+ );
182
+
183
+ expect(onChangeMock).toBeCalledWith({
184
+ buttonSets: ["basic", "advanced relations"],
185
+ });
186
+ });
187
+
188
+ it("should be possible to add an answer", () => {
189
+ const onChangeMock = jest.fn();
190
+
191
+ render(<ExpressionEditor onChange={onChangeMock} />);
192
+
193
+ userEvent.click(
194
+ screen.getByRole("button", {
195
+ name: "Add new answer",
196
+ }),
197
+ );
198
+
199
+ expect(onChangeMock).toBeCalledWith(
200
+ {
201
+ answerForms: [
202
+ {
203
+ considered: "correct",
204
+ form: false,
205
+ key: 0,
206
+ simplify: false,
207
+ value: "",
208
+ },
209
+ ],
210
+ buttonSets: ["basic"],
211
+ functions: ["f", "g", "h"],
212
+ times: false,
213
+ },
214
+ undefined,
215
+ );
216
+ });
217
+
218
+ it("should be possible to update answer", () => {
219
+ const onChangeMock = jest.fn();
220
+
221
+ render(
222
+ <ExpressionEditor
223
+ onChange={onChangeMock}
224
+ answerForms={[
225
+ {
226
+ considered: "correct",
227
+ form: false,
228
+ key: "0",
229
+ simplify: false,
230
+ value: "",
231
+ },
232
+ ]}
233
+ />,
234
+ );
235
+
236
+ userEvent.click(
237
+ screen.getByRole("switch", {
238
+ name: "open math keypad",
239
+ }),
240
+ );
241
+
242
+ userEvent.click(
243
+ screen.getByRole("button", {
244
+ name: "9",
245
+ }),
246
+ );
247
+
248
+ expect(onChangeMock).toBeCalledWith(
249
+ {
250
+ answerForms: [
251
+ {
252
+ buttonSets: ["basic"],
253
+ buttonsVisible: "focused",
254
+ considered: "correct",
255
+ form: false,
256
+ functions: ["f", "g", "h"],
257
+ key: "0",
258
+ linterContext: {
259
+ contentType: "",
260
+ highlightLint: false,
261
+ paths: [],
262
+ stack: [],
263
+ },
264
+ simplify: false,
265
+ times: false,
266
+ value: "9",
267
+ },
268
+ ],
269
+ buttonSets: ["basic"],
270
+ functions: ["f", "g", "h"],
271
+ times: false,
272
+ },
273
+ undefined,
274
+ );
275
+ });
276
+
277
+ it("should be possible to toggle same form", () => {
278
+ const onChangeMock = jest.fn();
279
+
280
+ render(
281
+ <ExpressionEditor
282
+ onChange={onChangeMock}
283
+ answerForms={[
284
+ {
285
+ considered: "correct",
286
+ form: false,
287
+ key: "0",
288
+ simplify: false,
289
+ value: "",
290
+ },
291
+ ]}
292
+ />,
293
+ );
294
+
295
+ userEvent.click(
296
+ screen.getByRole("checkbox", {
297
+ name: "Answer expression must have the same form.",
298
+ }),
299
+ );
300
+
301
+ expect(onChangeMock).toBeCalledWith(
302
+ {
303
+ answerForms: [
304
+ {
305
+ considered: "correct",
306
+ form: true,
307
+ key: "0",
308
+ simplify: false,
309
+ value: "",
310
+ },
311
+ ],
312
+ buttonSets: ["basic"],
313
+ functions: ["f", "g", "h"],
314
+ times: false,
315
+ },
316
+ undefined,
317
+ );
318
+ });
319
+
320
+ it("should be possible to toggle expanded and simplified", () => {
321
+ const onChangeMock = jest.fn();
322
+
323
+ render(
324
+ <ExpressionEditor
325
+ onChange={onChangeMock}
326
+ answerForms={[
327
+ {
328
+ considered: "correct",
329
+ form: false,
330
+ key: "0",
331
+ simplify: false,
332
+ value: "",
333
+ },
334
+ ]}
335
+ />,
336
+ );
337
+
338
+ userEvent.click(
339
+ screen.getByRole("checkbox", {
340
+ name: "Answer expression must be fully expanded and simplified.",
341
+ }),
342
+ );
343
+
344
+ expect(onChangeMock).toBeCalledWith(
345
+ {
346
+ answerForms: [
347
+ {
348
+ considered: "correct",
349
+ form: false,
350
+ key: "0",
351
+ simplify: true,
352
+ value: "",
353
+ },
354
+ ],
355
+ buttonSets: ["basic"],
356
+ functions: ["f", "g", "h"],
357
+ times: false,
358
+ },
359
+ undefined,
360
+ );
361
+ });
362
+
363
+ it("should be possible to delete answer", () => {
364
+ const onChangeMock = jest.fn();
365
+
366
+ render(
367
+ <ExpressionEditor
368
+ onChange={onChangeMock}
369
+ answerForms={[
370
+ {
371
+ considered: "correct",
372
+ form: false,
373
+ key: "0",
374
+ simplify: false,
375
+ value: "",
376
+ },
377
+ ]}
378
+ />,
379
+ );
380
+
381
+ userEvent.click(
382
+ screen.getByRole("button", {
383
+ name: "Delete",
384
+ }),
385
+ );
386
+
387
+ userEvent.click(
388
+ screen.getByRole("button", {
389
+ name: "I'm sure!",
390
+ }),
391
+ );
392
+
393
+ expect(onChangeMock).toBeCalledWith(
394
+ {
395
+ answerForms: [],
396
+ buttonSets: ["basic"],
397
+ functions: ["f", "g", "h"],
398
+ times: false,
399
+ },
400
+ undefined,
401
+ );
402
+ });
56
403
  });
@@ -17,7 +17,7 @@ import SortableArea from "../components/sortable";
17
17
 
18
18
  import type {PerseusExpressionWidgetOptions} from "@khanacademy/perseus";
19
19
 
20
- const {InfoTip, PropCheckBox, TexButtons} = components;
20
+ const {InfoTip, PropCheckBox} = components;
21
21
 
22
22
  type Props = {
23
23
  widgetId?: any;
@@ -36,6 +36,15 @@ type DefaultProps = {
36
36
  functions: Props["functions"];
37
37
  };
38
38
 
39
+ const buttonSetsList: any = [
40
+ "basic",
41
+ "trig",
42
+ "prealgebra",
43
+ "logarithms",
44
+ "basic relations",
45
+ "advanced relations",
46
+ ];
47
+
39
48
  const parseAnswerKey = ({key}: AnswerForm): number => {
40
49
  // We don't throw here because there is data stored in some
41
50
  // exercises/articles where the answer forms don't have a key. If we throw,
@@ -67,11 +76,7 @@ const _makeNewKey = (answerForms: ReadonlyArray<AnswerForm>) => {
67
76
  return usedKeys.length;
68
77
  };
69
78
 
70
- type State = {
71
- isTex: boolean;
72
- };
73
-
74
- class ExpressionEditor extends React.Component<Props, State> {
79
+ class ExpressionEditor extends React.Component<Props> {
75
80
  static widgetName = "expression" as const;
76
81
 
77
82
  static defaultProps: DefaultProps = {
@@ -81,29 +86,6 @@ class ExpressionEditor extends React.Component<Props, State> {
81
86
  functions: ["f", "g", "h"],
82
87
  };
83
88
 
84
- constructor(props: Props) {
85
- super(props);
86
- // Is the format of `value` TeX or plain text?
87
- // TODO(alex): Remove after backfilling everything to TeX
88
- // TODO(joel) - sucks if you edit some expression without
89
- // backslashes or curly braces, then come back to the question and
90
- // it's surprisingly not TeX anymore.
91
-
92
- let isTex;
93
- // default to TeX if new;
94
- if (props.answerForms.length === 0) {
95
- isTex = true;
96
- } else {
97
- isTex = props.answerForms.some((form) => {
98
- const {value} = form;
99
- // only TeX has backslashes and curly braces
100
- return value.indexOf("\\") !== -1 || value.indexOf("{") !== -1;
101
- });
102
- }
103
-
104
- this.state = {isTex};
105
- }
106
-
107
89
  change(...args) {
108
90
  return Changeable.change.apply(this, args);
109
91
  }
@@ -156,38 +138,31 @@ class ExpressionEditor extends React.Component<Props, State> {
156
138
  );
157
139
 
158
140
  // checkboxes to choose which sets of input buttons are shown
159
- const buttonSetChoices = Object.keys(TexButtons.buttonSets).map(
160
- (name) => {
161
- // The first one gets special cased to always be checked, disabled,
162
- // and float left.
163
- const isFirst = name === "basic";
164
- const checked =
165
- this.props.buttonSets.includes(
166
- name as PerseusExpressionWidgetOptions["buttonSets"][number],
167
- ) || isFirst;
168
- const className = isFirst
169
- ? "button-set-label-float"
170
- : "button-set-label";
171
- return (
172
- <label className={className} key={name}>
173
- <input
174
- type="checkbox"
175
- checked={checked}
176
- disabled={isFirst}
177
- onChange={() => this.handleButtonSet(name)}
178
- />
179
- {name}
180
- </label>
181
- );
182
- },
183
- );
141
+ const buttonSetChoices = buttonSetsList.map((name) => {
142
+ // The first one gets special cased to always be checked, disabled,
143
+ // and float left.
144
+ const isFirst = name === "basic";
145
+ const checked =
146
+ this.props.buttonSets.includes(
147
+ name as PerseusExpressionWidgetOptions["buttonSets"][number],
148
+ ) || isFirst;
149
+ return (
150
+ <label className="button-set-label" key={name}>
151
+ <input
152
+ type="checkbox"
153
+ checked={checked}
154
+ disabled={isFirst}
155
+ onChange={() => this.handleButtonSet(name)}
156
+ />
157
+ {name}
158
+ </label>
159
+ );
160
+ });
184
161
 
185
162
  const {TeX} = Dependencies.getDependencies(); // OldExpression only
186
163
 
187
- buttonSetChoices.splice(
188
- 1,
189
- 1,
190
- <label key="show-div">
164
+ buttonSetChoices.unshift(
165
+ <label className="button-set-label" key="show-div">
191
166
  <input type="checkbox" onChange={this.handleToggleDiv} />
192
167
  <span className="show-div-button">
193
168
  show <TeX>\div</TeX> button
@@ -240,15 +215,6 @@ class ExpressionEditor extends React.Component<Props, State> {
240
215
  {buttonSetChoices}
241
216
  </div>
242
217
 
243
- {this.state.isTex && (
244
- <TexButtons
245
- className="math-input-buttons"
246
- sets={this.props.buttonSets}
247
- convertDotToTimes={this.props.times}
248
- onInsert={this.handleTexInsert}
249
- />
250
- )}
251
-
252
218
  <h3 className="expression-editor-h3">Answers</h3>
253
219
 
254
220
  <p style={{margin: "4px 0"}}>
@@ -397,9 +363,7 @@ class ExpressionEditor extends React.Component<Props, State> {
397
363
 
398
364
  // called when the selected buttonset changes
399
365
  handleButtonSet: (changingName: string) => void = (changingName) => {
400
- const buttonSetNames = Object.keys(
401
- TexButtons.buttonSets,
402
- ) as LegacyButtonSet[];
366
+ const buttonSetNames = buttonSetsList;
403
367
 
404
368
  // Filter to preserve order - using .union and .difference would always
405
369
  // move the last added button set to the end.