@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.
- package/CHANGELOG.md +43 -0
- package/dist/components/svg-image.d.ts +10 -9
- package/dist/es/index.css +0 -7
- package/dist/es/index.js +37 -31
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +0 -7
- package/dist/index.js +61 -49
- package/dist/index.js.map +1 -1
- package/dist/renderer.d.ts +2 -2
- package/dist/widgets/matcher.d.ts +1 -1
- package/package.json +35 -35
- package/src/__stories__/hints-renderer.stories.tsx +41 -44
- package/src/__tests__/__snapshots__/renderer.test.tsx.snap +9 -33
- package/src/__tests__/article-renderer.test.tsx +2 -3
- package/src/__tests__/renderer-api.test.tsx +35 -52
- package/src/components/graph.tsx +14 -8
- package/src/components/svg-image.tsx +13 -11
- package/src/mixins/provide-keypad.tsx +0 -1
- package/src/perseus-types.ts +8 -3
- package/src/renderer.tsx +2 -2
- package/src/styles/khan-exercise.css +0 -2
- package/src/styles/perseus-renderer.less +0 -7
- package/src/widgets/__stories__/random-widgets.stories.tsx +59 -0
- package/src/widgets/__stories__/test-keypad-context-wrapper.tsx +0 -1
- package/src/widgets/__testdata__/categorizer.testdata.ts +40 -0
- package/src/widgets/__testdata__/dropdown.testdata.ts +38 -0
- package/src/widgets/__testdata__/explanation.testdata.ts +30 -0
- package/src/widgets/__testdata__/expression.testdata.ts +82 -0
- package/src/widgets/__testdata__/interactive-graph-random.testdata.ts +269 -0
- package/src/widgets/__testdata__/radio.testdata.ts +63 -0
- package/src/widgets/__testdata__/randomizers.ts +51 -0
- package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +6 -22
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt.test.ts.snap +3 -3
- package/src/widgets/__tests__/__snapshots__/graded-group-set.test.ts.snap +1 -1
- package/src/widgets/__tests__/__snapshots__/graded-group.test.ts.snap +1 -1
- package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +4 -4
- package/src/widgets/__tests__/expression-mobile.test.tsx +0 -1
- package/src/widgets/__tests__/matcher.test.tsx +1 -1
- package/src/widgets/__tests__/randomizers.test.ts +53 -0
- package/src/widgets/image.tsx +27 -20
- package/src/widgets/matcher.tsx +1 -1
- package/src/widgets/radio/choice.tsx +1 -0
- 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
|
-
<
|
|
43
|
+
<span
|
|
44
44
|
aria-hidden="true"
|
|
45
|
-
class="svg_aeiopc-o_O-caret_ntkmq"
|
|
46
|
-
|
|
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
|
-
<
|
|
108
|
+
<span
|
|
117
109
|
aria-hidden="true"
|
|
118
|
-
class="svg_aeiopc-o_O-caret_ntkmq"
|
|
119
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
701
|
+
class="text_f1191h-o_O-LabelLarge_5s82ln-o_O-text_6kf3xs"
|
|
702
702
|
>
|
|
703
703
|
Check
|
|
704
704
|
</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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4198
|
+
class="perseus-radio-rationale-content rationale_1ctj0s0"
|
|
4199
4199
|
data-test-id="perseus-radio-rationale-content-3"
|
|
4200
4200
|
>
|
|
4201
4201
|
<div
|
|
@@ -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 "
|
|
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
|
+
});
|
package/src/widgets/image.tsx
CHANGED
|
@@ -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
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
package/src/widgets/matcher.tsx
CHANGED