@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
@@ -1,7 +1,15 @@
1
+ import {
2
+ arrayOfLength,
3
+ randomBoolean,
4
+ randomInteger,
5
+ randomSentence,
6
+ } from "./randomizers";
7
+
1
8
  import type {
2
9
  PerseusRenderer,
3
10
  RadioWidget,
4
11
  PassageWidget,
12
+ PerseusRadioChoice,
5
13
  } from "../../perseus-types";
6
14
 
7
15
  export const question: PerseusRenderer = {
@@ -259,3 +267,58 @@ export const multiChoiceQuestionAndAnswer: [
259
267
  [0, 1, 2, 3],
260
268
  ],
261
269
  ];
270
+
271
+ export const randomRadioGenerator = (): PerseusRenderer => {
272
+ const randomChoice = (
273
+ isMultiSelect: boolean,
274
+ isCorrect: boolean,
275
+ isNoneOfTheAbove: boolean,
276
+ ): PerseusRadioChoice => {
277
+ return {
278
+ content: randomSentence(12),
279
+ isNoneOfTheAbove: isNoneOfTheAbove,
280
+ correct: isMultiSelect ? randomBoolean() : isCorrect,
281
+ };
282
+ };
283
+ const numberOfChoices = randomInteger(2, 6);
284
+
285
+ const isMultiSelect = randomBoolean();
286
+ // only used if not multi select
287
+ const correctIndex = randomInteger(0, numberOfChoices);
288
+
289
+ const containsNoneOfTheAbove = randomBoolean();
290
+ // only used if containsNoneOfTheAbove
291
+ const noneOfTheAboveIndex = randomInteger(0, numberOfChoices);
292
+
293
+ return {
294
+ content: `${randomSentence(30)}\n\n[[\u2603 radio 1]]`,
295
+ images: {},
296
+ widgets: {
297
+ "radio 1": {
298
+ graded: randomBoolean(),
299
+ version: {
300
+ major: 1,
301
+ minor: 0,
302
+ },
303
+ static: randomBoolean(0.05),
304
+ type: "radio",
305
+ options: {
306
+ onePerLine: randomBoolean(),
307
+ displayCount: null,
308
+ choices: arrayOfLength(numberOfChoices).map((_, i) =>
309
+ randomChoice(
310
+ isMultiSelect,
311
+ correctIndex === i,
312
+ containsNoneOfTheAbove && noneOfTheAboveIndex === i,
313
+ ),
314
+ ),
315
+ hasNoneOfTheAbove: randomBoolean(),
316
+ multipleSelect: isMultiSelect,
317
+ randomize: randomBoolean(),
318
+ deselectEnabled: randomBoolean(),
319
+ },
320
+ alignment: "default",
321
+ } as RadioWidget,
322
+ },
323
+ };
324
+ };
@@ -0,0 +1,51 @@
1
+ // Helpers to generate random test data for widgets
2
+
3
+ // Returns a random letter inclusive of emoji and japanese characters
4
+ export const randomLetter = (): string => {
5
+ const alphabet = "abcdefghijklmnopqrstuvwxyz".split("");
6
+ const emojis = ["🍇", "🍈", "🍌"];
7
+ const japaneseCharacters = ["ツ", "デ", "ト", "ド", "ナ", "ぁ", "あ"];
8
+ const characters = alphabet.concat(emojis).concat(japaneseCharacters);
9
+ const letter = randomInteger(0, characters.length - 1);
10
+ return characters[letter];
11
+ };
12
+
13
+ // Returns a random word 3-20 characters long composed of the random letter function
14
+ export const randomWord = (): string => {
15
+ // words are 3 - 20 characters
16
+ const numLetters = randomInteger(3, 11);
17
+ return arrayOfLength(numLetters).map(randomLetter).join("");
18
+ };
19
+
20
+ // Returns a random sentence of a specified number of words, using the random word function
21
+ export const randomSentence = (maxWords: number): string => {
22
+ const numWords = randomInteger(5, Math.max(maxWords, 5));
23
+ return arrayOfLength(numWords).map(randomWord).join(" ");
24
+ };
25
+
26
+ // generates a boolean. 50/50 unless rateTrue is specified
27
+ export const randomBoolean = (rateTrue?: number): boolean => {
28
+ let threshold = rateTrue;
29
+ if (threshold == null) {
30
+ threshold = 0.5;
31
+ }
32
+ return Math.random() < threshold;
33
+ };
34
+
35
+ // generates an integer inclusive of the specified min and max
36
+ export const randomInteger = (min: number, max: number): number => {
37
+ return Math.floor(Math.random() * (max + 1 - min)) + min;
38
+ };
39
+
40
+ // A helper to create an array of a certain length.
41
+ export const arrayOfLength = (length: number): number[] => {
42
+ return Array.from(Array(length).keys());
43
+ };
44
+
45
+ // Returns a random element from a provided list
46
+ export function randomElement<T>(items: Array<T>): T | undefined {
47
+ if (items.length > 0) {
48
+ return items[randomInteger(0, items.length - 1)];
49
+ }
50
+ return undefined;
51
+ }
@@ -40,18 +40,10 @@ exports[`Dropdown widget should snapshot when opened: dropdown open 1`] = `
40
40
  >
41
41
  greater/less than or equal to
42
42
  </span>
43
- <svg
43
+ <span
44
44
  aria-hidden="true"
45
- class="svg_aeiopc-o_O-caret_ntkmq"
46
- height="16"
47
- viewBox="0 0 16 16"
48
- width="16"
49
- >
50
- <path
51
- 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"
52
- fill="rgba(33,36,44,0.64)"
53
- />
54
- </svg>
45
+ class="svg_aeiopc-o_O-caret_ntkmq-o_O-inlineStyles_13xtep5"
46
+ />
55
47
  </button>
56
48
  </div>
57
49
  </div>
@@ -113,18 +105,10 @@ exports[`Dropdown widget should snapshot: initial render 1`] = `
113
105
  >
