@khanacademy/perseus 13.2.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.
@@ -0,0 +1,269 @@
1
+ import {
2
+ arrayOfLength,
3
+ randomBoolean,
4
+ randomElement,
5
+ randomInteger,
6
+ randomLetter,
7
+ randomWord,
8
+ } from "./randomizers";
9
+
10
+ import type {Coord} from "../../interactive2/types";
11
+ import type {
12
+ PerseusGraphType,
13
+ PerseusGraphTypeAngle,
14
+ PerseusGraphTypeCircle,
15
+ PerseusGraphTypeLinear,
16
+ PerseusGraphTypeLinearSystem,
17
+ PerseusGraphTypePoint,
18
+ PerseusGraphTypePolygon,
19
+ PerseusGraphTypeQuadratic,
20
+ PerseusGraphTypeRay,
21
+ PerseusGraphTypeSegment,
22
+ PerseusGraphTypeSinusoid,
23
+ PerseusImageBackground,
24
+ PerseusRenderer,
25
+ } from "../../perseus-types";
26
+
27
+ const randomCoord = (): Coord => {
28
+ return [randomInteger(1, 100), randomInteger(1, 100)];
29
+ };
30
+
31
+ const randomGraphTypeAngle = (): PerseusGraphTypeAngle => {
32
+ return {
33
+ type: "angle",
34
+ showAngles: randomBoolean(),
35
+ allowReflexAngles: randomBoolean(),
36
+ angleOffsetDeg: randomInteger(1, 90),
37
+ snapDegrees: randomInteger(1, 90),
38
+ match: "congruent",
39
+ // must be 3 coords for an angle graph
40
+ coords: arrayOfLength(3).map(randomCoord),
41
+ };
42
+ };
43
+
44
+ const randomGraphTypeCircle = (): PerseusGraphTypeCircle => {
45
+ return {
46
+ type: "circle",
47
+ center: randomCoord(),
48
+ radius: randomInteger(1, 20),
49
+ };
50
+ };
51
+
52
+ const randomGraphTypeLinear = (): PerseusGraphTypeLinear => {
53
+ return {
54
+ type: "linear",
55
+ coords: arrayOfLength(2).map(randomCoord),
56
+ };
57
+ };
58
+
59
+ const randomGraphTypeLinearSystem = (): PerseusGraphTypeLinearSystem => {
60
+ return {
61
+ type: "linear-system",
62
+ coords: arrayOfLength(2).map(() => arrayOfLength(2).map(randomCoord)),
63
+ };
64
+ };
65
+
66
+ const randomGraphTypePoint = (): PerseusGraphTypePoint => {
67
+ let minCoords = 1;
68
+ const numPoints = randomElement(["unlimited", randomInteger(1, 10)]);
69
+ if (typeof numPoints === "number") {
70
+ minCoords = numPoints;
71
+ }
72
+ return {
73
+ type: "point",
74
+ numPoints: typeof numPoints === "number" ? numPoints : "unlimited",
75
+ coords: arrayOfLength(randomInteger(minCoords, 10)).map(randomCoord),
76
+ };
77
+ };
78
+
79
+ const randomGraphTypePolygon = (): PerseusGraphTypePolygon => {
80
+ // Note Jhead - I saw this create at least one broken graph but didn't figure out why
81
+ return {
82
+ type: "polygon",
83
+ numSides: randomElement([
84
+ "unlimited",
85
+ undefined,
86
+ randomInteger(3, 30),
87
+ randomInteger(3, 30),
88
+ ]),
89
+ showAngles: randomBoolean(),
90
+ showSides: randomBoolean(),
91
+ snapTo: randomElement(["grid", "angles", "sides"]),
92
+ match: randomElement(["similar", "congruent", "approx", undefined]),
93
+ coords: arrayOfLength(randomInteger(3, 20)).map(randomCoord),
94
+ };
95
+ };
96
+
97
+ const randomGraphTypeQuadratic = (): PerseusGraphTypeQuadratic => {
98
+ return {
99
+ type: "quadratic",
100
+ coords: arrayOfLength(3).map(randomCoord),
101
+ };
102
+ };
103
+
104
+ const randomGraphTypeRay = (): PerseusGraphTypeRay => {
105
+ return {
106
+ type: "ray",
107
+ coords: arrayOfLength(2).map(randomCoord),
108
+ };
109
+ };
110
+
111
+ const randomGraphTypeSegment = (): PerseusGraphTypeSegment => {
112
+ const numSegments = randomInteger(1, 6);
113
+ return {
114
+ type: "segment",
115
+ numSegments,
116
+ coords: arrayOfLength(numSegments).map(() => [
117
+ randomCoord(),
118
+ randomCoord(),
119
+ ]),
120
+ };
121
+ };
122
+
123
+ const randomGraphTypeSinusoid = (): PerseusGraphTypeSinusoid => {
124
+ return {
125
+ type: "sinusoid",
126
+ coords: arrayOfLength(2).map(randomCoord),
127
+ };
128
+ };
129
+
130
+ export const randomBackgroundImage = (): PerseusImageBackground => {
131
+ const height = randomInteger(200, 500);
132
+ const width = randomInteger(200, 500);
133
+ const graphieUrl =
134
+ "web+graphie://ka-perseus-graphie.s3.amazonaws.com/a1b7a05c177742523250b64a3995c9b37aac3399";
135
+ const placeKittenUrl = `https://placekitten.com/g/${width}/${height}`;
136
+
137
+ return {
138
+ scale: randomInteger(8, 12) / 10,
139
+ bottom: randomInteger(0, 10) - 5,
140
+ url: randomElement([graphieUrl, placeKittenUrl, null]),
141
+ height,
142
+ width,
143
+ left: randomInteger(0, 10) - 5,
144
+ };
145
+ };
146
+
147
+ export const randomInteractiveGraphGenerator = (
148
+ type?: string,
149
+ ): PerseusRenderer => {
150
+ const randomRange = (): [number, number] => {
151
+ const val1 = randomInteger(0, 400) - 200;
152
+ const val2 = randomInteger(0, 400) - 200;
153
+ if (val1 < val2) {
154
+ return [val1, val2];
155
+ }
156
+ return [val2, val1];
157
+ };
158
+
159
+ const graphType =
160
+ type ||
161
+ randomElement([
162
+ "angle",
163
+ "circle",
164
+ "linear",
165
+ "linear-system",
166
+ "point",
167
+ "polygon",
168
+ "quadratic",
169
+ "ray",
170
+ "segment",
171
+ "sinusoid",
172
+ ]);
173
+
174
+ let graph: PerseusGraphType;
175
+ let correct: PerseusGraphType;
176
+
177
+ switch (graphType) {
178
+ case "angle": {
179
+ graph = randomGraphTypeAngle();
180
+ correct = randomGraphTypeAngle();
181
+ break;
182
+ }
183
+ case "circle": {
184
+ graph = randomGraphTypeCircle();
185
+ correct = randomGraphTypeCircle();
186
+ break;
187
+ }
188
+ case "linear": {
189
+ graph = randomGraphTypeLinear();
190
+ correct = randomGraphTypeLinear();
191
+ break;
192
+ }
193
+ case "linear-system": {
194
+ graph = randomGraphTypeLinearSystem();
195
+ correct = randomGraphTypeLinearSystem();
196
+ break;
197
+ }
198
+ case "point": {
199
+ graph = randomGraphTypePoint();
200
+ correct = randomGraphTypePoint();
201
+ break;
202
+ }
203
+ case "polygon": {
204
+ graph = randomGraphTypePolygon();
205
+ correct = randomGraphTypePolygon();
206
+ break;
207
+ }
208
+ case "quadratic": {
209
+ graph = randomGraphTypeQuadratic();
210
+ correct = randomGraphTypeQuadratic();
211
+ break;
212
+ }
213
+ case "ray": {
214
+ graph = randomGraphTypeRay();
215
+ correct = randomGraphTypeRay();
216
+ break;
217
+ }
218
+ case "segment": {
219
+ graph = randomGraphTypeSegment();
220
+ correct = randomGraphTypeSegment();
221
+ break;
222
+ }
223
+ case "sinusoid": {
224
+ graph = randomGraphTypeSinusoid();
225
+ correct = randomGraphTypeSinusoid();
226
+ break;
227
+ }
228
+ default: {
229
+ graph = randomGraphTypeAngle();
230
+ correct = randomGraphTypeAngle();
231
+ }
232
+ }
233
+
234
+ return {
235
+ content: `[[\u2603 interactive-graph 1]]`,
236
+ images: {},
237
+ widgets: {
238
+ "interactive-graph 1": {
239
+ graded: randomBoolean(),
240
+ version: {
241
+ major: 0,
242
+ minor: 0,
243
+ },
244
+ static: false, //always false for this widget
245
+ type: "interactive-graph",
246
+ options: {
247
+ rulerTicks: randomInteger(1, 100),
248
+ showProtractor: randomBoolean(0.2),
249
+ graph,
250
+ snapStep: [
251
+ randomInteger(1, 10) / 10,
252
+ randomInteger(1, 10) / 10,
253
+ ],
254
+ labels: [randomLetter() || "x", randomLetter() || "y"],
255
+ step: [randomInteger(1, 10), randomInteger(1, 10)],
256
+ gridStep: [randomInteger(1, 10), randomInteger(1, 10)],
257
+ backgroundImage: randomBackgroundImage(),
258
+ range: [randomRange(), randomRange()],
259
+ showRuler: randomBoolean(0.2),
260
+ markings:
261
+ randomElement(["none", "graph", "grid"]) || "none",
262
+ rulerLabel: randomElement(["", randomWord()]) || "",
263
+ correct,
264
+ },
265
+ alignment: "default",
266
+ },
267
+ },
268
+ };
269
+ };
@@ -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
+ }
@@ -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
  }}
@@ -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