@khanacademy/perseus-editor 2.16.2 → 2.17.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 +13 -0
- package/dist/es/index.js +11 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/editor.test.tsx +120 -98
- package/src/__tests__/hint-editor.test.tsx +83 -0
- package/src/editor.tsx +7 -0
- package/src/hint-editor.tsx +5 -0
- package/tsconfig-build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Perseus editors",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.17.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@khanacademy/kas": "^0.3.7",
|
|
26
26
|
"@khanacademy/kmath": "^0.1.8",
|
|
27
27
|
"@khanacademy/math-input": "^16.5.0",
|
|
28
|
-
"@khanacademy/perseus": "^17.
|
|
28
|
+
"@khanacademy/perseus": "^17.8.0",
|
|
29
29
|
"@khanacademy/perseus-core": "1.4.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
@@ -1,120 +1,142 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ApiOptions,
|
|
3
|
+
Dependencies,
|
|
4
|
+
Widgets,
|
|
5
|
+
widgets,
|
|
6
|
+
Util,
|
|
7
|
+
} from "@khanacademy/perseus";
|
|
2
8
|
import {render, screen} from "@testing-library/react";
|
|
3
9
|
import userEvent from "@testing-library/user-event";
|
|
4
10
|
import * as React from "react";
|
|
5
11
|
import "@testing-library/jest-dom"; // Imports custom mathers
|
|
6
12
|
|
|
7
13
|
import {testDependencies} from "../../../../testing/test-dependencies";
|
|
8
|
-
import {wait} from "../../../../testing/wait";
|
|
9
|
-
import {question1} from "../__testdata__/input-number.testdata";
|
|
10
14
|
import Editor from "../editor";
|
|
15
|
+
import ImageEditor from "../widgets/image-editor";
|
|
16
|
+
|
|
17
|
+
import type {PropsFor} from "@khanacademy/wonder-blocks-core";
|
|
18
|
+
|
|
19
|
+
const Harnessed = (props: Partial<PropsFor<typeof Editor>>) => {
|
|
20
|
+
return (
|
|
21
|
+
<Editor
|
|
22
|
+
apiOptions={ApiOptions.defaults}
|
|
23
|
+
onChange={() => {}}
|
|
24
|
+
content="[[☃ image 1]]"
|
|
25
|
+
widgets={{
|
|
26
|
+
"image 1": {
|
|
27
|
+
type: "image",
|
|
28
|
+
options: {
|
|
29
|
+
backgroundImage: {
|
|
30
|
+
url: "http://placekitten.com/200/300",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
11
39
|
|
|
12
40
|
describe("Editor", () => {
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
43
|
+
const ImageWidget = widgets.find((w) => w.name === "image")!;
|
|
44
|
+
expect(ImageWidget).toBeDefined();
|
|
45
|
+
Widgets.registerWidget("image", ImageWidget);
|
|
46
|
+
Widgets.registerEditors([ImageEditor]);
|
|
47
|
+
});
|
|
48
|
+
|
|
13
49
|
beforeEach(() => {
|
|
14
50
|
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
|
|
15
51
|
testDependencies,
|
|
16
52
|
);
|
|
17
53
|
});
|
|
18
54
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
it("should delete widget if confirmed", async () => {
|
|
56
|
+
const onChangeMock = jest.fn();
|
|
57
|
+
jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
58
|
+
|
|
59
|
+
// Arrange
|
|
60
|
+
render(<Harnessed onChange={onChangeMock} />);
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
userEvent.click(
|
|
64
|
+
screen.getByRole("button", {name: "Remove image widget"}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(onChangeMock).toHaveBeenCalledWith({content: ""});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should NOT delete widget if not confirmed", async () => {
|
|
72
|
+
const onChangeMock = jest.fn();
|
|
73
|
+
jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
74
|
+
|
|
75
|
+
// Arrange
|
|
76
|
+
render(<Harnessed onChange={onChangeMock} />);
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
userEvent.click(
|
|
80
|
+
screen.getByRole("button", {name: "Remove image widget"}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
84
|
+
expect(onChangeMock).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("clicking on the widget editor should open it", async () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
render(<Harnessed />);
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
const widgetDisclosure = screen.getByRole("link", {
|
|
93
|
+
name: "image 1",
|
|
28
94
|
});
|
|
95
|
+
userEvent.click(widgetDisclosure);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
const previewImage = screen.getByAltText("Editor preview of image");
|
|
99
|
+
expect(previewImage).toHaveAttribute(
|
|
100
|
+
"src",
|
|
101
|
+
"http://placekitten.com/200/300",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should update values", async () => {
|
|
106
|
+
// Arrange
|
|
107
|
+
jest.spyOn(Util, "getImageSizeModern").mockResolvedValue([200, 200]);
|
|
29
108
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
placeholder=""
|
|
37
|
-
widgets={question1.widgets}
|
|
38
|
-
images={question1.images}
|
|
39
|
-
disabled={false}
|
|
40
|
-
widgetEnabled={true}
|
|
41
|
-
immutableWidgets={false}
|
|
42
|
-
showWordCount={true}
|
|
43
|
-
warnNoPrompt={true}
|
|
44
|
-
warnNoWidgets={true}
|
|
45
|
-
onChange={(props) => {}}
|
|
46
|
-
/>,
|
|
47
|
-
);
|
|
48
|
-
await wait();
|
|
49
|
-
|
|
50
|
-
// Act
|
|
51
|
-
const widgetDisclosure = screen.getByRole("link", {
|
|
52
|
-
name: "input-number 1",
|
|
53
|
-
});
|
|
54
|
-
userEvent.click(widgetDisclosure);
|
|
55
|
-
const correctAnswerInput = screen.getByLabelText("Correct answer:");
|
|
56
|
-
|
|
57
|
-
// Assert
|
|
58
|
-
expect(correctAnswerInput).toHaveValue("0.5");
|
|
109
|
+
const changeFn = jest.fn();
|
|
110
|
+
render(<Harnessed onChange={changeFn} />);
|
|
111
|
+
|
|
112
|
+
// Act
|
|
113
|
+
const widgetDisclosure = screen.getByRole("link", {
|
|
114
|
+
name: "image 1",
|
|
59
115
|
});
|
|
116
|
+
userEvent.click(widgetDisclosure);
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
await wait();
|
|
81
|
-
|
|
82
|
-
// Act
|
|
83
|
-
const widgetDisclosure = screen.getByRole("link", {
|
|
84
|
-
name: "input-number 1",
|
|
85
|
-
});
|
|
86
|
-
userEvent.click(widgetDisclosure);
|
|
87
|
-
const correctAnswerInput = screen.getByLabelText("Correct answer:");
|
|
88
|
-
|
|
89
|
-
userEvent.clear(correctAnswerInput);
|
|
90
|
-
userEvent.paste(correctAnswerInput, "0.75");
|
|
91
|
-
userEvent.tab(); // blurring the input triggers onChange to be called
|
|
92
|
-
|
|
93
|
-
// Assert
|
|
94
|
-
expect(changeFn).toHaveBeenCalledWith(
|
|
95
|
-
{
|
|
96
|
-
widgets: {
|
|
97
|
-
"input-number 1": {
|
|
98
|
-
graded: true,
|
|
99
|
-
version: {major: 0, minor: 0},
|
|
100
|
-
static: false,
|
|
101
|
-
type: "input-number",
|
|
102
|
-
options: {
|
|
103
|
-
value: 0.75,
|
|
104
|
-
simplify: "required",
|
|
105
|
-
size: "normal",
|
|
106
|
-
inexact: false,
|
|
107
|
-
maxError: 0.1,
|
|
108
|
-
answerType: "number",
|
|
109
|
-
rightAlign: false,
|
|
110
|
-
},
|
|
111
|
-
alignment: "default",
|
|
112
|
-
},
|
|
113
|
-
},
|
|
118
|
+
const captionInput = screen.getByLabelText(/Caption:/);
|
|
119
|
+
|
|
120
|
+
userEvent.clear(captionInput);
|
|
121
|
+
userEvent.type(captionInput, "A picture of kittens");
|
|
122
|
+
userEvent.tab(); // blurring the input triggers onChange to be called
|
|
123
|
+
jest.runOnlyPendingTimers();
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
expect(changeFn).toHaveBeenCalledWith(
|
|
127
|
+
{
|
|
128
|
+
widgets: {
|
|
129
|
+
"image 1": expect.objectContaining({
|
|
130
|
+
type: "image",
|
|
131
|
+
graded: true,
|
|
132
|
+
options: expect.objectContaining({
|
|
133
|
+
caption: "A picture of kittens",
|
|
134
|
+
}),
|
|
135
|
+
}),
|
|
114
136
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
},
|
|
138
|
+
undefined,
|
|
139
|
+
undefined,
|
|
140
|
+
);
|
|
119
141
|
});
|
|
120
142
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {render, screen} from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import CombinedHintsEditor from "../hint-editor";
|
|
7
|
+
|
|
8
|
+
describe("CombinedHintsEditor", () => {
|
|
9
|
+
it("should render", () => {
|
|
10
|
+
render(
|
|
11
|
+
<CombinedHintsEditor deviceType="phone" previewURL="about:blank" />,
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should confirm before removing a hint", () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
const comfirmSpy = jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
18
|
+
render(
|
|
19
|
+
<CombinedHintsEditor
|
|
20
|
+
deviceType="phone"
|
|
21
|
+
previewURL="about:blank"
|
|
22
|
+
hints={[
|
|
23
|
+
{content: "You know this one!", widgets: {}, images: {}},
|
|
24
|
+
{content: "Ok, the answer is 3", widgets: {}, images: {}},
|
|
25
|
+
]}
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
userEvent.click(screen.getAllByText("Remove this hint")[0]);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(comfirmSpy).toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should not remove the hint if not confirmed", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
39
|
+
const onChangeMock = jest.fn();
|
|
40
|
+
render(
|
|
41
|
+
<CombinedHintsEditor
|
|
42
|
+
deviceType="phone"
|
|
43
|
+
previewURL="about:blank"
|
|
44
|
+
hints={[
|
|
45
|
+
{content: "You know this one!", widgets: {}, images: {}},
|
|
46
|
+
{content: "Ok, the answer is 3", widgets: {}, images: {}},
|
|
47
|
+
]}
|
|
48
|
+
onChange={onChangeMock}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
userEvent.click(screen.getAllByText("Remove this hint")[0]);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(onChangeMock).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should remove the hint if confirmed", () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
62
|
+
const onChangeMock = jest.fn();
|
|
63
|
+
render(
|
|
64
|
+
<CombinedHintsEditor
|
|
65
|
+
deviceType="phone"
|
|
66
|
+
previewURL="about:blank"
|
|
67
|
+
hints={[
|
|
68
|
+
{content: "You know this one!", widgets: {}, images: {}},
|
|
69
|
+
{content: "Ok, the answer is 3", widgets: {}, images: {}},
|
|
70
|
+
]}
|
|
71
|
+
onChange={onChangeMock}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Act
|
|
76
|
+
userEvent.click(screen.getAllByText("Remove this hint")[0]);
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(onChangeMock).toHaveBeenCalledWith({
|
|
80
|
+
hints: [{content: "Ok, the answer is 3", widgets: {}, images: {}}],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/editor.tsx
CHANGED
|
@@ -285,6 +285,11 @@ class Editor extends React.Component<Props, State> {
|
|
|
285
285
|
};
|
|
286
286
|
|
|
287
287
|
_handleWidgetEditorRemove: (id: string) => void = (id: string) => {
|
|
288
|
+
// eslint-disable-next-line no-alert
|
|
289
|
+
if (!confirm("Are you sure you want to delete this item?")) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
288
293
|
const textarea = this.textarea.current;
|
|
289
294
|
const re = new RegExp(widgetRegExp.replace("{id}", id), "gm");
|
|
290
295
|
this.props.onChange({content: textarea?.value.replace(re, "")});
|
|
@@ -905,6 +910,8 @@ class Editor extends React.Component<Props, State> {
|
|
|
905
910
|
underlayPieces = [];
|
|
906
911
|
|
|
907
912
|
for (let i = 0; i < pieces.length; i++) {
|
|
913
|
+
// We split on widgets so every even-numbered indexed piece is
|
|
914
|
+
// text and odd-numbered indexes are the widget references.
|
|
908
915
|
if (i % 2 === 0) {
|
|
909
916
|
// Normal text
|
|
910
917
|
underlayPieces.push(pieces[i]);
|
package/src/hint-editor.tsx
CHANGED
|
@@ -334,6 +334,11 @@ class CombinedHintsEditor extends React.Component<CombinedHintsEditorProps> {
|
|
|
334
334
|
};
|
|
335
335
|
|
|
336
336
|
handleHintRemove: (i: number) => void = (i: number) => {
|
|
337
|
+
// eslint-disable-next-line no-alert
|
|
338
|
+
if (!confirm("Are you sure you want to delete this hint?")) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
337
342
|
const hints = _(this.props.hints).clone();
|
|
338
343
|
// @ts-expect-error - TS2339 - Property 'splice' does not exist on type 'Hint'.
|
|
339
344
|
hints.splice(i, 1);
|