114
106
  greater/less than or equal to
115
107
  </span>
116
- <svg
108
+ <span
117
109
  aria-hidden="true"
118
- class="svg_aeiopc-o_O-caret_ntkmq"
119
- height="16"
120
- viewBox="0 0 16 16"
121
- width="16"
122
- >
123
- <path
124
- 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"
125
- fill="rgba(33,36,44,0.64)"
126
- />
127
- </svg>
110
+ class="svg_aeiopc-o_O-caret_ntkmq-o_O-inlineStyles_13xtep5"
111
+ />
128
112
  </button>
129
113
  </div>
130
114
  </div>
@@ -242,7 +242,7 @@ exports[`graded-group-set should render all graded groups 1`] = `
242
242
  type="button"
243
243
  >
244
244
  <span
245
- class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_m8a8ki"
245
+ class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_6kf3xs"
246
246
  >
247
247
  Check
248
248
  </span>
@@ -470,7 +470,7 @@ exports[`graded-group-set should render all graded groups 1`] = `
470
470
  type="button"
471
471
  >
472
472
  <span
473
- class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_m8a8ki"
473
+ class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_6kf3xs"
474
474
  >
475
475
  Check
476
476
  </span>
@@ -698,7 +698,7 @@ exports[`graded-group-set should render all graded groups 1`] = `
698
698
  type="button"
699
699
  >
700
700
  <span
701
- class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_m8a8ki"
701
+ class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_6kf3xs"
702
702
  >
703
703
  Check
704
704
  </span>
@@ -308,7 +308,7 @@ exports[`graded group widget should snapshot 1`] = `
308
308
  type="button"
309
309
  >
310
310
  <span
311
- class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_m8a8ki"
311
+ class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_6kf3xs"
312
312
  >
313
313
  Check
314
314
  </span>
