@khanacademy/perseus 1.4.1 → 2.0.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.
- package/CHANGELOG.md +18 -0
- package/dist/es/index.css +10 -246
- package/dist/es/index.js +143 -585
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +10 -246
- package/dist/index.js +140 -582
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__stories__/hints-renderer.stories.jsx +9 -1
- package/src/__testdata__/graphie_testdata.js +53 -0
- package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +0 -9
- package/src/__tests__/__snapshots__/renderer_test.jsx.snap +0 -9
- package/src/components/__stories__/graphie.stories.jsx +5 -1
- package/src/hint-renderer.jsx +11 -14
- package/src/perseus-api.jsx +0 -23
- package/src/perseus-markdown.jsx +0 -25
- package/src/styles/articles.less +6 -1
- package/src/styles/constants.js +0 -21
- package/src/styles/global-constants.js +0 -6
- package/src/styles/khan-exercise.css +9 -1
- package/src/styles/perseus-renderer.less +0 -259
- package/src/styles/variables.less +0 -11
- package/src/styles/variables.shared.less +0 -10
- package/src/types.js +0 -16
- package/src/widget-container.jsx +4 -0
- package/src/widgets/__stories__/definition.stories.jsx +27 -1
- package/src/widgets/__stories__/input-number.stories.jsx +0 -14
- package/src/widgets/__stories__/radio.stories.jsx +0 -3
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +20 -30
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +0 -9
- package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +0 -18
- package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +2 -440
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +66 -3726
- package/src/widgets/__tests__/numeric-input_test.jsx +54 -118
- package/src/widgets/__tests__/radio/choice-icon_test.jsx +25 -75
- package/src/widgets/__tests__/radio_test.jsx +362 -394
- package/src/widgets/definition.jsx +27 -14
- package/src/widgets/input-number.jsx +2 -60
- package/src/widgets/numeric-input.jsx +17 -89
- package/src/widgets/radio/__stories__/base-radio.stories.jsx +1 -3
- package/src/widgets/radio/__stories__/choice-icon.stories.jsx +23 -105
- package/src/widgets/radio/__stories__/option-status.stories.jsx +0 -8
- package/src/widgets/radio/base-radio.jsx +13 -96
- package/src/widgets/radio/choice-icon/choice-icon.jsx +177 -50
- package/src/widgets/radio/choice-icon/cross-out-line.jsx +9 -13
- package/src/widgets/radio/choice-icon/shared-styles.js +2 -3
- package/src/widgets/radio/choice.jsx +5 -59
- package/src/widgets/radio/option-status.jsx +2 -14
- package/src/components/__stories__/possible-answers.stories.jsx +0 -22
- package/src/components/possible-answers.jsx +0 -36
- package/src/styles/widgets/definition.less +0 -86
- package/src/widgets/radio/__stories__/sat-styles.stories.jsx +0 -191
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +0 -225
- package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +0 -120
|
@@ -45,488 +45,458 @@ describe("single-choice question", () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
const [question, correct, incorrect] = questionAndAnswer;
|
|
48
|
+
const apiOptions = Object.freeze({});
|
|
48
49
|
|
|
49
|
-
describe.each([[true], [false]])(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
describe.each([[true], [false]])(
|
|
51
|
+
"reviewMode: %s",
|
|
52
|
+
(reviewMode: boolean) => {
|
|
53
|
+
it("should snapshot the same", () => {
|
|
54
|
+
// Arrange & Act
|
|
55
|
+
const {container} = renderQuestion(question, apiOptions, {
|
|
56
|
+
reviewMode,
|
|
57
|
+
});
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
it("should snapshot the same", () => {
|
|
58
|
-
// Arrange & Act
|
|
59
|
-
const {container} = renderQuestion(question, apiOptions, {
|
|
60
|
-
reviewMode,
|
|
61
|
-
});
|
|
59
|
+
// Assert
|
|
60
|
+
expect(container).toMatchSnapshot("first render");
|
|
61
|
+
});
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
it("should snapshot the same with correct answer", () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const {container} = renderQuestion(question, apiOptions);
|
|
66
|
+
|
|
67
|
+
// Act
|
|
68
|
+
selectOption(correct);
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(container).toMatchSnapshot("correct answer");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should snapshot the same with incorrect answer", () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
const {container} = renderQuestion(question, apiOptions);
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
selectOption(incorrect[0]);
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(container).toMatchSnapshot("incorrect answer");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should accept the right answer (mouse)", () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const {renderer} = renderQuestion(question, apiOptions, {
|
|
88
|
+
reviewMode,
|
|
65
89
|
});
|
|
66
90
|
|
|
67
|
-
|
|
91
|
+
// Act
|
|
92
|
+
selectOption(correct);
|
|
93
|
+
// Assert
|
|
94
|
+
expect(renderer).toHaveBeenAnsweredCorrectly();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should accept the right answer (touch)", () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const {renderer} = renderQuestion(question, apiOptions);
|
|
100
|
+
const correctRadio = screen.getAllByRole("radio")[correct];
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
fireEvent.touchStart(correctRadio);
|
|
104
|
+
fireEvent.touchEnd(correctRadio);
|
|
105
|
+
// We're using fireEvent.click() because mobile browsers synthesize
|
|
106
|
+
// click events from matching touchStart/touchEnd events. user-event
|
|
107
|
+
// does not support touch events so we have to do this ourselves.
|
|
108
|
+
// eslint-disable-next-line testing-library/prefer-user-event
|
|
109
|
+
fireEvent.click(correctRadio);
|
|
110
|
+
|
|
111
|
+
// Assert
|
|
112
|
+
expect(renderer).toHaveBeenAnsweredCorrectly();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it.each(incorrect)(
|
|
116
|
+
"should reject incorrect answer - choice %d",
|
|
117
|
+
(incorrect: number) => {
|
|
68
118
|
// Arrange
|
|
69
|
-
const {
|
|
119
|
+
const {renderer} = renderQuestion(question, apiOptions);
|
|
70
120
|
|
|
71
121
|
// Act
|
|
72
|
-
selectOption(
|
|
122
|
+
selectOption(incorrect);
|
|
73
123
|
|
|
74
124
|
// Assert
|
|
75
|
-
expect(
|
|
125
|
+
expect(renderer).toHaveBeenAnsweredIncorrectly();
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
it("calling .focus() on the renderer should succeed", () => {
|
|
130
|
+
// Arrange
|
|
131
|
+
const {renderer} = renderQuestion(question, apiOptions, {
|
|
132
|
+
reviewMode,
|
|
76
133
|
});
|
|
77
134
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const {container} = renderQuestion(question, apiOptions);
|
|
135
|
+
// Act
|
|
136
|
+
const gotFocus = renderer.focus();
|
|
81
137
|
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
// Assert
|
|
139
|
+
expect(gotFocus).toBeTrue();
|
|
140
|
+
});
|
|
84
141
|
|
|
85
|
-
|
|
86
|
-
|
|
142
|
+
it("should deselect incorrect selected choices", () => {
|
|
143
|
+
// Arrange
|
|
144
|
+
const {renderer} = renderQuestion(question, apiOptions, {
|
|
145
|
+
reviewMode,
|
|
87
146
|
});
|
|
88
147
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
148
|
+
// Act
|
|
149
|
+
// Since this is a single-select setup, just select the first
|
|
150
|
+
// incorrect choice.
|
|
151
|
+
selectOption(incorrect[0]);
|
|
152
|
+
renderer.deselectIncorrectSelectedChoices();
|
|
94
153
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
expect(renderer).toHaveBeenAnsweredCorrectly();
|
|
154
|
+
// Assert
|
|
155
|
+
screen.getAllByRole("radio").forEach((r) => {
|
|
156
|
+
expect(r).not.toBeChecked();
|
|
99
157
|
});
|
|
158
|
+
});
|
|
100
159
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
160
|
+
it("should disable all radio inputs when static is true", () => {
|
|
161
|
+
// Arrange
|
|
162
|
+
const staticQuestion = {
|
|
163
|
+
...question,
|
|
164
|
+
widgets: {
|
|
165
|
+
...question.widgets,
|
|
166
|
+
"radio 1": {
|
|
167
|
+
...question.widgets["radio 1"],
|
|
168
|
+
static: true,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
renderQuestion(staticQuestion, apiOptions, {reviewMode});
|
|
105
173
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
fireEvent.touchEnd(correctRadio);
|
|
109
|
-
// We're using fireEvent.click() because mobile browsers synthesize
|
|
110
|
-
// click events from matching touchStart/touchEnd events. user-event
|
|
111
|
-
// does not support touch events so we have to do this ourselves.
|
|
112
|
-
// eslint-disable-next-line testing-library/prefer-user-event
|
|
113
|
-
fireEvent.click(correctRadio);
|
|
174
|
+
// Act
|
|
175
|
+
selectOption(correct);
|
|
114
176
|
|
|
115
|
-
|
|
116
|
-
|
|
177
|
+
// Assert
|
|
178
|
+
// Everything's read-only so no selections made
|
|
179
|
+
// Note(TB): The visual buttons are hidden from screen
|
|
180
|
+
// readers, so they need to be identified as hidden;
|
|
181
|
+
// the visual button has the aria attributes
|
|
182
|
+
screen.getAllByRole("button", {hidden: true}).forEach((r) => {
|
|
183
|
+
expect(r).toHaveAttribute("aria-disabled", "true");
|
|
184
|
+
});
|
|
185
|
+
// Note(TB): Confirms the screen reader only
|
|
186
|
+
// radio options are also disabled
|
|
187
|
+
screen.getAllByRole("radio").forEach((r) => {
|
|
188
|
+
expect(r).toHaveAttribute("disabled");
|
|
117
189
|
});
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
);
|
|
118
193
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// Arrange
|
|
123
|
-
const {renderer} = renderQuestion(question, apiOptions);
|
|
194
|
+
it("should be able to navigate down by keyboard", () => {
|
|
195
|
+
// Arrange
|
|
196
|
+
renderQuestion(question, apiOptions);
|
|
124
197
|
|
|
125
|
-
|
|
126
|
-
|
|
198
|
+
// Act
|
|
199
|
+
userEvent.tab();
|
|
200
|
+
userEvent.tab();
|
|
127
201
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
202
|
+
// Assert
|
|
203
|
+
// Note(TB): The visual buttons are hidden from screen readers
|
|
204
|
+
// so they need to be identified as hidden;
|
|
205
|
+
// cannot access screen reader buttons via tabbing
|
|
206
|
+
expect(screen.getAllByRole("button", {hidden: true})[1]).toHaveFocus();
|
|
207
|
+
});
|
|
132
208
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
reviewMode,
|
|
137
|
-
});
|
|
209
|
+
it("should be able to navigate up by keyboard", () => {
|
|
210
|
+
// Arrange
|
|
211
|
+
renderQuestion(question, apiOptions);
|
|
138
212
|
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
// Act
|
|
214
|
+
userEvent.tab();
|
|
215
|
+
// Note(TB): The visual buttons are hidden from screen readers
|
|
216
|
+
// so they need to be identified as hidden
|
|
217
|
+
expect(screen.getAllByRole("button", {hidden: true})[0]).toHaveFocus();
|
|
141
218
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
219
|
+
userEvent.tab();
|
|
220
|
+
expect(screen.getAllByRole("button", {hidden: true})[1]).toHaveFocus();
|
|
145
221
|
|
|
146
|
-
|
|
147
|
-
// Arrange
|
|
148
|
-
const {renderer} = renderQuestion(question, apiOptions, {
|
|
149
|
-
reviewMode,
|
|
150
|
-
});
|
|
222
|
+
userEvent.tab({shift: true});
|
|
151
223
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
selectOption(incorrect[0]);
|
|
156
|
-
renderer.deselectIncorrectSelectedChoices();
|
|
224
|
+
// Assert
|
|
225
|
+
expect(screen.getAllByRole("button", {hidden: true})[0]).toHaveFocus();
|
|
226
|
+
});
|
|
157
227
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
});
|
|
228
|
+
it("should be able to select an option by keyboard", () => {
|
|
229
|
+
// Arrange
|
|
230
|
+
renderQuestion(question, apiOptions);
|
|
163
231
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
...question,
|
|
168
|
-
widgets: {
|
|
169
|
-
...question.widgets,
|
|
170
|
-
"radio 1": {
|
|
171
|
-
...question.widgets["radio 1"],
|
|
172
|
-
static: true,
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
renderQuestion(staticQuestion, apiOptions, {reviewMode});
|
|
232
|
+
// Act
|
|
233
|
+
userEvent.tab();
|
|
234
|
+
userEvent.keyboard("{space}");
|
|
177
235
|
|
|
178
|
-
|
|
179
|
-
|
|
236
|
+
// Assert
|
|
237
|
+
expect(screen.getAllByRole("radio")[0]).toBeChecked();
|
|
238
|
+
});
|
|
180
239
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
240
|
+
it("should be able to navigate to 'None of the above' choice by keyboard", () => {
|
|
241
|
+
// Arrange
|
|
242
|
+
const q = clone(question);
|
|
243
|
+
((q.widgets["radio 1"]
|
|
244
|
+
.options: any): PerseusRadioWidgetOptions).choices[3].isNoneOfTheAbove = true;
|
|
245
|
+
renderQuestion(q, apiOptions);
|
|
246
|
+
|
|
247
|
+
// Act
|
|
248
|
+
userEvent.tab();
|
|
249
|
+
userEvent.tab();
|
|
250
|
+
userEvent.tab();
|
|
251
|
+
|
|
252
|
+
// Assert
|
|
253
|
+
// Note(TB): The visual buttons are hidden from screen readers
|
|
254
|
+
// so they need to be identified as hidden
|
|
255
|
+
expect(screen.getAllByRole("button", {hidden: true})[2]).toHaveFocus();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it.each([
|
|
259
|
+
["No", "Yes"],
|
|
260
|
+
["False", "True"],
|
|
261
|
+
])("should enforce ordering for common answers: %j", (...answers) => {
|
|
262
|
+
// Arrange
|
|
263
|
+
const q = clone(question);
|
|
264
|
+
((q.widgets["radio 1"]
|
|
265
|
+
.options: any): PerseusRadioWidgetOptions).choices = answers.map(
|
|
266
|
+
(answer, idx) => ({
|
|
267
|
+
content: answer,
|
|
268
|
+
correct: idx === 1, // Correct answer is the "truthy" item
|
|
269
|
+
}),
|
|
198
270
|
);
|
|
199
271
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
272
|
+
// Act
|
|
273
|
+
const {renderer} = renderQuestion(q, apiOptions);
|
|
274
|
+
// We click on the first item, which was the second (index == 1)
|
|
275
|
+
// item in the original choices. But because of enforced ordering,
|
|
276
|
+
// it is now at the top of the list (and thus our correct answer).
|
|
277
|
+
userEvent.click(screen.getAllByRole("radio")[0]);
|
|
203
278
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
279
|
+
// Assert
|
|
280
|
+
const items = screen.getAllByRole("listitem");
|
|
281
|
+
expect(items[0]).toHaveTextContent(answers[1]);
|
|
282
|
+
expect(items[1]).toHaveTextContent(answers[0]);
|
|
283
|
+
expect(renderer).toHaveBeenAnsweredCorrectly();
|
|
284
|
+
});
|
|
207
285
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
|
|
286
|
+
it("should not change ordering of non-common answers", () => {
|
|
287
|
+
// Arrange
|
|
288
|
+
const answers = ["Last", "First"];
|
|
289
|
+
const q = clone(question);
|
|
290
|
+
((q.widgets["radio 1"]
|
|
291
|
+
.options: any): PerseusRadioWidgetOptions).choices = answers.map(
|
|
292
|
+
(answer, idx) => ({
|
|
293
|
+
content: answer,
|
|
294
|
+
correct: idx === 1,
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
216
297
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
renderQuestion(question, apiOptions);
|
|
298
|
+
// Act
|
|
299
|
+
renderQuestion(q, apiOptions);
|
|
220
300
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
screen.getAllByRole("button", {hidden: true})[0],
|
|
227
|
-
).toHaveFocus();
|
|
301
|
+
// Assert
|
|
302
|
+
const items = screen.getAllByRole("listitem");
|
|
303
|
+
expect(items[0]).toHaveTextContent(answers[0]);
|
|
304
|
+
expect(items[1]).toHaveTextContent(answers[1]);
|
|
305
|
+
});
|
|
228
306
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
307
|
+
it("should transform inline passage-refs to references to passage widgets", () => {
|
|
308
|
+
// Arrange
|
|
309
|
+
|
|
310
|
+
// Add a passage widget to the question content and then reference it
|
|
311
|
+
// in an answer.
|
|
312
|
+
const questionWithPassage = clone(question);
|
|
313
|
+
questionWithPassage.content = `[[\u2603 passage 1]]\n\nNow what's the answer?\n\n[[\u2603 radio 1]]`;
|
|
314
|
+
questionWithPassage.widgets["passage 1"] = passageWidget;
|
|
315
|
+
const radioOptions = questionWithPassage.widgets["radio 1"];
|
|
316
|
+
// HACK(jeremy): Flow doesn't know what type these options are because
|
|
317
|
+
// we've extracted them out of a generic `PerseusJson` blob and all the
|
|
318
|
+
// `widget` items are just type `Widget`.
|
|
319
|
+
(radioOptions.options: any).choices[0].content =
|
|
320
|
+
'{{passage-ref 1 1 "Reference 1 here"}}';
|
|
321
|
+
|
|
322
|
+
// Act
|
|
323
|
+
renderQuestion(questionWithPassage, apiOptions);
|
|
324
|
+
// Passage refs use `_.defer()` to update their reference ranges.
|
|
325
|
+
jest.runOnlyPendingTimers();
|
|
326
|
+
|
|
327
|
+
// Assert
|
|
328
|
+
// By using a `passage-ref` in a choice, we should now have the
|
|
329
|
+
// reference on the radio we used it on.
|
|
330
|
+
// NOTE: We get by listitem role here because the 'radio' role
|
|
331
|
+
// element does not contain the HTML that provides the label
|
|
332
|
+
// for it.
|
|
333
|
+
const passageRefRadio = screen.getAllByRole("listitem")[0];
|
|
334
|
+
expect(passageRefRadio).toHaveTextContent("lines NaN–NaN");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should render rationales for selected choices", () => {
|
|
338
|
+
// Arrange
|
|
339
|
+
const {renderer} = renderQuestion(question, apiOptions);
|
|
340
|
+
|
|
341
|
+
// Act
|
|
342
|
+
selectOption(incorrect[0]);
|
|
343
|
+
renderer.showRationalesForCurrentlySelectedChoices();
|
|
233
344
|
|
|
234
|
-
|
|
345
|
+
// Assert
|
|
346
|
+
expect(
|
|
347
|
+
screen.queryAllByTestId(
|
|
348
|
+
`perseus-radio-rationale-content-${incorrect[0]}`,
|
|
349
|
+
),
|
|
350
|
+
).toHaveLength(1);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("cross-out is enabled", () => {
|
|
354
|
+
const crossOutApiOptions = {
|
|
355
|
+
...apiOptions,
|
|
356
|
+
crossOutEnabled: true,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
it("should render cross-out menu button", () => {
|
|
360
|
+
// Arrange & Act
|
|
361
|
+
renderQuestion(question, crossOutApiOptions);
|
|
235
362
|
|
|
236
363
|
// Assert
|
|
237
364
|
expect(
|
|
238
|
-
screen.
|
|
239
|
-
|
|
365
|
+
screen.getByRole("button", {
|
|
366
|
+
name: /Open menu for Choice B/,
|
|
367
|
+
}),
|
|
368
|
+
).toBeVisible();
|
|
240
369
|
});
|
|
241
370
|
|
|
242
|
-
it("should
|
|
371
|
+
it("should open the cross-out menu when button clicked", async () => {
|
|
243
372
|
// Arrange
|
|
244
|
-
renderQuestion(question,
|
|
373
|
+
renderQuestion(question, crossOutApiOptions);
|
|
245
374
|
|
|
246
375
|
// Act
|
|
247
|
-
userEvent.
|
|
248
|
-
|
|
376
|
+
userEvent.click(
|
|
377
|
+
screen.getByRole("button", {
|
|
378
|
+
name: /Open menu for Choice B/,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
await act(async () => {
|
|
382
|
+
await jest.runAllTimers();
|
|
383
|
+
});
|
|
249
384
|
|
|
250
385
|
// Assert
|
|
251
|
-
expect(
|
|
386
|
+
expect(
|
|
387
|
+
screen.getByRole("button", {
|
|
388
|
+
name: /Cross out Choice B/,
|
|
389
|
+
}),
|
|
390
|
+
).toBeVisible();
|
|
252
391
|
});
|
|
253
392
|
|
|
254
|
-
it("should
|
|
393
|
+
it("should open the cross-out menu when focused and spacebar pressed", async () => {
|
|
255
394
|
// Arrange
|
|
256
|
-
|
|
257
|
-
(
|
|
258
|
-
|
|
259
|
-
renderQuestion(q, apiOptions);
|
|
395
|
+
renderQuestion(question, crossOutApiOptions);
|
|
396
|
+
userEvent.tab(); // Choice icon
|
|
397
|
+
userEvent.tab(); // Cross-out menu ellipsis
|
|
260
398
|
|
|
261
399
|
// Act
|
|
262
|
-
userEvent.
|
|
263
|
-
|
|
264
|
-
|
|
400
|
+
userEvent.keyboard("{space}");
|
|
401
|
+
await act(async () => {
|
|
402
|
+
await jest.runAllTimers();
|
|
403
|
+
});
|
|
265
404
|
|
|
266
405
|
// Assert
|
|
267
|
-
// Note(TB): The visual buttons are hidden from screen readers
|
|
268
|
-
// so they need to be identified as hidden
|
|
269
406
|
expect(
|
|
270
|
-
screen.
|
|
271
|
-
|
|
407
|
+
screen.getByRole("button", {
|
|
408
|
+
name: /Cross out Choice A/,
|
|
409
|
+
}),
|
|
410
|
+
).toBeVisible();
|
|
272
411
|
});
|
|
273
412
|
|
|
274
|
-
it
|
|
275
|
-
["No", "Yes"],
|
|
276
|
-
["False", "True"],
|
|
277
|
-
])("should enforce ordering for common answers: %j", (...answers) => {
|
|
413
|
+
it("should cross-out selection and dismiss button when clicked", () => {
|
|
278
414
|
// Arrange
|
|
279
|
-
|
|
280
|
-
(
|
|
281
|
-
.
|
|
282
|
-
|
|
283
|
-
content: answer,
|
|
284
|
-
correct: idx === 1, // Correct answer is the "truthy" item
|
|
415
|
+
renderQuestion(question, crossOutApiOptions);
|
|
416
|
+
userEvent.click(
|
|
417
|
+
screen.getByRole("button", {
|
|
418
|
+
name: /Open menu for Choice B/,
|
|
285
419
|
}),
|
|
286
420
|
);
|
|
287
421
|
|
|
288
422
|
// Act
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
423
|
+
userEvent.click(
|
|
424
|
+
screen.getByRole("button", {
|
|
425
|
+
name: /Cross out Choice B/,
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
294
428
|
|
|
295
429
|
// Assert
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
430
|
+
expect(
|
|
431
|
+
screen.queryAllByRole("button", {
|
|
432
|
+
name: /Cross out Choice B/,
|
|
433
|
+
}),
|
|
434
|
+
).toHaveLength(0);
|
|
435
|
+
|
|
436
|
+
expect(
|
|
437
|
+
screen.getByTestId("choice-icon__cross-out-line"),
|
|
438
|
+
).toBeVisible();
|
|
300
439
|
});
|
|
301
440
|
|
|
302
|
-
it("should
|
|
441
|
+
it("should remove cross-out line on selection", async () => {
|
|
303
442
|
// Arrange
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
(answer, idx) => ({
|
|
309
|
-
content: answer,
|
|
310
|
-
correct: idx === 1,
|
|
443
|
+
renderQuestion(question, crossOutApiOptions);
|
|
444
|
+
userEvent.click(
|
|
445
|
+
screen.getByRole("button", {
|
|
446
|
+
name: /Open menu for Choice B/,
|
|
311
447
|
}),
|
|
312
448
|
);
|
|
313
449
|
|
|
314
450
|
// Act
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("should transform inline passage-refs to references to passage widgets", () => {
|
|
324
|
-
// Arrange
|
|
325
|
-
|
|
326
|
-
// Add a passage widget to the question content and then reference it
|
|
327
|
-
// in an answer.
|
|
328
|
-
const questionWithPassage = clone(question);
|
|
329
|
-
questionWithPassage.content = `[[\u2603 passage 1]]\n\nNow what's the answer?\n\n[[\u2603 radio 1]]`;
|
|
330
|
-
questionWithPassage.widgets["passage 1"] = passageWidget;
|
|
331
|
-
const radioOptions = questionWithPassage.widgets["radio 1"];
|
|
332
|
-
// HACK(jeremy): Flow doesn't know what type these options are because
|
|
333
|
-
// we've extracted them out of a generic `PerseusJson` blob and all the
|
|
334
|
-
// `widget` items are just type `Widget`.
|
|
335
|
-
(radioOptions.options: any).choices[0].content =
|
|
336
|
-
'{{passage-ref 1 1 "Reference 1 here"}}';
|
|
451
|
+
userEvent.click(
|
|
452
|
+
screen.getByRole("button", {
|
|
453
|
+
name: /Cross out Choice B/,
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
jest.runAllTimers();
|
|
337
457
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
458
|
+
userEvent.click(
|
|
459
|
+
screen.getByRole("radio", {
|
|
460
|
+
name: "(Choice B, Crossed out) -8",
|
|
461
|
+
}),
|
|
462
|
+
);
|
|
342
463
|
|
|
343
464
|
// Assert
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// element does not contain the HTML that provides the label
|
|
348
|
-
// for it.
|
|
349
|
-
const passageRefRadio = screen.getAllByRole("listitem")[0];
|
|
350
|
-
expect(passageRefRadio).toHaveTextContent("lines NaN–NaN");
|
|
465
|
+
expect(
|
|
466
|
+
screen.queryByTestId("choice-icon__cross-out-line"),
|
|
467
|
+
).not.toBeInTheDocument();
|
|
351
468
|
});
|
|
352
469
|
|
|
353
|
-
it("should
|
|
470
|
+
it("should dismiss cross-out button with {tab} key", async () => {
|
|
354
471
|
// Arrange
|
|
355
|
-
|
|
472
|
+
renderQuestion(question, crossOutApiOptions);
|
|
473
|
+
userEvent.tab(); // Choice icon
|
|
474
|
+
userEvent.tab(); // Cross-out menu ellipsis
|
|
356
475
|
|
|
357
476
|
// Act
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// Assert
|
|
362
|
-
expect(
|
|
363
|
-
screen.queryAllByTestId(
|
|
364
|
-
`perseus-radio-rationale-content-${incorrect[0]}`,
|
|
365
|
-
),
|
|
366
|
-
).toHaveLength(1);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe("cross-out is enabled", () => {
|
|
370
|
-
const crossOutApiOptions = {
|
|
371
|
-
...apiOptions,
|
|
372
|
-
crossOutEnabled: true,
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
it("should render cross-out menu button", () => {
|
|
376
|
-
// Arrange & Act
|
|
377
|
-
renderQuestion(question, crossOutApiOptions);
|
|
378
|
-
|
|
379
|
-
// Assert
|
|
380
|
-
expect(
|
|
381
|
-
screen.getByRole("button", {
|
|
382
|
-
name: /Open menu for Choice B/,
|
|
383
|
-
}),
|
|
384
|
-
).toBeVisible();
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it("should open the cross-out menu when button clicked", async () => {
|
|
388
|
-
// Arrange
|
|
389
|
-
renderQuestion(question, crossOutApiOptions);
|
|
390
|
-
|
|
391
|
-
// Act
|
|
392
|
-
userEvent.click(
|
|
393
|
-
screen.getByRole("button", {
|
|
394
|
-
name: /Open menu for Choice B/,
|
|
395
|
-
}),
|
|
396
|
-
);
|
|
397
|
-
await act(async () => {
|
|
398
|
-
await jest.runAllTimers();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// Assert
|
|
402
|
-
expect(
|
|
403
|
-
screen.getByRole("button", {
|
|
404
|
-
name: /Cross out Choice B/,
|
|
405
|
-
}),
|
|
406
|
-
).toBeVisible();
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it("should open the cross-out menu when focused and spacebar pressed", async () => {
|
|
410
|
-
// Arrange
|
|
411
|
-
renderQuestion(question, crossOutApiOptions);
|
|
412
|
-
userEvent.tab(); // Choice icon
|
|
413
|
-
userEvent.tab(); // Cross-out menu ellipsis
|
|
414
|
-
|
|
415
|
-
// Act
|
|
416
|
-
userEvent.keyboard("{space}");
|
|
417
|
-
await act(async () => {
|
|
418
|
-
await jest.runAllTimers();
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Assert
|
|
422
|
-
expect(
|
|
423
|
-
screen.getByRole("button", {
|
|
424
|
-
name: /Cross out Choice A/,
|
|
425
|
-
}),
|
|
426
|
-
).toBeVisible();
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it("should cross-out selection and dismiss button when clicked", () => {
|
|
430
|
-
// Arrange
|
|
431
|
-
renderQuestion(question, crossOutApiOptions);
|
|
432
|
-
userEvent.click(
|
|
433
|
-
screen.getByRole("button", {
|
|
434
|
-
name: /Open menu for Choice B/,
|
|
435
|
-
}),
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
// Act
|
|
439
|
-
userEvent.click(
|
|
440
|
-
screen.getByRole("button", {
|
|
441
|
-
name: /Cross out Choice B/,
|
|
442
|
-
}),
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
// Assert
|
|
446
|
-
expect(
|
|
447
|
-
screen.queryAllByRole("button", {
|
|
448
|
-
name: /Cross out Choice B/,
|
|
449
|
-
}),
|
|
450
|
-
).toHaveLength(0);
|
|
451
|
-
|
|
452
|
-
expect(
|
|
453
|
-
screen.getByTestId("choice-icon__cross-out-line"),
|
|
454
|
-
).toBeVisible();
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it("should remove cross-out line on selection", async () => {
|
|
458
|
-
// Arrange
|
|
459
|
-
renderQuestion(question, crossOutApiOptions);
|
|
460
|
-
userEvent.click(
|
|
461
|
-
screen.getByRole("button", {
|
|
462
|
-
name: /Open menu for Choice B/,
|
|
463
|
-
}),
|
|
464
|
-
);
|
|
465
|
-
|
|
466
|
-
// Act
|
|
467
|
-
userEvent.click(
|
|
468
|
-
screen.getByRole("button", {
|
|
469
|
-
name: /Cross out Choice B/,
|
|
470
|
-
}),
|
|
471
|
-
);
|
|
472
|
-
jest.runAllTimers();
|
|
473
|
-
|
|
474
|
-
userEvent.click(
|
|
475
|
-
screen.getByRole("radio", {
|
|
476
|
-
name: "(Choice B, Crossed out) -8",
|
|
477
|
-
}),
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
// Assert
|
|
481
|
-
expect(
|
|
482
|
-
screen.queryByTestId("choice-icon__cross-out-line"),
|
|
483
|
-
).not.toBeInTheDocument();
|
|
477
|
+
userEvent.keyboard("{space}");
|
|
478
|
+
await act(async () => {
|
|
479
|
+
await jest.runAllTimers();
|
|
484
480
|
});
|
|
485
481
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
// Act
|
|
493
|
-
userEvent.keyboard("{space}");
|
|
494
|
-
await act(async () => {
|
|
495
|
-
await jest.runAllTimers();
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
expect(
|
|
499
|
-
screen.getByRole("button", {
|
|
500
|
-
name: /Cross out Choice A/,
|
|
501
|
-
}),
|
|
502
|
-
).toBeVisible();
|
|
482
|
+
expect(
|
|
483
|
+
screen.getByRole("button", {
|
|
484
|
+
name: /Cross out Choice A/,
|
|
485
|
+
}),
|
|
486
|
+
).toBeVisible();
|
|
503
487
|
|
|
504
|
-
|
|
505
|
-
|
|
488
|
+
userEvent.keyboard("{space}");
|
|
489
|
+
jest.runAllTimers();
|
|
506
490
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
});
|
|
491
|
+
// Assert
|
|
492
|
+
expect(
|
|
493
|
+
screen.queryAllByRole("button", {
|
|
494
|
+
name: /Cross out Choice A/,
|
|
495
|
+
}),
|
|
496
|
+
).toHaveLength(0);
|
|
514
497
|
});
|
|
515
498
|
});
|
|
516
499
|
|
|
517
|
-
it("should not display instructions when satStyling true", () => {
|
|
518
|
-
// Arrange
|
|
519
|
-
const apiOptions: APIOptions = {
|
|
520
|
-
satStyling: true,
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Act
|
|
524
|
-
renderQuestion(question, apiOptions);
|
|
525
|
-
|
|
526
|
-
// Assert
|
|
527
|
-
expect(screen.queryByText("Choose 1 answer:")).not.toBeInTheDocument();
|
|
528
|
-
});
|
|
529
|
-
|
|
530
500
|
it("should be invalid when first rendered", () => {
|
|
531
501
|
// Arrange
|
|
532
502
|
const apiOptions: APIOptions = {
|
|
@@ -627,9 +597,7 @@ describe("multi-choice question", () => {
|
|
|
627
597
|
const [question, correct, incorrect, invalid] =
|
|
628
598
|
multiChoiceQuestionAndAnswer;
|
|
629
599
|
|
|
630
|
-
const apiOptions: APIOptions = {
|
|
631
|
-
satStyling: false,
|
|
632
|
-
};
|
|
600
|
+
const apiOptions: APIOptions = Object.freeze({});
|
|
633
601
|
|
|
634
602
|
beforeEach(() => {
|
|
635
603
|
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
|