@khanacademy/perseus 1.0.0 → 1.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/es/index.css +17 -8
  3. package/dist/es/index.js +258 -223
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +17 -8
  6. package/dist/index.js +261 -225
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/__stories__/input-with-examples.stories.jsx +1 -0
  10. package/src/components/input-with-examples.jsx +41 -24
  11. package/src/components/number-input.jsx +2 -2
  12. package/src/components/simple-keypad-input.jsx +18 -21
  13. package/src/components/text-input.jsx +5 -3
  14. package/src/index.js +1 -1
  15. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +356 -65
  16. package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
  17. package/src/perseus-types.js +18 -7
  18. package/src/styles/perseus-renderer.less +8 -2
  19. package/src/styles/widgets/matrix.less +2 -0
  20. package/src/styles/widgets/passage.less +9 -5
  21. package/src/util/answer-types.js +2 -3
  22. package/src/util/math.js +3 -10
  23. package/src/widgets/__stories__/numeric-input.stories.jsx +94 -7
  24. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
  25. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +15 -6
  26. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +5 -2
  27. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +4 -1
  28. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +228 -67
  29. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +24 -24
  30. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +4 -4
  31. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +2581 -679
  32. package/src/widgets/__tests__/group_test.jsx +4 -4
  33. package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
  34. package/src/widgets/__tests__/passage_test.jsx +81 -0
  35. package/src/widgets/__tests__/radio/base-radio_test.jsx +55 -7
  36. package/src/widgets/__tests__/radio/choice_test.jsx +164 -63
  37. package/src/widgets/__tests__/radio_test.jsx +39 -15
  38. package/src/widgets/definition.jsx +1 -1
  39. package/src/widgets/graded-group.jsx +5 -1
  40. package/src/widgets/group.jsx +84 -102
  41. package/src/widgets/input-number.jsx +6 -1
  42. package/src/widgets/matrix.jsx +17 -1
  43. package/src/widgets/numeric-input.jsx +99 -86
  44. package/src/widgets/radio/base-radio.jsx +3 -1
  45. package/src/widgets/radio/choice-a11y-text.js +62 -0
  46. package/src/widgets/radio/choice-icon/choice-icon.jsx +0 -68
  47. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +5 -10
  48. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +3 -12
  49. package/src/widgets/radio/choice.jsx +46 -3