@@ -443,7 +443,7 @@ exports[`graded-group should snapshot: initial render (mobile: false) 1`] = `
443
443
  type="button"
444
444
  >
445
445
  <span
446
- class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_m8a8ki"
446
+ class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_6kf3xs"
447
447
  >
448
448
  Check
449
449
  </span>
@@ -3638,7 +3638,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
3638
3638
  </button>
3639
3639
  </div>
3640
3640
  <div
3641
- class="perseus-radio-rationale-content rationale_1w3tmyh"
3641
+ class="perseus-radio-rationale-content rationale_1ctj0s0"
3642
3642
  data-test-id="perseus-radio-rationale-content-0"
3643
3643
  >
3644
3644
  <div
@@ -3834,7 +3834,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
3834
3834
  </button>
3835
3835
  </div>
3836
3836
  <div
3837
- class="perseus-radio-rationale-content rationale_1w3tmyh"
3837
+ class="perseus-radio-rationale-content rationale_1ctj0s0"
3838
3838
  data-test-id="perseus-radio-rationale-content-1"
3839
3839
  >
3840
3840
  <div
@@ -4014,7 +4014,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
4014
4014
  </button>
4015
4015
  </div>
4016
4016
  <div
4017
- class="perseus-radio-rationale-content rationale_1w3tmyh"
4017
+ class="perseus-radio-rationale-content rationale_1ctj0s0"
4018
4018
  data-test-id="perseus-radio-rationale-content-2"
4019
4019
  >
4020
4020
  <div
@@ -4195,7 +4195,7 @@ exports[`single-choice question reviewMode: true should snapshot the same: first
4195
4195
  </button>
4196
4196
  </div>
4197
4197
  <div
4198
- class="perseus-radio-rationale-content rationale_1w3tmyh"
4198
+ class="perseus-radio-rationale-content rationale_1ctj0s0"
4199
4199
  data-test-id="perseus-radio-rationale-content-3"
4200
4200
  >
4201
4201
  <div
@@ -60,7 +60,6 @@ function KeypadWithContext() {
60
60
  onElementMounted={setKeypadElement}
61
61
  onDismiss={() => {}}
62
62
  onAnalyticsEvent={async () => {}}
63
- useV2Keypad
64
63
  />
65
64
  );
66
65
  }}
@@ -9,7 +9,7 @@ import {question1} from "../__testdata__/matcher.testdata";
9
9
  import {renderQuestion} from "./renderQuestion";
10
10
 
11
11
  import type {APIOptions} from "../../types";
12
- import type {Matcher} from "enzyme-matchers/lib/types/Matcher";
12
+ import type {Matcher} from "../matcher";
13
13
 
14
14
  describe("matcher widget", () => {
15
15
  beforeEach(() => {
@@ -0,0 +1,53 @@
1
+ import {
2
+ arrayOfLength,
3
+ randomBoolean,
4
+ randomElement,
5
+ randomLetter,
6
+ randomSentence,
7
+ randomWord,
8
+ } from "../__testdata__/randomizers";
9
+
10
+ describe("randomizers", () => {
11
+ describe("randomLetter", () => {
12
+ it("returns one letter", () => {
13
+ // can't check for just one character because some non english chars are longer than 1 char
14
+ expect(typeof randomLetter()).toBe("string");
15
+ });
16
+ });
17
+
18
+ describe("randomWord", () => {
19
+ it("returns a word of at least 3 letters", () => {
20
+ expect(randomWord().length).toBeGreaterThanOrEqual(3);
21
+ });
22
+ });
23
+
24
+ describe("randomSentence", () => {
25
+ it("returns a sentence of at least 5 words", () => {
26
+ expect(randomSentence(10).split(" ").length).toBeGreaterThanOrEqual(
27
+ 5,
28
+ );
29
+ });
30
+ });
31
+
32
+ describe("randomBoolean", () => {
33
+ it("is true when rateTrue is 1", () => {
34
+ expect(randomBoolean(1)).toEqual(true);
35
+ });
36
+ it("is false when rateTrue is 0", () => {
37
+ expect(randomBoolean(0)).toEqual(false);
38
+ });
39
+ });
40
+
41
+ describe("arrayOfLength", () => {
42
+ it("returns an array of the specified length", () => {
43
+ expect(arrayOfLength(3).length).toEqual(3);
44
+ expect(arrayOfLength(12).length).toEqual(12);
45
+ });
46
+ });
47
+
48
+ describe("randomElement", () => {
49
+ it("returns one of the input array elements", () => {
50
+ expect([1, 2, 3]).toContain(randomElement([1, 2, 3]));
51
+ });
52
+ });
53
+ });
@@ -4,6 +4,7 @@ import classNames from "classnames";
4
4
  import * as React from "react";
5
5
  import _ from "underscore";
6
6
 
7
+ import AssetContext from "../asset-context";
7
8
  import SvgImage from "../components/svg-image";
8
9
  import * as Changeable from "../mixins/changeable";
9
10
  import Renderer from "../renderer";
@@ -93,11 +94,14 @@ class ImageWidget extends React.Component<Props> {
93
94
  const backgroundImage = this.props.backgroundImage;
94
95
 
95
96
  if (backgroundImage.url) {
97
+ const url = backgroundImage.url;
96
98
  image = (
97
- <SvgImage
98
- src={backgroundImage.url}
99
- alt={
100
- /* alt text is formatted in a sr-only
99
+ <AssetContext.Consumer>
100
+ {({setAssetStatus}) => (
101
+ <SvgImage
102
+ src={url}
103
+ alt={
104
+ /* alt text is formatted in a sr-only
101
105
  div next to the image in addition to
102
106
  the alt attribute.
103
107
  If there is no alt text at all,
@@ -117,22 +121,25 @@ class ImageWidget extends React.Component<Props> {
117
121
  in practice right now, although
118
122
  it will exhibit weird behaviour
119
123
  while editing. */
120
- this.props.alt
121
- }
122
- overrideAriaHidden={true}
123
- width={backgroundImage.width}
124
- height={backgroundImage.height}
125
- preloader={apiOptions.imagePreloader}
126
- extraGraphie={{
127
- box: this.props.box,
128
- range: this.props.range,
129
- labels: this.props.labels,
130
- }}
131
- trackInteraction={this.props.trackInteraction}
132
- zoomToFullSizeOnMobile={apiOptions.isMobile}
133
- constrainHeight={apiOptions.isMobile}
134
- allowFullBleed={apiOptions.isMobile}
135
- />
124
+ this.props.alt
125
+ }
126
+ overrideAriaHidden={true}
127
+ width={backgroundImage.width}
128
+ height={backgroundImage.height}
129
+ preloader={apiOptions.imagePreloader}
130
+ extraGraphie={{
131
+ box: this.props.box,
132
+ range: this.props.range,
133
+ labels: this.props.labels,
134
+ }}
135
+ trackInteraction={this.props.trackInteraction}
136
+ zoomToFullSizeOnMobile={apiOptions.isMobile}
137
+ constrainHeight={apiOptions.isMobile}
138
+ allowFullBleed={apiOptions.isMobile}
139
+ setAssetStatus={setAssetStatus}
140
+ />
141
+ )}
142
+ </AssetContext.Consumer>
136
143
  );
137
144
  }
138
145
 
@@ -28,7 +28,7 @@ type State = {
28
28
  texRendererLoaded: boolean;
29
29
  };
30
30
 
31
- class Matcher extends React.Component<any, any> {
31
+ export class Matcher extends React.Component<any, any> {
32
32
  static propTypes = {
33
33
  apiOptions: ApiOptions.propTypes,
34
34
  labels: PropTypes.array,
@@ -346,6 +346,7 @@ const styles = StyleSheet.create({
346
346
  padding: intermediateCheckboxPadding,
347
347
  paddingTop: 0,
348
348
  marginLeft: 54,
349
+ color: Color.offBlack64,
349
350
  [mediaQueries.smOrSmaller]: {
350
351
  padding: intermediateCheckboxPaddingPhone,
351
352
  paddingTop: 0,