@@ -347,7 +347,7 @@ describe("group widget", () => {
347
347
 
348
348
  // Arrange
349
349
  const {renderer} = renderQuestion(question1);
350
- userEvent.click(screen.getAllByRole("checkbox")[4]);
350
+ userEvent.click(screen.getAllByRole("radio")[4]);
351
351
  userEvent.type(screen.getAllByRole("textbox")[0], "1000");
352
352
  userEvent.type(screen.getAllByRole("textbox")[1], "9999");
353
353
 
@@ -360,7 +360,7 @@ describe("group widget", () => {
360
360
  renderer1.restoreSerializedState(state);
361
361
 
362
362
  // Assert
363
- expect(screen.getAllByRole("checkbox")[4]).toBeChecked();
363
+ expect(screen.getAllByRole("radio")[4]).toBeChecked();
364
364
  expect(screen.getAllByRole("textbox")[0]).toHaveValue("1000");
365
365
  expect(screen.getAllByRole("textbox")[1]).toHaveValue("9999");
366
366
  });
@@ -369,7 +369,7 @@ describe("group widget", () => {
369
369
  // Arrange
370
370
  const {renderer} = renderQuestion(question1);
371
371
  // Answer all widgets correctly
372
- userEvent.click(screen.getAllByRole("checkbox")[4]);
372
+ userEvent.click(screen.getAllByRole("radio")[4]);
373
373
  userEvent.type(screen.getAllByRole("textbox")[0], "230");
374
374
  userEvent.type(screen.getAllByRole("textbox")[1], "200");
375
375
 
@@ -453,7 +453,7 @@ describe("group widget", () => {
453
453
  it("should show rationales for contained widgets", () => {
454
454
  // Arrange
455
455
  const {renderer} = renderQuestion(question1);
456
- userEvent.click(screen.getAllByRole("checkbox")[2]); // Incorrect!
456
+ userEvent.click(screen.getAllByRole("radio")[2]); // Incorrect!
457
457
 
458
458
  // Act
459
459
  renderer.showRationalesForCurrentlySelectedChoices();
@@ -529,14 +529,18 @@ describe("unionAnswerForms utility function", () => {
529
529
  it("removes duplicates", () => {
530
530
  // arrange
531
531
  const forms = [
532
- {
533
- status: "correct",
534
- value: 1,
535
- },
536
- {
537
- status: "correct",
538
- value: 1,
539
- },
532
+ [
533
+ {
534
+ simplify: "required",
535
+ name: "integer",
536
+ },
537
+ ],
538
+ [
539
+ {
540
+ simplify: "required",
541
+ name: "integer",
542
+ },
543
+ ],
540
544
  ];
541
545
 
542
546
  // act
@@ -1,9 +1,12 @@
1
1
  // @flow
2
2
 
3
3
  import "@testing-library/jest-dom";
4
+ import {render, screen} from "@testing-library/react";
5
+ import React from "react";
4
6
 
5
7
  import {testDependencies} from "../../../../../testing/test-dependencies.js";
6
8
  import * as Dependencies from "../../dependencies.js";
9
+ import {ApiOptions} from "../../perseus-api.jsx";
7
10
  import {question1, question2} from "../__testdata__/passage_testdata.js";
8
11
  import PassageWidgetExport from "../passage.jsx";
9
12
  import * as getLineHeightModule from "../passage/get-line-height-for-node.js";
@@ -14,6 +17,40 @@ import type {APIOptions} from "../../types.js";
14
17
 
15
18
  jest.mock("../passage/get-line-height-for-node.js");
16
19
 
20
+ function renderPassage(overwrite) {
21
+ const widgetPropsBase = {
22
+ footnotes: "",
23
+ passageText: "",
24
+ passageTitle: "",
25
+ showLineNumbers: false,
26
+ static: true,
27
+ };
28
+
29
+ const base = {
30
+ ...widgetPropsBase,
31
+ alignment: null,
32
+ apiOptions: {
33
+ ...ApiOptions.defaults,
34
+ },
35
+ containerSizeClass: "small",
36
+ findWidgets: (callback) => [],
37
+ isLastUsedWidget: false,
38
+ onBlur: () => {},
39
+ onChange: () => {},
40
+ onFocus: () => {},
41
+ problemNum: 1,
42
+ reviewModeRubric: {
43
+ ...widgetPropsBase,
44
+ },
45
+ static: true,
46
+ trackInteraction: () => {},
47
+ widgetId: "passage",
48
+ };
49
+
50
+ const extended = {...base, ...overwrite};
51
+ return render(<PassageWidgetExport.widget {...extended} />);
52
+ }
53
+
17
54
  describe("passage widget", () => {
18
55
  beforeEach(() => {
19
56
  jest.spyOn(getLineHeightModule, "getLineHeightForNode").mockReturnValue(
@@ -130,4 +167,48 @@ describe("passage widget", () => {
130
167
  // Assert
131
168
  expect(reference).toBe(null);
132
169
  });
170
+
171
+ it("should render passage title", () => {
172
+ renderPassage({passageTitle: "Passage title"});
173
+
174
+ expect(screen.getByText("Passage title")).toBeInTheDocument();
175
+ });
176
+
177
+ it("should render passage text", () => {
178
+ renderPassage({passageText: "Passage text"});
179
+
180
+ expect(screen.getByText("Passage text")).toBeInTheDocument();
181
+ });
182
+
183
+ it("should render footnotes", () => {
184
+ renderPassage({footnotes: "Footnote text"});
185
+
186
+ expect(screen.getByText("Footnote text")).toBeInTheDocument();
187
+ });
188
+
189
+ it("should render first question instructions", () => {
190
+ renderPassage({passageText: "[[test]] Passage text"});
191
+
192
+ expect(screen.getByText("The symbol")).toBeInTheDocument();
193
+ expect(screen.getAllByText("[Marker for question test]")).toHaveLength(
194
+ 2,
195
+ );
196
+ expect(
197
+ screen.getByText(
198
+ "indicates that question test references this portion of the passage",
199
+ ),
200
+ ).toBeInTheDocument();
201
+ });
202
+
203
+ it("should render first sentence instructions", () => {
204
+ renderPassage({passageText: "[[1]] Passage text"});
205
+
206
+ expect(screen.getByText("The symbol")).toBeInTheDocument();
207
+ expect(screen.getAllByText("[Marker for question 1]")).toHaveLength(2);
208
+ expect(
209
+ screen.getByText(
210
+ "indicates that question 1 references this portion of the passage",
211
+ ),
212
+ ).toBeInTheDocument();
213
+ });
133
214
  });
@@ -69,9 +69,17 @@ describe("base-radio", () => {
69
69
 
70
70
  // Assert
71
71
  expect(
72
- screen.getByRole("checkbox", {name: "Select Choice E"}),
72
+ screen.getByRole("radio", {
73
+ name: "(Choice E) None of the above",
74
+ }),
73
75
  ).toBeInTheDocument();
74
- expect(screen.getByText("None of the above")).toBeInTheDocument();
76
+ expect(
77
+ screen.getByLabelText("(Choice E) None of the above"),
78
+ ).toBeInTheDocument();
79
+ // Note(Tamara): There should be two of these options: one for the
80
+ // screen reader only radio input and a button for learners
81
+ // using the visual choices
82
+ expect(screen.getAllByText("None of the above")).toHaveLength(2);
75
83
  });
76
84
 
77
85
  describe("edit mode", () => {
@@ -111,7 +119,7 @@ describe("base-radio", () => {
111
119
 
112
120
  // Act
113
121
  userEvent.click(
114
- screen.getByRole("checkbox", {name: "Select Choice C"}),
122
+ screen.getByRole("radio", {name: "(Choice C) Option Gamma"}),
115
123
  );
116
124
 
117
125
  // Assert
@@ -158,7 +166,7 @@ describe("base-radio", () => {
158
166
 
159
167
  // Act
160
168
  const radioButton = screen.getByRole("checkbox", {
161
- name: "Select Choice C",
169
+ name: "(Choice C, Checked) Option Gamma",
162
170
  });
163
171
  userEvent.click(radioButton);
164
172
 
@@ -169,7 +177,7 @@ describe("base-radio", () => {
169
177
  });
170
178
 
171
179
  // Equivalent to "should toggle choice when inner element clicked" but with editMode set to false
172
- it("select single select choices", () => {
180
+ it("selects single select choices", () => {
173
181
  // Arrange
174
182
  let updatedValues = null;
175
183
  const onChangeHandler = (newValues) => {
@@ -177,7 +185,7 @@ describe("base-radio", () => {
177
185
  };
178
186
 
179
187
  renderBaseRadio({
180
- multipleSelect: true,
188
+ multipleSelect: false,
181
189
  choices: [
182
190
  generateChoice({
183
191
  content: "Option 1",
@@ -201,7 +209,7 @@ describe("base-radio", () => {
201
209
 
202
210
  // Act
203
211
  userEvent.click(
204
- screen.getByRole("checkbox", {name: "Select Choice C"}),
212
+ screen.getByRole("radio", {name: "(Choice C) Option Gamma"}),
205
213
  );
206
214
 
207
215
  // Assert
@@ -209,6 +217,46 @@ describe("base-radio", () => {
209
217
  checked: [false, false, true, false],
210
218
  });
211
219
  });
220
+
221
+ it("deselects single select selected choice", () => {
222
+ // Arrange
223
+ let updatedValues = null;
224
+ const onChangeHandler = (newValues) => {
225
+ updatedValues = newValues;
226
+ };
227
+
228
+ renderBaseRadio({
229
+ multipleSelect: false,
230
+ choices: [
231
+ generateChoice({
232
+ content: "Option 1",
233
+ correct: false,
234
+ checked: true,
235
+ }),
236
+ generateChoice({
237
+ content: "Option B",
238
+ correct: false,
239
+ }),
240
+ generateChoice({
241
+ content: "Option Gamma",
242
+ correct: true,
243
+ }),
244
+ generateChoice({
245
+ content: "Option Delta",
246
+ correct: false,
247
+ }),
248
+ ],
249
+ onChange: onChangeHandler,
250
+ });
251
+
252
+ // Act
253
+ userEvent.click(screen.getAllByRole("radio")[0]);
254
+
255
+ // Assert
256
+ expect(updatedValues).toMatchObject({
257
+ checked: [false, false, false, false],
258
+ });
259
+ });
212
260
  });
213
261
 
214
262
  describe("instruction text", () => {
@@ -30,66 +30,105 @@ function renderChoice(options) {
30
30
  return render(<Choice {...overwrittenOptions} />);
31
31
  }
32
32
 
33
- describe("choice", () => {
34
- it("renders choice content", () => {
33
+ describe("all choice options", () => {
34
+ it("renders rationale", () => {
35
35
  // Arrange / Act
36
- renderChoice();
36
+ renderChoice({showRationale: true});
37
37
 
38
38
  // Assert
39
- expect(screen.getByText("This is a possible choice")).toBeVisible();
39
+ expect(screen.getByText("This is a good rationale")).toBeVisible();
40
40
  });
41
41
 
42
- it("renders rationale", () => {
42
+ it("shows correct when in review mode", () => {
43
43
  // Arrange / Act
44
- renderChoice({showRationale: true});
44
+ renderChoice({
45
+ checked: false,
46
+ correct: true,
47
+ showCorrectness: true,
48
+ reviewMode: true,
49
+ });
45
50
 
46
51
  // Assert
47
- expect(screen.getByText("This is a good rationale")).toBeVisible();
52
+ expect(screen.getByText("Correct")).toBeVisible();
48
53
  });
49
54
 
50
- it("renders choice button", () => {
55
+ it("shows incorrect when in review mode", () => {
51
56
  // Arrange / Act
52
- renderChoice();
57
+ renderChoice({
58
+ checked: false,
59
+ correct: false,
60
+ showCorrectness: true,
61
+ reviewMode: true,
62
+ });
53
63
 
54
- const button = screen.getByRole("checkbox", {
55
- name: "Select Choice A",
64
+ // Assert
65
+ expect(screen.getByText("Incorrect")).toBeVisible();
66
+ });
67
+
68
+ it("shows correct a11y text when in review mode", () => {
69
+ // Arrange / Act
70
+ renderChoice({
71
+ checked: false,
72
+ correct: true,
73
+ showCorrectness: true,
74
+ reviewMode: true,
56
75
  });
57
76
 
58
77
  // Assert
59
- expect(button).toBeVisible();
78
+ expect(
79
+ screen.getByText("(Choice A, Correct) This is a possible choice"),
80
+ ).toBeVisible();
60
81
  });
61
82
 
62
- it("has correct aria-checked when checked", () => {
83
+ it("shows correct selected when in review mode", () => {
63
84
  // Arrange / Act
64
- renderChoice({checked: true});
85
+ renderChoice({
86
+ checked: true,
87
+ correct: true,
88
+ showCorrectness: true,
89
+ reviewMode: true,
90
+ });
65
91
 
66
- const button = screen.getByRole("checkbox", {
67
- name: "Select Choice A",
92
+ // Assert
93
+ expect(screen.getByText("Correct (selected)")).toBeVisible();
94
+ });
95
+
96
+ it("shows incorrect selected when in review mode", () => {
97
+ // Arrange / Act
98
+ renderChoice({
99
+ checked: true,
100
+ correct: false,
101
+ showCorrectness: true,
102
+ reviewMode: true,
68
103
  });
69
104
 
70
105
  // Assert
71
- expect(button).toBeChecked();
106
+ expect(screen.getByText("Incorrect (selected)")).toBeVisible();
72
107
  });
108
+ });
73
109
 
74
- it("has correct aria-checked when unchecked", () => {
110
+ // Tests 1 of 2 element types used to select a choice
111
+ describe("choice button", () => {
112
+ it("selects the choice by clicking the button", () => {
75
113
  // Arrange / Act
76
- renderChoice({checked: false});
114
+ const onChangeSpy = jest.fn();
115
+ renderChoice({onChange: onChangeSpy});
77
116
 
78
- const button = screen.getByRole("checkbox", {
79
- name: "Select Choice A",
80
- });
117
+ const button = screen.getByRole("button", {hidden: true});
118
+ userEvent.click(button);
81
119
 
82
120
  // Assert
83
- expect(button).not.toBeChecked();
121
+ expect(onChangeSpy).toHaveBeenCalledWith({
122
+ checked: true,
123
+ crossedOut: false,
124
+ });
84
125
  });
85
126
 
86
127
  it("has correct aria-disabled when disabled", () => {
87
128
  // Arrange / Act
88
129
  renderChoice({disabled: true});
89
130
 
90
- const button = screen.getByRole("checkbox", {
91
- name: "Select Choice A",
92
- });
131
+ const button = screen.getByRole("button", {hidden: true});
93
132
 
94
133
  // Assert
95
134
  expect(button.getAttribute("aria-disabled")).toBe("true");
@@ -99,64 +138,126 @@ describe("choice", () => {
99
138
  // Arrange / Act
100
139
  renderChoice({disabled: false});
101
140
 
102
- const button = screen.getByRole("checkbox", {
103
- name: "Select Choice A",
104
- });
141
+ const button = screen.getByRole("button", {hidden: true});
105
142
 
106
143
  // Assert
107
144
  expect(button.getAttribute("aria-disabled")).toBe("false");
108
145
  });
109
146
 
110
- it("shows correct when in review mode", () => {
111
- // Arrange / Act
147
+ it("can be checked", () => {
148
+ // Arrange
149
+ let checked = false;
112
150
  renderChoice({
113
- checked: false,
114
- correct: true,
115
- showCorrectness: true,
116
- reviewMode: true,
151
+ checked,
152
+ onChange: (next) => {
153
+ checked = next.checked;
154
+ },
117
155
  });
118
156
 
157
+ // Act
158
+ const button = screen.getByRole("button", {hidden: true});
159
+ userEvent.click(button);
160
+
119
161
  // Assert
120
- expect(screen.getByText("Correct")).toBeVisible();
162
+ expect(checked).toBeTrue();
121
163
  });
122
164
 
123
- it("shows correct selected when in review mode", () => {
124
- // Arrange / Act
165
+ it("can be unchecked", () => {
166
+ // Arrange
167
+ let checked = true;
125
168
  renderChoice({
169
+ checked,
170
+ onChange: (next) => {
171
+ checked = next.checked;
172
+ },
173
+ });
174
+
175
+ // Act
176
+ const button = screen.getByRole("button", {hidden: true});
177
+ userEvent.click(button);
178
+
179
+ // Assert
180
+ expect(checked).toBeFalse();
181
+ });
182
+ });
183
+
184
+ // Tests 2 of 2 element types used to select a choice
185
+ describe("choice input (screen reader only)", () => {
186
+ it("selects the choice by clicking the input", () => {
187
+ // Arrange / Act
188
+ const onChangeSpy = jest.fn();
189
+ renderChoice({onChange: onChangeSpy});
190
+
191
+ const input = screen.getByRole("radio", {
192
+ name: "(Choice A) This is a possible choice",
193
+ });
194
+ userEvent.click(input);
195
+
196
+ // Assert
197
+ expect(onChangeSpy).toHaveBeenCalledWith({
126
198
  checked: true,
127
- correct: true,
128
- showCorrectness: true,
129
- reviewMode: true,
199
+ crossedOut: false,
200
+ });
201
+ });
202
+
203
+ it("registers as unchecked with checked set to false", () => {
204
+ // Arrange / Act
205
+ renderChoice({checked: false});
206
+
207
+ const input = screen.getByRole("radio", {
208
+ name: "(Choice A) This is a possible choice",
130
209
  });
131
210
 
132
211
  // Assert
133
- expect(screen.getByText("Correct (selected)")).toBeVisible();
212
+ expect(input).not.toBeChecked();
134
213
  });
135
214
 
136
- it("shows incorrect when in review mode", () => {
215
+ it("registers as checked with checked set to true", () => {
137
216
  // Arrange / Act
138
- renderChoice({
139
- checked: false,
140
- correct: false,
141
- showCorrectness: true,
142
- reviewMode: true,
217
+ renderChoice({checked: true});
218
+
219
+ const input = screen.getByRole("radio", {
220
+ name: "(Choice A, Checked) This is a possible choice",
143
221
  });
144
222
 
145
223
  // Assert
146
- expect(screen.getByText("Incorrect")).toBeVisible();
224
+ expect(input).toBeChecked();
147
225
  });
148
226
 
149
- it("shows incorrect selected when in review mode", () => {
227
+ it("has correct a11y text when crossed out", () => {
150
228
  // Arrange / Act
151
- renderChoice({
152
- checked: true,
153
- correct: false,
154
- showCorrectness: true,
155
- reviewMode: true,
229
+ renderChoice({crossedOut: true});
230
+
231
+ const input = screen.getByRole("radio", {
232
+ name: "(Choice A, Crossed out) This is a possible choice",
156
233
  });
157
234
 
158
235
  // Assert
159
- expect(screen.getByText("Incorrect (selected)")).toBeVisible();
236
+ expect(input).toBeVisible();
237
+ });
238
+
239
+ it("has correct a11y text when incorrect, crossed out, and showCorrectness is true", () => {
240
+ // Arrange / Act
241
+ renderChoice({crossedOut: true, correct: false, showCorrectness: true});
242
+
243
+ const input = screen.getByRole("radio", {
244
+ name: "(Choice A, Crossed out, Incorrect) This is a possible choice",
245
+ });
246
+
247
+ // Assert
248
+ expect(input).toBeVisible();
249
+ });
250
+
251
+ it("has correct a11y text when correct, crossed out, and showCorrectness is true", () => {
252
+ // Arrange / Act
253
+ renderChoice({crossedOut: true, correct: true, showCorrectness: true});
254
+
255
+ const input = screen.getByRole("radio", {
256
+ name: "(Choice A, Crossed out, Correct) This is a possible choice",
257
+ });
258
+
259
+ // Assert
260
+ expect(input).toBeVisible();
160
261
  });
161
262
 
162
263
  it("can be checked", () => {
@@ -170,13 +271,13 @@ describe("choice", () => {
170
271
  });
171
272
 
172
273
  // Act
173
- const button = screen.getByRole("checkbox", {
174
- name: "Select Choice A",
274
+ const input = screen.getByRole("radio", {
275
+ name: "(Choice A) This is a possible choice",
175
276
  });
176
- userEvent.click(button);
277
+ userEvent.click(input);
177
278
 
178
279
  // Assert
179
- expect(checked).toBeTrue();
280
+ expect(checked).toBe(true);
180
281
  });
181
282
 
182
283
  it("can be unchecked", () => {
@@ -190,10 +291,10 @@ describe("choice", () => {
190
291
  });
191
292
 
192
293
  // Act
193
- const button = screen.getByRole("checkbox", {
194
- name: "Select Choice A",
294
+ const input = screen.getByRole("radio", {
295
+ name: "(Choice A, Checked) This is a possible choice",
195
296
  });
196
- userEvent.click(button);
297
+ userEvent.click(input);
197
298
 
198
299
  // Assert
199
300
  expect(checked).toBeFalse();